@intellectif/lk-react 16.1.0 → 17.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 (148) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +4 -2
  3. package/dist/{WrittenResponse-xJ7I_5yq.d.ts → WrittenResponse-B4kY-U0v.d.ts} +1 -1
  4. package/dist/{WrittenResponse-6tSj6U3G.d.cts → WrittenResponse-Dl3zah22.d.cts} +1 -1
  5. package/dist/ai/LkAiProvider.cjs +17 -0
  6. package/dist/ai/LkAiProvider.cjs.map +1 -0
  7. package/dist/ai/LkAiProvider.d.cts +60 -0
  8. package/dist/ai/LkAiProvider.d.ts +60 -0
  9. package/dist/ai/LkAiProvider.js +4 -0
  10. package/dist/ai/LkAiProvider.js.map +1 -0
  11. package/dist/{chunk-PXGJHZKJ.cjs → chunk-3FBBP5QR.cjs} +6 -6
  12. package/dist/{chunk-PXGJHZKJ.cjs.map → chunk-3FBBP5QR.cjs.map} +1 -1
  13. package/dist/{chunk-WOYCT5M6.cjs → chunk-3TJSFPO4.cjs} +26 -22
  14. package/dist/chunk-3TJSFPO4.cjs.map +1 -0
  15. package/dist/chunk-5F4Q6JT7.cjs +289 -0
  16. package/dist/chunk-5F4Q6JT7.cjs.map +1 -0
  17. package/dist/{chunk-3UXRX4KU.js → chunk-6DTW5T3Z.js} +39 -7
  18. package/dist/chunk-6DTW5T3Z.js.map +1 -0
  19. package/dist/{chunk-ML6L4LJH.cjs → chunk-6RRFH7C4.cjs} +25 -25
  20. package/dist/{chunk-ML6L4LJH.cjs.map → chunk-6RRFH7C4.cjs.map} +1 -1
  21. package/dist/{chunk-JPDR6TTA.js → chunk-7K4B2EMU.js} +4 -4
  22. package/dist/{chunk-JPDR6TTA.js.map → chunk-7K4B2EMU.js.map} +1 -1
  23. package/dist/{chunk-MQAZJQRE.cjs → chunk-AHGB5HH6.cjs} +13 -2
  24. package/dist/chunk-AHGB5HH6.cjs.map +1 -0
  25. package/dist/{chunk-5QHVYOME.js → chunk-ALH6TUX2.js} +39 -7
  26. package/dist/chunk-ALH6TUX2.js.map +1 -0
  27. package/dist/{chunk-2S3MK4VX.cjs → chunk-BAKLJKFZ.cjs} +5 -5
  28. package/dist/{chunk-2S3MK4VX.cjs.map → chunk-BAKLJKFZ.cjs.map} +1 -1
  29. package/dist/{chunk-ZPCO3CEB.js → chunk-BY4UQH2Y.js} +3 -3
  30. package/dist/{chunk-ZPCO3CEB.js.map → chunk-BY4UQH2Y.js.map} +1 -1
  31. package/dist/chunk-GR33G5QM.js +286 -0
  32. package/dist/chunk-GR33G5QM.js.map +1 -0
  33. package/dist/{chunk-ZEDMFTKD.js → chunk-H3IRVX7J.js} +10 -10
  34. package/dist/{chunk-ZEDMFTKD.js.map → chunk-H3IRVX7J.js.map} +1 -1
  35. package/dist/{chunk-PTJTC6A3.cjs → chunk-H63O5HPW.cjs} +23 -14
  36. package/dist/chunk-H63O5HPW.cjs.map +1 -0
  37. package/dist/{chunk-IQI5I2X5.js → chunk-HZMILQYO.js} +25 -7
  38. package/dist/chunk-HZMILQYO.js.map +1 -0
  39. package/dist/{chunk-ZSY4VANQ.cjs → chunk-JKMBFHER.cjs} +47 -15
  40. package/dist/chunk-JKMBFHER.cjs.map +1 -0
  41. package/dist/{chunk-GUPBIEUA.js → chunk-KP57YCYQ.js} +6 -6
  42. package/dist/{chunk-GUPBIEUA.js.map → chunk-KP57YCYQ.js.map} +1 -1
  43. package/dist/{chunk-GDMM2WEG.js → chunk-KWKB74KL.js} +16 -12
  44. package/dist/chunk-KWKB74KL.js.map +1 -0
  45. package/dist/{chunk-TRP5Z3XW.cjs → chunk-L7WY5YFQ.cjs} +5 -5
  46. package/dist/{chunk-TRP5Z3XW.cjs.map → chunk-L7WY5YFQ.cjs.map} +1 -1
  47. package/dist/{chunk-B6S5ADHR.cjs → chunk-QA42VDTA.cjs} +47 -15
  48. package/dist/chunk-QA42VDTA.cjs.map +1 -0
  49. package/dist/{chunk-VC2MPB2Y.js → chunk-QHK6PTUR.js} +39 -7
  50. package/dist/chunk-QHK6PTUR.js.map +1 -0
  51. package/dist/{chunk-BI547RIA.js → chunk-QJW3K6JN.js} +4 -4
  52. package/dist/{chunk-BI547RIA.js.map → chunk-QJW3K6JN.js.map} +1 -1
  53. package/dist/{chunk-RSQFGF5W.cjs → chunk-RZ52SEYV.cjs} +33 -15
  54. package/dist/chunk-RZ52SEYV.cjs.map +1 -0
  55. package/dist/{chunk-2TFNDHJ5.cjs → chunk-TI23GIFM.cjs} +47 -15
  56. package/dist/chunk-TI23GIFM.cjs.map +1 -0
  57. package/dist/{chunk-DFMBVA7D.cjs → chunk-UQIRDE52.cjs} +8 -8
  58. package/dist/{chunk-DFMBVA7D.cjs.map → chunk-UQIRDE52.cjs.map} +1 -1
  59. package/dist/{chunk-DVOPHC2Q.js → chunk-UYQBEPNZ.js} +13 -9
  60. package/dist/chunk-UYQBEPNZ.js.map +1 -0
  61. package/dist/{chunk-2XNCPLHU.js → chunk-V5BTVGPA.js} +16 -7
  62. package/dist/chunk-V5BTVGPA.js.map +1 -0
  63. package/dist/{chunk-FZWWVJE4.cjs → chunk-XEGBGKHV.cjs} +29 -25
  64. package/dist/chunk-XEGBGKHV.cjs.map +1 -0
  65. package/dist/chunk-YNDKAYW3.js +17 -0
  66. package/dist/chunk-YNDKAYW3.js.map +1 -0
  67. package/dist/{chunk-KSAINXRG.js → chunk-YRNEOBXA.js} +13 -2
  68. package/dist/chunk-YRNEOBXA.js.map +1 -0
  69. package/dist/{chunk-DKZWUIQD.js → chunk-YTGRXD23.js} +3 -3
  70. package/dist/{chunk-DKZWUIQD.js.map → chunk-YTGRXD23.js.map} +1 -1
  71. package/dist/{chunk-OO24JGYH.cjs → chunk-Z5XY66PO.cjs} +18 -18
  72. package/dist/{chunk-OO24JGYH.cjs.map → chunk-Z5XY66PO.cjs.map} +1 -1
  73. package/dist/chunk-ZDYNAWDZ.cjs +20 -0
  74. package/dist/chunk-ZDYNAWDZ.cjs.map +1 -0
  75. package/dist/components/ActivityPreview.cjs +14 -12
  76. package/dist/components/ActivityPreview.d.cts +3 -2
  77. package/dist/components/ActivityPreview.d.ts +3 -2
  78. package/dist/components/ActivityPreview.js +13 -11
  79. package/dist/components/ActivitySequence.cjs +15 -13
  80. package/dist/components/ActivitySequence.d.cts +12 -3
  81. package/dist/components/ActivitySequence.d.ts +12 -3
  82. package/dist/components/ActivitySequence.js +14 -12
  83. package/dist/components/Dictation.cjs +7 -5
  84. package/dist/components/Dictation.d.cts +3 -1
  85. package/dist/components/Dictation.d.ts +3 -1
  86. package/dist/components/Dictation.js +6 -4
  87. package/dist/components/FillInTheBlanks.cjs +7 -5
  88. package/dist/components/FillInTheBlanks.d.cts +3 -1
  89. package/dist/components/FillInTheBlanks.d.ts +3 -1
  90. package/dist/components/FillInTheBlanks.js +6 -4
  91. package/dist/components/GapSelect.cjs +7 -5
  92. package/dist/components/GapSelect.d.cts +3 -1
  93. package/dist/components/GapSelect.d.ts +3 -1
  94. package/dist/components/GapSelect.js +6 -4
  95. package/dist/components/InteractiveVideo.cjs +14 -12
  96. package/dist/components/InteractiveVideo.d.cts +30 -2
  97. package/dist/components/InteractiveVideo.d.ts +30 -2
  98. package/dist/components/InteractiveVideo.js +12 -10
  99. package/dist/components/MultipleChoice.cjs +7 -5
  100. package/dist/components/MultipleChoice.d.cts +3 -1
  101. package/dist/components/MultipleChoice.d.ts +3 -1
  102. package/dist/components/MultipleChoice.js +6 -4
  103. package/dist/components/PronunciationFeedback.cjs +4 -4
  104. package/dist/components/PronunciationFeedback.d.cts +3 -1
  105. package/dist/components/PronunciationFeedback.d.ts +3 -1
  106. package/dist/components/PronunciationFeedback.js +3 -3
  107. package/dist/components/ReadAloud.cjs +6 -6
  108. package/dist/components/ReadAloud.d.cts +4 -2
  109. package/dist/components/ReadAloud.d.ts +4 -2
  110. package/dist/components/ReadAloud.js +5 -5
  111. package/dist/components/StimulusPanel.cjs +4 -4
  112. package/dist/components/StimulusPanel.d.cts +3 -1
  113. package/dist/components/StimulusPanel.d.ts +3 -1
  114. package/dist/components/StimulusPanel.js +3 -3
  115. package/dist/components/WrittenResponse.cjs +5 -5
  116. package/dist/components/WrittenResponse.d.cts +5 -3
  117. package/dist/components/WrittenResponse.d.ts +5 -3
  118. package/dist/components/WrittenResponse.js +4 -4
  119. package/dist/i18n/LkIntlProvider.cjs +7 -7
  120. package/dist/i18n/LkIntlProvider.d.cts +4 -2
  121. package/dist/i18n/LkIntlProvider.d.ts +4 -2
  122. package/dist/i18n/LkIntlProvider.js +1 -1
  123. package/dist/index.cjs +47 -37
  124. package/dist/index.cjs.map +1 -1
  125. package/dist/index.d.cts +4 -3
  126. package/dist/index.d.ts +4 -3
  127. package/dist/index.js +17 -15
  128. package/dist/index.js.map +1 -1
  129. package/dist/{strings-C6wM5aUQ.d.cts → strings-BOPKhnS-.d.cts} +30 -0
  130. package/dist/{strings-Cd_2F-Bw.d.ts → strings-PCYKWCwd.d.ts} +30 -0
  131. package/dist/theme/skin.css +90 -0
  132. package/package.json +13 -3
  133. package/dist/chunk-2TFNDHJ5.cjs.map +0 -1
  134. package/dist/chunk-2XNCPLHU.js.map +0 -1
  135. package/dist/chunk-3UXRX4KU.js.map +0 -1
  136. package/dist/chunk-5QHVYOME.js.map +0 -1
  137. package/dist/chunk-B6S5ADHR.cjs.map +0 -1
  138. package/dist/chunk-DVOPHC2Q.js.map +0 -1
  139. package/dist/chunk-FZWWVJE4.cjs.map +0 -1
  140. package/dist/chunk-GDMM2WEG.js.map +0 -1
  141. package/dist/chunk-IQI5I2X5.js.map +0 -1
  142. package/dist/chunk-KSAINXRG.js.map +0 -1
  143. package/dist/chunk-MQAZJQRE.cjs.map +0 -1
  144. package/dist/chunk-PTJTC6A3.cjs.map +0 -1
  145. package/dist/chunk-RSQFGF5W.cjs.map +0 -1
  146. package/dist/chunk-VC2MPB2Y.js.map +0 -1
  147. package/dist/chunk-WOYCT5M6.cjs.map +0 -1
  148. package/dist/chunk-ZSY4VANQ.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @intellectif/lk-react
2
2
 
3
+ ## 17.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 15a3435: Learners can ask an AI for help. "Explain my answer" appears under a graded answer, and "Get a hint"
8
+ before submit, in `<MultipleChoice>`, `<FillInTheBlanks>` and `<GapSelect>`. `<Dictation>` gets
9
+ explanations only; it has word hints of its own.
10
+
11
+ - **Your model, through ports you supply.** `<LkAiProvider ai={{ explain, hint, maxHints, learnerLocale }}>`
12
+ (new subpath `@intellectif/lk-react/ai/LkAiProvider`, also on the root) supplies them. So does an
13
+ `ai` prop on every activity, on `<ActivitySequence>` and on `<InteractiveVideo>`. A component's own
14
+ `ai` wins over the provider's, whole. A question a host draws — a `renderers` override, or the video's
15
+ `renderQuestion` (as the new `ai` field of `InteractiveVideoQuestion`) — is handed the ports in force,
16
+ except in `exam`. Leave a port out and that help does not appear.
17
+ - **Where it appears:**
18
+ - hints in `practice` before submit, up to `maxHints` (3 by default, at most 10), on a question that
19
+ is not disabled;
20
+ - an explanation in `practice` after submit, or in `review` when `outcome` is scored;
21
+ - **nothing in `exam`**, whatever is passed;
22
+ - nothing an item's author switched off with `ai: { hints: false }` or `ai: { explanations: false }`.
23
+ - **What a learner sees is checked first.** A hint containing the answer, an explanation of a verdict
24
+ other than the SDK's, and malformed, empty or over-long text are all refused. The learner sees "not
25
+ available" and can ask again, and a development build logs the reason. A refused hint is not
26
+ counted.
27
+ - **Everything a model wrote is marked:** "Written by AI. It can make mistakes."
28
+ - **A call only when the learner presses a button**, with an `AbortSignal` that fires when the answer
29
+ it was about goes away. A late answer is dropped either way, and a failure never blocks the
30
+ question.
31
+ - **For accessibility:** focus moves to an explanation when it arrives, and the buttons stay focusable
32
+ while working (`aria-busy`, `aria-disabled`).
33
+ - **Records:** `ai-hint-shown` and `ai-explanation-shown` reach `onInteraction`, each with the port's
34
+ `provenance`.
35
+ - **Styling and strings:** eleven new strings (`aiExplain` … `aiNotice`, all in `docs/i18n.md`) and
36
+ `.lk-ai-*` classes in the optional skin.
37
+
38
+ **Major** because the peer range moves to `@intellectif/lk-core@^0.17.0`, whose `ai` field can
39
+ refuse stored content (see its changelog). A host that passes no AI port sees nothing new.
40
+
41
+ See `docs/ai.md`.
42
+
43
+ ### Patch Changes
44
+
45
+ - 15a3435: `<WrittenResponse>` gives every inline correction in a returned grade a list key of its own. A grade
46
+ that carries the same correction twice — the same mistake made twice in one essay — used to render
47
+ both rows under one key. React warns about that, and when the grade re-renders it can drop or
48
+ duplicate a row. A correction is now keyed by its `range`, or by its position when it has no range
49
+ or repeats one the grader already used. No API changes.
50
+ - Updated dependencies [15a3435]
51
+ - @intellectif/lk-core@0.17.0
52
+
53
+ ## 16.1.1
54
+
55
+ ### Patch Changes
56
+
57
+ - 287c896: Documentation: `<InteractiveVideo>` is a formative tool, for lessons and practice — not for official
58
+ summative exams, which `<ActivitySequence>` delivers. The learner controls the recording while the
59
+ questions are open (rewinds it, slows it, reads its transcript), a quiz can be reopened, and nothing
60
+ in the player times a question or holds a single pass. Its `exam` mode is a check-in without
61
+ feedback, not exam conditions; its questions still grade exactly as they do anywhere else.
62
+
63
+ The component's and `renderMode`'s documentation say so now, as do the README and the interactive
64
+ video guide. No behaviour changes.
65
+
3
66
  ## 16.1.0
4
67
 
5
68
  ### Minor Changes
package/README.md CHANGED
@@ -72,7 +72,8 @@ export function Demo() {
72
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
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.
74
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.
75
- - **`<InteractiveVideo>`** — a video that stops at each quiz and asks its questions over itself, then plays on. One player: a progress bar that marks every quiz and previews the caption under the pointer, speed, volume, captions drawn from WebVTT it parses itself (through a `captionsLoader` when the file needs the learner's credentials) — in one language or two at once, a second line under the first for a learner matching what they hear to what it means — a contents and transcript panel, picture-in-picture, fullscreen and a keyboard map with a shortcut sheet. A quiz covers the video and grows the player rather than scrolling inside it, every question of an opened quiz stays mounted so a rewind loses no answer, the controls are `inert` while it is open, and a **required** quiz holds both playback and seeking until it is answered. Answers report with the slot the question sits in and the quiz it belongs to, and `onProgress` gives a resume point that is never past an unanswered required quiz. A host with renderers of its own draws any question itself through `renderQuestion` — the video still counts it, tells it when it leaves the screen so it can stop its microphone, and holds Finish while its answer is still on its way. See the [interactive video guide](https://github.com/intellectif/learning-kit/blob/main/docs/interactive-video.md).
75
+ - **`<InteractiveVideo>`** — a video that stops at each quiz and asks its questions over itself, then plays on. **A formative tool**, for lessons and practice — deliver official summative exams with `<ActivitySequence>`. One player: a progress bar that marks every quiz and previews the caption under the pointer, speed, volume, captions drawn from WebVTT it parses itself (through a `captionsLoader` when the file needs the learner's credentials) — in one language or two at once, a second line under the first for a learner matching what they hear to what it means — a contents and transcript panel, picture-in-picture, fullscreen and a keyboard map with a shortcut sheet. A quiz covers the video and grows the player rather than scrolling inside it, every question of an opened quiz stays mounted so a rewind loses no answer, the controls are `inert` while it is open, and a **required** quiz holds both playback and seeking until it is answered. Answers report with the slot the question sits in and the quiz it belongs to, and `onProgress` gives a resume point that is never past an unanswered required quiz. A host with renderers of its own draws any question itself through `renderQuestion` — the video still counts it, tells it when it leaves the screen so it can stop its microphone, and holds Finish while its answer is still on its way. See the [interactive video guide](https://github.com/intellectif/learning-kit/blob/main/docs/interactive-video.md).
76
+ - **AI help for learners** — "Explain my answer" after grading and "Get a hint" before submit, in `<MultipleChoice>`, `<FillInTheBlanks>`, `<GapSelect>` and `<Dictation>` (explanations only), through ports **you** supply with `<LkAiProvider>` or an `ai` prop (also on `<ActivitySequence>` and `<InteractiveVideo>`). The SDK never calls a model or holds a key: it builds the facts your model is given, refuses an explanation that contradicts the grade and a hint that contains the answer, marks everything a model wrote, and drops a late answer. Never in `exam`. See the [AI guide](https://github.com/intellectif/learning-kit/blob/main/docs/ai.md).
76
77
  - **`<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).
77
78
  - **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`.
78
79
  - **`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.
@@ -91,7 +92,7 @@ export function Demo() {
91
92
  - **`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.
92
93
  - **`<ThemeProvider>` + `defaults.css`** — `--lk-*` design-token system; automatic dark mode via `prefers-color-scheme` (SSR-safe with `useSyncExternalStore`).
93
94
  - **`createTailwindTheme(theme)`** — optional Tailwind interop; consume the SDK palette from your own utilities.
94
- - **`<LkIntlProvider>`** — all 168 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).
95
+ - **`<LkIntlProvider>`** — all 179 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).
95
96
  - **WCAG 2.2 AA** — axe-clean unit + Playwright e2e tests; full keyboard operability; numerically-verified contrast.
96
97
  - **RSC-compatible** — every component carries `'use client'` and hydrates inside a React Server Component tree.
97
98
 
@@ -114,6 +115,7 @@ export function Demo() {
114
115
  | `@intellectif/lk-react/hooks/useActivityState` | Lifecycle + timing |
115
116
  | `@intellectif/lk-react/hooks/useSpeechRecorder` | Microphone capture to 16 kHz mono WAV, and `CAPTURE_PROCESSOR_SOURCE` for a self-hosted worklet |
116
117
  | `@intellectif/lk-react/hooks/useXAPI` | LRS delivery (retry, never-throws) |
118
+ | `@intellectif/lk-react/ai/LkAiProvider` | `<LkAiProvider>`, `useLearnerAi`, and the `LearnerAi` type |
117
119
  | `@intellectif/lk-react/i18n/LkIntlProvider` | `<LkIntlProvider>`, `useLkStrings`, `useLkDirection`, `DEFAULT_STRINGS`, `mergeStrings`, `directionForLocale` |
118
120
  | `@intellectif/lk-react/theme/ThemeProvider` | `<ThemeProvider>`, `darkTheme`, `useTheme`, `createTailwindTheme` |
119
121
  | `@intellectif/lk-react/theme/defaults.css` | Tokens (required) |
@@ -1,5 +1,5 @@
1
1
  import { XAPIStatement, WrittenResponseData, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
2
- import { f as Renderable, e as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings-Cd_2F-Bw.js';
2
+ import { f as Renderable, e as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings-PCYKWCwd.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 { f as Renderable, e as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings-C6wM5aUQ.cjs';
2
+ import { f as Renderable, e as RenderMode, H as HtmlSanitizer, M as MediaBudgetBinding, b as MediaTransportStrings, a as LkStringsOverride } from './strings-BOPKhnS-.cjs';
3
3
 
4
4
  /**
5
5
  * Payload delivered when the learner submits a written response. Grading is
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var chunkZDYNAWDZ_cjs = require('../chunk-ZDYNAWDZ.cjs');
5
+
6
+
7
+
8
+ Object.defineProperty(exports, "LkAiProvider", {
9
+ enumerable: true,
10
+ get: function () { return chunkZDYNAWDZ_cjs.LkAiProvider; }
11
+ });
12
+ Object.defineProperty(exports, "useLearnerAi", {
13
+ enumerable: true,
14
+ get: function () { return chunkZDYNAWDZ_cjs.useLearnerAi; }
15
+ });
16
+ //# sourceMappingURL=LkAiProvider.cjs.map
17
+ //# sourceMappingURL=LkAiProvider.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"LkAiProvider.cjs"}
@@ -0,0 +1,60 @@
1
+ import { AiExplanationRequest, AiTextResult, AiHintRequest } from '@intellectif/lk-core';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * The model a host runs for learners, as the components call it. The SDK never
6
+ * calls a model, holds a key or writes a prompt: each port is the host's
7
+ * function — usually a request to its own server, which holds the key and the
8
+ * prompt and chooses the model.
9
+ *
10
+ * A port is called only when the learner asks — a button, never on render —
11
+ * and gets an `AbortSignal` that fires when the answer it was asked about goes
12
+ * away. Whatever it resolves to is checked before a learner sees it: see
13
+ * `checkAiExplanation` and `checkAiHint` in lk-core. A port that rejects, or
14
+ * resolves to something refused, costs the learner the help, never the
15
+ * question.
16
+ *
17
+ * Leave a port out to switch that feature off. Nothing AI renders in `exam`,
18
+ * whatever is passed.
19
+ */
20
+ interface LearnerAi {
21
+ /** Explains a graded answer. */
22
+ explain?: (request: AiExplanationRequest, options: {
23
+ signal: AbortSignal;
24
+ }) => Promise<AiTextResult>;
25
+ /** Writes the next hint for an answer not yet submitted. */
26
+ hint?: (request: AiHintRequest, options: {
27
+ signal: AbortSignal;
28
+ }) => Promise<AiTextResult>;
29
+ /** The most hints a learner can ask for on one question. Default 3, at most 10. */
30
+ maxHints?: number;
31
+ /**
32
+ * The language to write explanations and hints in — the learner's own,
33
+ * which in a language course need not be the item's. Defaults to the
34
+ * component's `locale`.
35
+ */
36
+ learnerLocale?: string;
37
+ }
38
+ interface LkAiProviderProps {
39
+ ai: LearnerAi;
40
+ children: ReactNode;
41
+ }
42
+ /**
43
+ * Supplies the host's AI ports to every activity beneath, as `LkIntlProvider`
44
+ * supplies strings. A component's own `ai` prop wins over it, whole.
45
+ *
46
+ * ```tsx
47
+ * <LkAiProvider ai={{
48
+ * explain: (request, { signal }) => api.explain(request, signal),
49
+ * hint: (request, { signal }) => api.hint(request, signal),
50
+ * learnerLocale: learner.language,
51
+ * }}>
52
+ * <ActivitySequence … />
53
+ * </LkAiProvider>
54
+ * ```
55
+ */
56
+ declare function LkAiProvider({ ai, children }: LkAiProviderProps): React.JSX.Element;
57
+ /** The ports in force: a component's own, else the provider's, else none. */
58
+ declare function useLearnerAi(override?: LearnerAi): LearnerAi | undefined;
59
+
60
+ export { type LearnerAi, LkAiProvider, type LkAiProviderProps, useLearnerAi };
@@ -0,0 +1,60 @@
1
+ import { AiExplanationRequest, AiTextResult, AiHintRequest } from '@intellectif/lk-core';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * The model a host runs for learners, as the components call it. The SDK never
6
+ * calls a model, holds a key or writes a prompt: each port is the host's
7
+ * function — usually a request to its own server, which holds the key and the
8
+ * prompt and chooses the model.
9
+ *
10
+ * A port is called only when the learner asks — a button, never on render —
11
+ * and gets an `AbortSignal` that fires when the answer it was asked about goes
12
+ * away. Whatever it resolves to is checked before a learner sees it: see
13
+ * `checkAiExplanation` and `checkAiHint` in lk-core. A port that rejects, or
14
+ * resolves to something refused, costs the learner the help, never the
15
+ * question.
16
+ *
17
+ * Leave a port out to switch that feature off. Nothing AI renders in `exam`,
18
+ * whatever is passed.
19
+ */
20
+ interface LearnerAi {
21
+ /** Explains a graded answer. */
22
+ explain?: (request: AiExplanationRequest, options: {
23
+ signal: AbortSignal;
24
+ }) => Promise<AiTextResult>;
25
+ /** Writes the next hint for an answer not yet submitted. */
26
+ hint?: (request: AiHintRequest, options: {
27
+ signal: AbortSignal;
28
+ }) => Promise<AiTextResult>;
29
+ /** The most hints a learner can ask for on one question. Default 3, at most 10. */
30
+ maxHints?: number;
31
+ /**
32
+ * The language to write explanations and hints in — the learner's own,
33
+ * which in a language course need not be the item's. Defaults to the
34
+ * component's `locale`.
35
+ */
36
+ learnerLocale?: string;
37
+ }
38
+ interface LkAiProviderProps {
39
+ ai: LearnerAi;
40
+ children: ReactNode;
41
+ }
42
+ /**
43
+ * Supplies the host's AI ports to every activity beneath, as `LkIntlProvider`
44
+ * supplies strings. A component's own `ai` prop wins over it, whole.
45
+ *
46
+ * ```tsx
47
+ * <LkAiProvider ai={{
48
+ * explain: (request, { signal }) => api.explain(request, signal),
49
+ * hint: (request, { signal }) => api.hint(request, signal),
50
+ * learnerLocale: learner.language,
51
+ * }}>
52
+ * <ActivitySequence … />
53
+ * </LkAiProvider>
54
+ * ```
55
+ */
56
+ declare function LkAiProvider({ ai, children }: LkAiProviderProps): React.JSX.Element;
57
+ /** The ports in force: a component's own, else the provider's, else none. */
58
+ declare function useLearnerAi(override?: LearnerAi): LearnerAi | undefined;
59
+
60
+ export { type LearnerAi, LkAiProvider, type LkAiProviderProps, useLearnerAi };
@@ -0,0 +1,4 @@
1
+ 'use client';
2
+ export { LkAiProvider, useLearnerAi } from '../chunk-YNDKAYW3.js';
3
+ //# sourceMappingURL=LkAiProvider.js.map
4
+ //# sourceMappingURL=LkAiProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"LkAiProvider.js"}
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkTRP5Z3XW_cjs = require('./chunk-TRP5Z3XW.cjs');
5
- var chunkMQAZJQRE_cjs = require('./chunk-MQAZJQRE.cjs');
4
+ var chunkL7WY5YFQ_cjs = require('./chunk-L7WY5YFQ.cjs');
5
+ var chunkAHGB5HH6_cjs = require('./chunk-AHGB5HH6.cjs');
6
6
  var react = require('react');
7
7
  var jsxRuntime = require('react/jsx-runtime');
8
8
 
@@ -19,7 +19,7 @@ function StimulusPanel({
19
19
  strings
20
20
  }) {
21
21
  const titleId = react.useId();
22
- const s = chunkMQAZJQRE_cjs.useLkStrings(strings);
22
+ const s = chunkAHGB5HH6_cjs.useLkStrings(strings);
23
23
  const bodyHtml = react.useMemo(() => {
24
24
  if (sanitizeHtml === void 0 || typeof stimulus.bodyHtml !== "string") {
25
25
  return null;
@@ -33,7 +33,7 @@ function StimulusPanel({
33
33
  stimulus.title !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-stimulus-title", id: titleId, ...contentLang, children: stimulus.title }) : null,
34
34
  rangeText !== null ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-stimulus-range", children: rangeText }) : null,
35
35
  stimulus.media !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
36
- chunkTRP5Z3XW_cjs.ActivityMedia,
36
+ chunkL7WY5YFQ_cjs.ActivityMedia,
37
37
  {
38
38
  media: stimulus.media,
39
39
  ...renderMode !== void 0 ? { renderMode } : {},
@@ -59,5 +59,5 @@ function StimulusPanel({
59
59
  }
60
60
 
61
61
  exports.StimulusPanel = StimulusPanel;
62
- //# sourceMappingURL=chunk-PXGJHZKJ.cjs.map
63
- //# sourceMappingURL=chunk-PXGJHZKJ.cjs.map
62
+ //# sourceMappingURL=chunk-3FBBP5QR.cjs.map
63
+ //# sourceMappingURL=chunk-3FBBP5QR.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/StimulusPanel/StimulusPanel.tsx"],"names":["useId","useLkStrings","useMemo","jsxs","jsx","ActivityMedia"],"mappings":"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","file":"chunk-PXGJHZKJ.cjs","sourcesContent":["'use client';\n\nimport type { InteractionEvent, Stimulus } from '@intellectif/lk-core';\nimport { useId, useMemo } from 'react';\nimport { useLkStrings } from '../../i18n/LkIntlProvider.js';\nimport type { LkStringsOverride } from '../../i18n/strings.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport type {\n HtmlSanitizer,\n MediaBudgetBinding,\n MediaTransportStrings,\n RenderMode,\n} from '../types.js';\n\nexport interface StimulusPanelProps {\n stimulus: Stimulus;\n /**\n * 1-based presented positions of the first and last question this material\n * serves, rendered as \"Questions 3–8\" so a learner knows how far the\n * passage carries.\n */\n range?: { first: number; last: number };\n /** Renders `stimulus.bodyHtml` when provided. See `HtmlSanitizer`. */\n sanitizeHtml?: HtmlSanitizer;\n locale?: string;\n /**\n * Forwarded to the stimulus's own media. `review` keeps the browser's control\n * bar even under an enforcing policy — a graded paper cannot be changed by\n * listening again.\n */\n renderMode?: RenderMode;\n /**\n * Binds this group's recording to a play budget the consumer persists. One\n * recording serves every question in the group, so the key is derived from\n * the ENTRY rather than the slot — see `stimulusMediaKey()` in lk-core.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. */\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: InteractionEvent) => void;\n disabled?: boolean;\n /** Overrides the SDK's chrome text for this panel. See {@link LkIntlProvider}. */\n strings?: LkStringsOverride;\n}\n\n/**\n * Presents a shared stimulus — the passage, recording or image an item group's\n * questions refer to. A landmark region named by its title (or by its kind),\n * so a screen-reader user can jump back to the material from any question.\n *\n * Rich text follows the SDK-wide rule: `bodyHtml` is rendered only through a\n * caller-supplied sanitiser, and the plain `body` is the fallback — which the\n * schema guarantees exists whenever `bodyHtml` does.\n */\nexport function StimulusPanel({\n stimulus,\n range,\n sanitizeHtml,\n locale,\n renderMode,\n mediaBudget,\n mediaStrings,\n onInteraction,\n disabled,\n strings,\n}: StimulusPanelProps): React.JSX.Element {\n const titleId = useId();\n const s = useLkStrings(strings);\n\n const bodyHtml = useMemo(() => {\n if (sanitizeHtml === undefined || typeof stimulus.bodyHtml !== 'string') {\n return null;\n }\n return sanitizeHtml(stimulus.bodyHtml);\n }, [stimulus.bodyHtml, sanitizeHtml]);\n\n const rangeText = range === undefined ? null : s.stimulusRange(range.first, range.last);\n\n const naming =\n stimulus.title !== undefined\n ? { 'aria-labelledby': titleId }\n : { 'aria-label': s.stimulusKind[stimulus.kind] };\n\n // The AUTHORED parts carry the stimulus language; the SDK's own chrome does\n // not. Declaring `stimulus.locale` on the whole region made a screen reader\n // read \"Recording\" and \"Questions 3–5\" — English strings this package ships\n // — in the passage's voice, so a Spanish listening panel announced its own\n // name and the only orientation text in it with Spanish phonetics\n // (WCAG 3.1.2 Language of Parts). Scope the declaration to the content it\n // actually describes.\n const contentLang = stimulus.locale !== undefined ? { lang: stimulus.locale } : {};\n\n return (\n <section className=\"lk-stimulus\" data-kind={stimulus.kind} lang={locale} {...naming}>\n {stimulus.title !== undefined ? (\n <p className=\"lk-stimulus-title\" id={titleId} {...contentLang}>\n {stimulus.title}\n </p>\n ) : null}\n {rangeText !== null ? <p className=\"lk-stimulus-range\">{rangeText}</p> : null}\n {stimulus.media !== undefined ? (\n <ActivityMedia\n media={stimulus.media}\n {...(renderMode !== undefined ? { renderMode } : {})}\n {...(mediaBudget !== undefined ? { mediaBudget } : {})}\n {...(mediaStrings !== undefined ? { mediaStrings } : {})}\n {...(strings !== undefined ? { strings } : {})}\n {...(onInteraction !== undefined ? { onInteraction } : {})}\n {...(disabled !== undefined ? { disabled } : {})}\n {...(locale !== undefined ? { locale } : {})}\n />\n ) : null}\n {bodyHtml !== null ? (\n <div\n className=\"lk-stimulus-body\"\n data-format=\"html\"\n {...contentLang}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered only through the caller-supplied HtmlSanitizer — the SDK never injects unsanitised markup\n dangerouslySetInnerHTML={{ __html: bodyHtml }}\n />\n ) : stimulus.body !== undefined ? (\n <div className=\"lk-stimulus-body\" data-format=\"text\" {...contentLang}>\n {stimulus.body}\n </div>\n ) : null}\n {stimulus.attribution !== undefined ? (\n <p className=\"lk-stimulus-attribution\" {...contentLang}>\n <cite>{stimulus.attribution}</cite>\n </p>\n ) : null}\n </section>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/components/StimulusPanel/StimulusPanel.tsx"],"names":["useId","useLkStrings","useMemo","jsxs","jsx","ActivityMedia"],"mappings":"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","file":"chunk-3FBBP5QR.cjs","sourcesContent":["'use client';\n\nimport type { InteractionEvent, Stimulus } from '@intellectif/lk-core';\nimport { useId, useMemo } from 'react';\nimport { useLkStrings } from '../../i18n/LkIntlProvider.js';\nimport type { LkStringsOverride } from '../../i18n/strings.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport type {\n HtmlSanitizer,\n MediaBudgetBinding,\n MediaTransportStrings,\n RenderMode,\n} from '../types.js';\n\nexport interface StimulusPanelProps {\n stimulus: Stimulus;\n /**\n * 1-based presented positions of the first and last question this material\n * serves, rendered as \"Questions 3–8\" so a learner knows how far the\n * passage carries.\n */\n range?: { first: number; last: number };\n /** Renders `stimulus.bodyHtml` when provided. See `HtmlSanitizer`. */\n sanitizeHtml?: HtmlSanitizer;\n locale?: string;\n /**\n * Forwarded to the stimulus's own media. `review` keeps the browser's control\n * bar even under an enforcing policy — a graded paper cannot be changed by\n * listening again.\n */\n renderMode?: RenderMode;\n /**\n * Binds this group's recording to a play budget the consumer persists. One\n * recording serves every question in the group, so the key is derived from\n * the ENTRY rather than the slot — see `stimulusMediaKey()` in lk-core.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. */\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: InteractionEvent) => void;\n disabled?: boolean;\n /** Overrides the SDK's chrome text for this panel. See {@link LkIntlProvider}. */\n strings?: LkStringsOverride;\n}\n\n/**\n * Presents a shared stimulus — the passage, recording or image an item group's\n * questions refer to. A landmark region named by its title (or by its kind),\n * so a screen-reader user can jump back to the material from any question.\n *\n * Rich text follows the SDK-wide rule: `bodyHtml` is rendered only through a\n * caller-supplied sanitiser, and the plain `body` is the fallback — which the\n * schema guarantees exists whenever `bodyHtml` does.\n */\nexport function StimulusPanel({\n stimulus,\n range,\n sanitizeHtml,\n locale,\n renderMode,\n mediaBudget,\n mediaStrings,\n onInteraction,\n disabled,\n strings,\n}: StimulusPanelProps): React.JSX.Element {\n const titleId = useId();\n const s = useLkStrings(strings);\n\n const bodyHtml = useMemo(() => {\n if (sanitizeHtml === undefined || typeof stimulus.bodyHtml !== 'string') {\n return null;\n }\n return sanitizeHtml(stimulus.bodyHtml);\n }, [stimulus.bodyHtml, sanitizeHtml]);\n\n const rangeText = range === undefined ? null : s.stimulusRange(range.first, range.last);\n\n const naming =\n stimulus.title !== undefined\n ? { 'aria-labelledby': titleId }\n : { 'aria-label': s.stimulusKind[stimulus.kind] };\n\n // The AUTHORED parts carry the stimulus language; the SDK's own chrome does\n // not. Declaring `stimulus.locale` on the whole region made a screen reader\n // read \"Recording\" and \"Questions 3–5\" — English strings this package ships\n // — in the passage's voice, so a Spanish listening panel announced its own\n // name and the only orientation text in it with Spanish phonetics\n // (WCAG 3.1.2 Language of Parts). Scope the declaration to the content it\n // actually describes.\n const contentLang = stimulus.locale !== undefined ? { lang: stimulus.locale } : {};\n\n return (\n <section className=\"lk-stimulus\" data-kind={stimulus.kind} lang={locale} {...naming}>\n {stimulus.title !== undefined ? (\n <p className=\"lk-stimulus-title\" id={titleId} {...contentLang}>\n {stimulus.title}\n </p>\n ) : null}\n {rangeText !== null ? <p className=\"lk-stimulus-range\">{rangeText}</p> : null}\n {stimulus.media !== undefined ? (\n <ActivityMedia\n media={stimulus.media}\n {...(renderMode !== undefined ? { renderMode } : {})}\n {...(mediaBudget !== undefined ? { mediaBudget } : {})}\n {...(mediaStrings !== undefined ? { mediaStrings } : {})}\n {...(strings !== undefined ? { strings } : {})}\n {...(onInteraction !== undefined ? { onInteraction } : {})}\n {...(disabled !== undefined ? { disabled } : {})}\n {...(locale !== undefined ? { locale } : {})}\n />\n ) : null}\n {bodyHtml !== null ? (\n <div\n className=\"lk-stimulus-body\"\n data-format=\"html\"\n {...contentLang}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered only through the caller-supplied HtmlSanitizer — the SDK never injects unsanitised markup\n dangerouslySetInnerHTML={{ __html: bodyHtml }}\n />\n ) : stimulus.body !== undefined ? (\n <div className=\"lk-stimulus-body\" data-format=\"text\" {...contentLang}>\n {stimulus.body}\n </div>\n ) : null}\n {stimulus.attribution !== undefined ? (\n <p className=\"lk-stimulus-attribution\" {...contentLang}>\n <cite>{stimulus.attribution}</cite>\n </p>\n ) : null}\n </section>\n );\n}\n"]}
@@ -1,13 +1,14 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkZSY4VANQ_cjs = require('./chunk-ZSY4VANQ.cjs');
5
- var chunk2TFNDHJ5_cjs = require('./chunk-2TFNDHJ5.cjs');
6
- var chunkB6S5ADHR_cjs = require('./chunk-B6S5ADHR.cjs');
7
- var chunkRSQFGF5W_cjs = require('./chunk-RSQFGF5W.cjs');
8
- var chunkML6L4LJH_cjs = require('./chunk-ML6L4LJH.cjs');
9
- var chunk2S3MK4VX_cjs = require('./chunk-2S3MK4VX.cjs');
10
- var chunkMQAZJQRE_cjs = require('./chunk-MQAZJQRE.cjs');
4
+ var chunkJKMBFHER_cjs = require('./chunk-JKMBFHER.cjs');
5
+ var chunkTI23GIFM_cjs = require('./chunk-TI23GIFM.cjs');
6
+ var chunkQA42VDTA_cjs = require('./chunk-QA42VDTA.cjs');
7
+ var chunkRZ52SEYV_cjs = require('./chunk-RZ52SEYV.cjs');
8
+ var chunkZDYNAWDZ_cjs = require('./chunk-ZDYNAWDZ.cjs');
9
+ var chunk6RRFH7C4_cjs = require('./chunk-6RRFH7C4.cjs');
10
+ var chunkBAKLJKFZ_cjs = require('./chunk-BAKLJKFZ.cjs');
11
+ var chunkAHGB5HH6_cjs = require('./chunk-AHGB5HH6.cjs');
11
12
  var lkCore = require('@intellectif/lk-core');
12
13
  var react = require('react');
13
14
  var jsxRuntime = require('react/jsx-runtime');
@@ -1218,13 +1219,14 @@ function Player(props) {
1218
1219
  theme,
1219
1220
  sanitizeHtml
1220
1221
  } = props;
1221
- const strings = chunkMQAZJQRE_cjs.useLkStrings(props.strings);
1222
+ const strings = chunkAHGB5HH6_cjs.useLkStrings(props.strings);
1223
+ const aiInForce = chunkZDYNAWDZ_cjs.useLearnerAi(props.ai);
1222
1224
  const group = react.useRef(props.group).current;
1223
1225
  const timeline = group.timeline ?? { cues: [] };
1224
1226
  const media = group.stimulus.media;
1225
1227
  const chapters = timeline.chapters ?? [];
1226
1228
  const devError = react.useMemo(() => {
1227
- if (!chunk2S3MK4VX_cjs.isDevelopment()) {
1229
+ if (!chunkBAKLJKFZ_cjs.isDevelopment()) {
1228
1230
  return null;
1229
1231
  }
1230
1232
  if (group.timeline === void 0) {
@@ -2021,7 +2023,7 @@ function Player(props) {
2021
2023
  if (reportTo.current.renderMode !== "review") {
2022
2024
  return false;
2023
2025
  }
2024
- if (chunk2S3MK4VX_cjs.isDevelopment() && !warned.has(call)) {
2026
+ if (chunkBAKLJKFZ_cjs.isDevelopment() && !warned.has(call)) {
2025
2027
  warned.add(call);
2026
2028
  console.warn(
2027
2029
  `InteractiveVideo: \`${call}()\` was called for question "${slotId}" in renderMode "review", where nothing is submittable. It was ignored.`
@@ -2112,6 +2114,7 @@ function Player(props) {
2112
2114
  defaultSubmitted: submitted.has(slot.slotId),
2113
2115
  ...outcome !== void 0 ? { outcome } : {},
2114
2116
  portalContainer,
2117
+ ...aiInForce !== void 0 && renderMode !== "exam" ? { ai: aiInForce } : {},
2115
2118
  ...callsFor(place)
2116
2119
  };
2117
2120
  };
@@ -2129,12 +2132,13 @@ function Player(props) {
2129
2132
  ...onInteraction !== void 0 ? { onInteraction } : {},
2130
2133
  ...sanitizeHtml !== void 0 ? { sanitizeHtml } : {},
2131
2134
  ...props.strings !== void 0 ? { strings: props.strings } : {},
2135
+ ...props.ai !== void 0 ? { ai: props.ai } : {},
2132
2136
  ...locale !== void 0 ? { locale } : {}
2133
2137
  };
2134
2138
  switch (activity.type) {
2135
2139
  case "multiple-choice":
2136
2140
  return /* @__PURE__ */ jsxRuntime.jsx(
2137
- chunkZSY4VANQ_cjs.MultipleChoice,
2141
+ chunkJKMBFHER_cjs.MultipleChoice,
2138
2142
  {
2139
2143
  data: activity,
2140
2144
  ...common,
@@ -2142,10 +2146,10 @@ function Player(props) {
2142
2146
  }
2143
2147
  );
2144
2148
  case "fill-in-the-blanks":
2145
- return /* @__PURE__ */ jsxRuntime.jsx(chunk2TFNDHJ5_cjs.FillInTheBlanks, { data: activity, ...common });
2149
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkTI23GIFM_cjs.FillInTheBlanks, { data: activity, ...common });
2146
2150
  case "gap-select":
2147
2151
  return /* @__PURE__ */ jsxRuntime.jsx(
2148
- chunkB6S5ADHR_cjs.GapSelect,
2152
+ chunkQA42VDTA_cjs.GapSelect,
2149
2153
  {
2150
2154
  data: activity,
2151
2155
  ...common,
@@ -2153,12 +2157,12 @@ function Player(props) {
2153
2157
  }
2154
2158
  );
2155
2159
  case "dictation":
2156
- return /* @__PURE__ */ jsxRuntime.jsx(chunkRSQFGF5W_cjs.Dictation, { data: activity, ...common });
2160
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkRZ52SEYV_cjs.Dictation, { data: activity, ...common });
2157
2161
  case "read-aloud": {
2158
2162
  const binding = slotBindings.get(slot.slotId);
2159
2163
  const assessment = props.assessments !== void 0 && Object.hasOwn(props.assessments, slot.slotId) ? props.assessments[slot.slotId] : void 0;
2160
2164
  return /* @__PURE__ */ jsxRuntime.jsx(
2161
- chunkML6L4LJH_cjs.ReadAloud,
2165
+ chunk6RRFH7C4_cjs.ReadAloud,
2162
2166
  {
2163
2167
  data: activity,
2164
2168
  ...common,
@@ -2787,7 +2791,7 @@ function SlotQuestion({
2787
2791
  ...content
2788
2792
  }) {
2789
2793
  return /* @__PURE__ */ jsxRuntime.jsx(
2790
- chunk2S3MK4VX_cjs.ActivityErrorBoundary,
2794
+ chunkBAKLJKFZ_cjs.ActivityErrorBoundary,
2791
2795
  {
2792
2796
  ...title !== void 0 ? { activityTitle: title } : {},
2793
2797
  strings,
@@ -2822,7 +2826,7 @@ function QuestionPane({
2822
2826
  if (!hidden) {
2823
2827
  return;
2824
2828
  }
2825
- chunkML6L4LJH_cjs.stopCaptureGroup(captureGroup);
2829
+ chunk6RRFH7C4_cjs.stopCaptureGroup(captureGroup);
2826
2830
  for (const media of ref.current?.querySelectorAll("audio, video") ?? []) {
2827
2831
  const element = media;
2828
2832
  if (!element.paused) {
@@ -2830,7 +2834,7 @@ function QuestionPane({
2830
2834
  }
2831
2835
  }
2832
2836
  }, [hidden, captureGroup]);
2833
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "lk-iv-question", hidden, children: /* @__PURE__ */ jsxRuntime.jsx(chunkML6L4LJH_cjs.SequenceSlotContext.Provider, { value: channel, children }) });
2837
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "lk-iv-question", hidden, children: /* @__PURE__ */ jsxRuntime.jsx(chunk6RRFH7C4_cjs.SequenceSlotContext.Provider, { value: channel, children }) });
2834
2838
  }
2835
2839
  function bufferedAhead(video) {
2836
2840
  const ranges = video.buffered;
@@ -2870,9 +2874,9 @@ function markerState(quizSlots, answered, renderMode, outcomes) {
2870
2874
  return "unreached";
2871
2875
  }
2872
2876
  function InteractiveVideo2(props) {
2873
- const strings = chunkMQAZJQRE_cjs.useLkStrings(props.strings);
2877
+ const strings = chunkAHGB5HH6_cjs.useLkStrings(props.strings);
2874
2878
  return /* @__PURE__ */ jsxRuntime.jsx(
2875
- chunk2S3MK4VX_cjs.ActivityErrorBoundary,
2879
+ chunkBAKLJKFZ_cjs.ActivityErrorBoundary,
2876
2880
  {
2877
2881
  ...props.group?.title !== void 0 ? { activityTitle: props.group.title } : {},
2878
2882
  strings,
@@ -2883,5 +2887,5 @@ function InteractiveVideo2(props) {
2883
2887
 
2884
2888
  exports.InteractiveVideo = InteractiveVideo2;
2885
2889
  exports.resolveCaptionTracks = resolveCaptionTracks;
2886
- //# sourceMappingURL=chunk-WOYCT5M6.cjs.map
2887
- //# sourceMappingURL=chunk-WOYCT5M6.cjs.map
2890
+ //# sourceMappingURL=chunk-3TJSFPO4.cjs.map
2891
+ //# sourceMappingURL=chunk-3TJSFPO4.cjs.map