@intellectif/lk-react 18.1.0 → 20.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 (137) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +2 -1
  3. package/dist/{WrittenResponse-CeHY5u3W.d.ts → WrittenResponse-D2YI8xOK.d.ts} +8 -2
  4. package/dist/{WrittenResponse-DT_Yqbq1.d.cts → WrittenResponse-obIDDYDH.d.cts} +8 -2
  5. package/dist/ai/useAiHelp.cjs +4 -4
  6. package/dist/ai/useAiHelp.d.cts +8 -2
  7. package/dist/ai/useAiHelp.d.ts +8 -2
  8. package/dist/ai/useAiHelp.js +2 -2
  9. package/dist/{chunk-IKV2I7KT.js → chunk-3XP6VO4G.js} +22 -15
  10. package/dist/chunk-3XP6VO4G.js.map +1 -0
  11. package/dist/{chunk-GM4PXDZ2.js → chunk-5GI53C6T.js} +22 -13
  12. package/dist/chunk-5GI53C6T.js.map +1 -0
  13. package/dist/{chunk-JH77MAON.cjs → chunk-6S2CKZ4H.cjs} +24 -17
  14. package/dist/chunk-6S2CKZ4H.cjs.map +1 -0
  15. package/dist/{chunk-JSRKES2Z.js → chunk-AG7MWZYZ.js} +25 -11
  16. package/dist/chunk-AG7MWZYZ.js.map +1 -0
  17. package/dist/{chunk-YGJE7RSW.js → chunk-AQIAQHJC.js} +42 -14
  18. package/dist/chunk-AQIAQHJC.js.map +1 -0
  19. package/dist/{chunk-4NERCBOW.js → chunk-BGA2IVQY.js} +7 -5
  20. package/dist/chunk-BGA2IVQY.js.map +1 -0
  21. package/dist/{chunk-JVWLXVYG.cjs → chunk-BJLQKJZX.cjs} +2 -2
  22. package/dist/chunk-BJLQKJZX.cjs.map +1 -0
  23. package/dist/{chunk-3SJ7TVL6.cjs → chunk-CEUEXISZ.cjs} +18 -13
  24. package/dist/chunk-CEUEXISZ.cjs.map +1 -0
  25. package/dist/{chunk-PROKRNL2.cjs → chunk-DZVF3IGB.cjs} +24 -18
  26. package/dist/chunk-DZVF3IGB.cjs.map +1 -0
  27. package/dist/{chunk-6NY7OTOO.js → chunk-HI6BUUIJ.js} +18 -12
  28. package/dist/chunk-HI6BUUIJ.js.map +1 -0
  29. package/dist/{chunk-7GYAUA56.cjs → chunk-HREA5SMB.cjs} +23 -14
  30. package/dist/chunk-HREA5SMB.cjs.map +1 -0
  31. package/dist/{chunk-THHFIP4N.js → chunk-IQ55QNBI.js} +16 -11
  32. package/dist/chunk-IQ55QNBI.js.map +1 -0
  33. package/dist/chunk-IXMT7X3E.cjs +44 -0
  34. package/dist/chunk-IXMT7X3E.cjs.map +1 -0
  35. package/dist/{chunk-X4YU6BX7.cjs → chunk-JLDSCHY5.cjs} +27 -13
  36. package/dist/chunk-JLDSCHY5.cjs.map +1 -0
  37. package/dist/{chunk-3SDCALS3.cjs → chunk-LK5VXNMR.cjs} +14 -14
  38. package/dist/{chunk-3SDCALS3.cjs.map → chunk-LK5VXNMR.cjs.map} +1 -1
  39. package/dist/{chunk-NWGQ7JML.cjs → chunk-NOC2M6LU.cjs} +7 -6
  40. package/dist/chunk-NOC2M6LU.cjs.map +1 -0
  41. package/dist/{chunk-UXV3V5NH.js → chunk-NWNQ2PHT.js} +23 -11
  42. package/dist/chunk-NWNQ2PHT.js.map +1 -0
  43. package/dist/chunk-ORBU3XKH.js +40 -0
  44. package/dist/chunk-ORBU3XKH.js.map +1 -0
  45. package/dist/{chunk-TXMP5AOW.cjs → chunk-PWLJDP3X.cjs} +29 -20
  46. package/dist/chunk-PWLJDP3X.cjs.map +1 -0
  47. package/dist/{chunk-MKWBLNTD.js → chunk-QKW2GGTV.js} +20 -11
  48. package/dist/chunk-QKW2GGTV.js.map +1 -0
  49. package/dist/{chunk-NZM3M5IR.cjs → chunk-RT2PP4SO.cjs} +27 -15
  50. package/dist/chunk-RT2PP4SO.cjs.map +1 -0
  51. package/dist/{chunk-BURYHHBM.js → chunk-RUS42NMN.js} +5 -4
  52. package/dist/chunk-RUS42NMN.js.map +1 -0
  53. package/dist/{chunk-TL3GVPBO.cjs → chunk-TRMWBJWT.cjs} +41 -13
  54. package/dist/chunk-TRMWBJWT.cjs.map +1 -0
  55. package/dist/{chunk-CQNKZ435.cjs → chunk-UHIOVOFK.cjs} +7 -5
  56. package/dist/chunk-UHIOVOFK.cjs.map +1 -0
  57. package/dist/{chunk-WSPVQR4O.js → chunk-W6DMHN33.js} +8 -8
  58. package/dist/{chunk-WSPVQR4O.js.map → chunk-W6DMHN33.js.map} +1 -1
  59. package/dist/{chunk-TEAVPUMX.js → chunk-WBLYD5QA.js} +2 -2
  60. package/dist/chunk-WBLYD5QA.js.map +1 -0
  61. package/dist/components/ActivityPreview.cjs +12 -12
  62. package/dist/components/ActivityPreview.d.cts +2 -2
  63. package/dist/components/ActivityPreview.d.ts +2 -2
  64. package/dist/components/ActivityPreview.js +11 -11
  65. package/dist/components/ActivitySequence.cjs +12 -12
  66. package/dist/components/ActivitySequence.d.cts +16 -4
  67. package/dist/components/ActivitySequence.d.ts +16 -4
  68. package/dist/components/ActivitySequence.js +11 -11
  69. package/dist/components/Dictation.cjs +6 -6
  70. package/dist/components/Dictation.d.cts +1 -1
  71. package/dist/components/Dictation.d.ts +1 -1
  72. package/dist/components/Dictation.js +5 -5
  73. package/dist/components/FillInTheBlanks.cjs +6 -6
  74. package/dist/components/FillInTheBlanks.d.cts +1 -1
  75. package/dist/components/FillInTheBlanks.d.ts +1 -1
  76. package/dist/components/FillInTheBlanks.js +5 -5
  77. package/dist/components/GapSelect.cjs +6 -6
  78. package/dist/components/GapSelect.d.cts +1 -1
  79. package/dist/components/GapSelect.d.ts +1 -1
  80. package/dist/components/GapSelect.js +5 -5
  81. package/dist/components/InteractiveVideo.cjs +12 -12
  82. package/dist/components/InteractiveVideo.d.cts +16 -3
  83. package/dist/components/InteractiveVideo.d.ts +16 -3
  84. package/dist/components/InteractiveVideo.js +10 -10
  85. package/dist/components/MultipleChoice.cjs +6 -6
  86. package/dist/components/MultipleChoice.d.cts +1 -1
  87. package/dist/components/MultipleChoice.d.ts +1 -1
  88. package/dist/components/MultipleChoice.js +5 -5
  89. package/dist/components/PronunciationFeedback.d.cts +1 -1
  90. package/dist/components/PronunciationFeedback.d.ts +1 -1
  91. package/dist/components/ReadAloud.cjs +4 -4
  92. package/dist/components/ReadAloud.d.cts +2 -2
  93. package/dist/components/ReadAloud.d.ts +2 -2
  94. package/dist/components/ReadAloud.js +3 -3
  95. package/dist/components/StimulusPanel.d.cts +1 -1
  96. package/dist/components/StimulusPanel.d.ts +1 -1
  97. package/dist/components/WrittenResponse.cjs +4 -3
  98. package/dist/components/WrittenResponse.d.cts +3 -3
  99. package/dist/components/WrittenResponse.d.ts +3 -3
  100. package/dist/components/WrittenResponse.js +3 -2
  101. package/dist/i18n/LkIntlProvider.d.cts +2 -2
  102. package/dist/i18n/LkIntlProvider.d.ts +2 -2
  103. package/dist/index.cjs +25 -25
  104. package/dist/index.cjs.map +1 -1
  105. package/dist/index.d.cts +2 -2
  106. package/dist/index.d.ts +2 -2
  107. package/dist/index.js +13 -13
  108. package/dist/index.js.map +1 -1
  109. package/dist/{types-DtPYG1hh.d.cts → types-B_PCcqO-.d.cts} +9 -1
  110. package/dist/{types-BY-4S5Rd.d.ts → types-C-GMCJ0g.d.ts} +9 -1
  111. package/package.json +3 -3
  112. package/dist/chunk-3SJ7TVL6.cjs.map +0 -1
  113. package/dist/chunk-4NERCBOW.js.map +0 -1
  114. package/dist/chunk-6NY7OTOO.js.map +0 -1
  115. package/dist/chunk-7GYAUA56.cjs.map +0 -1
  116. package/dist/chunk-BRPEB43C.cjs +0 -13
  117. package/dist/chunk-BRPEB43C.cjs.map +0 -1
  118. package/dist/chunk-BURYHHBM.js.map +0 -1
  119. package/dist/chunk-CQNKZ435.cjs.map +0 -1
  120. package/dist/chunk-GM4PXDZ2.js.map +0 -1
  121. package/dist/chunk-IKV2I7KT.js.map +0 -1
  122. package/dist/chunk-JH77MAON.cjs.map +0 -1
  123. package/dist/chunk-JSRKES2Z.js.map +0 -1
  124. package/dist/chunk-JVWLXVYG.cjs.map +0 -1
  125. package/dist/chunk-MKWBLNTD.js.map +0 -1
  126. package/dist/chunk-NWGQ7JML.cjs.map +0 -1
  127. package/dist/chunk-NZM3M5IR.cjs.map +0 -1
  128. package/dist/chunk-PROKRNL2.cjs.map +0 -1
  129. package/dist/chunk-RCMYEWBE.js +0 -11
  130. package/dist/chunk-RCMYEWBE.js.map +0 -1
  131. package/dist/chunk-TEAVPUMX.js.map +0 -1
  132. package/dist/chunk-THHFIP4N.js.map +0 -1
  133. package/dist/chunk-TL3GVPBO.cjs.map +0 -1
  134. package/dist/chunk-TXMP5AOW.cjs.map +0 -1
  135. package/dist/chunk-UXV3V5NH.js.map +0 -1
  136. package/dist/chunk-X4YU6BX7.cjs.map +0 -1
  137. package/dist/chunk-YGJE7RSW.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -3,24 +3,24 @@
3
3
 
4
4
  var chunkPNNUPUH4_cjs = require('./chunk-PNNUPUH4.cjs');
5
5
  var chunkK63VPFOV_cjs = require('./chunk-K63VPFOV.cjs');
6
- var chunkPROKRNL2_cjs = require('./chunk-PROKRNL2.cjs');
7
- var chunk3SDCALS3_cjs = require('./chunk-3SDCALS3.cjs');
8
- var chunkTXMP5AOW_cjs = require('./chunk-TXMP5AOW.cjs');
6
+ var chunkDZVF3IGB_cjs = require('./chunk-DZVF3IGB.cjs');
7
+ var chunkLK5VXNMR_cjs = require('./chunk-LK5VXNMR.cjs');
8
+ var chunkPWLJDP3X_cjs = require('./chunk-PWLJDP3X.cjs');
9
9
  var chunk3FBBP5QR_cjs = require('./chunk-3FBBP5QR.cjs');
10
- var chunkCQNKZ435_cjs = require('./chunk-CQNKZ435.cjs');
11
- var chunkX4YU6BX7_cjs = require('./chunk-X4YU6BX7.cjs');
12
- var chunkJH77MAON_cjs = require('./chunk-JH77MAON.cjs');
13
- var chunk3SJ7TVL6_cjs = require('./chunk-3SJ7TVL6.cjs');
14
- var chunk7GYAUA56_cjs = require('./chunk-7GYAUA56.cjs');
15
- require('./chunk-NWGQ7JML.cjs');
16
- var chunkTL3GVPBO_cjs = require('./chunk-TL3GVPBO.cjs');
10
+ var chunkUHIOVOFK_cjs = require('./chunk-UHIOVOFK.cjs');
11
+ var chunkJLDSCHY5_cjs = require('./chunk-JLDSCHY5.cjs');
12
+ var chunk6S2CKZ4H_cjs = require('./chunk-6S2CKZ4H.cjs');
13
+ var chunkCEUEXISZ_cjs = require('./chunk-CEUEXISZ.cjs');
14
+ var chunkHREA5SMB_cjs = require('./chunk-HREA5SMB.cjs');
15
+ require('./chunk-NOC2M6LU.cjs');
16
+ var chunkTRMWBJWT_cjs = require('./chunk-TRMWBJWT.cjs');
17
17
  var chunkZDYNAWDZ_cjs = require('./chunk-ZDYNAWDZ.cjs');
18
- var chunkNZM3M5IR_cjs = require('./chunk-NZM3M5IR.cjs');
18
+ var chunkRT2PP4SO_cjs = require('./chunk-RT2PP4SO.cjs');
19
19
  var chunkLX2X4KC7_cjs = require('./chunk-LX2X4KC7.cjs');
20
20
  var chunkUDCQTOVU_cjs = require('./chunk-UDCQTOVU.cjs');
21
21
  require('./chunk-OD65OZIW.cjs');
22
- require('./chunk-JVWLXVYG.cjs');
23
- require('./chunk-BRPEB43C.cjs');
22
+ require('./chunk-IXMT7X3E.cjs');
23
+ require('./chunk-BJLQKJZX.cjs');
24
24
  require('./chunk-APZ6JFN6.cjs');
25
25
  require('./chunk-EANGKAKG.cjs');
26
26
  require('./chunk-L7WY5YFQ.cjs');
@@ -61,19 +61,19 @@ Object.defineProperty(exports, "useTheme", {
61
61
  });
62
62
  Object.defineProperty(exports, "InteractiveVideo", {
63
63
  enumerable: true,
64
- get: function () { return chunkPROKRNL2_cjs.InteractiveVideo; }
64
+ get: function () { return chunkDZVF3IGB_cjs.InteractiveVideo; }
65
65
  });
66
66
  Object.defineProperty(exports, "resolveCaptionTracks", {
67
67
  enumerable: true,
68
- get: function () { return chunkPROKRNL2_cjs.resolveCaptionTracks; }
68
+ get: function () { return chunkDZVF3IGB_cjs.resolveCaptionTracks; }
69
69
  });
70
70
  Object.defineProperty(exports, "ActivityPreview", {
71
71
  enumerable: true,
72
- get: function () { return chunk3SDCALS3_cjs.ActivityPreview; }
72
+ get: function () { return chunkLK5VXNMR_cjs.ActivityPreview; }
73
73
  });
74
74
  Object.defineProperty(exports, "ActivitySequence", {
75
75
  enumerable: true,
76
- get: function () { return chunkTXMP5AOW_cjs.ActivitySequence; }
76
+ get: function () { return chunkPWLJDP3X_cjs.ActivitySequence; }
77
77
  });
78
78
  Object.defineProperty(exports, "StimulusPanel", {
79
79
  enumerable: true,
@@ -81,31 +81,31 @@ Object.defineProperty(exports, "StimulusPanel", {
81
81
  });
82
82
  Object.defineProperty(exports, "WrittenResponse", {
83
83
  enumerable: true,
84
- get: function () { return chunkCQNKZ435_cjs.WrittenResponse; }
84
+ get: function () { return chunkUHIOVOFK_cjs.WrittenResponse; }
85
85
  });
86
86
  Object.defineProperty(exports, "MultipleChoice", {
87
87
  enumerable: true,
88
- get: function () { return chunkX4YU6BX7_cjs.MultipleChoice; }
88
+ get: function () { return chunkJLDSCHY5_cjs.MultipleChoice; }
89
89
  });
90
90
  Object.defineProperty(exports, "FillInTheBlanks", {
91
91
  enumerable: true,
92
- get: function () { return chunkJH77MAON_cjs.FillInTheBlanks; }
92
+ get: function () { return chunk6S2CKZ4H_cjs.FillInTheBlanks; }
93
93
  });
94
94
  Object.defineProperty(exports, "GapSelect", {
95
95
  enumerable: true,
96
- get: function () { return chunk3SJ7TVL6_cjs.GapSelect; }
96
+ get: function () { return chunkCEUEXISZ_cjs.GapSelect; }
97
97
  });
98
98
  Object.defineProperty(exports, "Dictation", {
99
99
  enumerable: true,
100
- get: function () { return chunk7GYAUA56_cjs.Dictation; }
100
+ get: function () { return chunkHREA5SMB_cjs.Dictation; }
101
101
  });
102
102
  Object.defineProperty(exports, "useAiExplanation", {
103
103
  enumerable: true,
104
- get: function () { return chunkTL3GVPBO_cjs.useAiExplanation; }
104
+ get: function () { return chunkTRMWBJWT_cjs.useAiExplanation; }
105
105
  });
106
106
  Object.defineProperty(exports, "useAiHints", {
107
107
  enumerable: true,
108
- get: function () { return chunkTL3GVPBO_cjs.useAiHints; }
108
+ get: function () { return chunkTRMWBJWT_cjs.useAiHints; }
109
109
  });
110
110
  Object.defineProperty(exports, "LkAiProvider", {
111
111
  enumerable: true,
@@ -117,7 +117,7 @@ Object.defineProperty(exports, "useLearnerAi", {
117
117
  });
118
118
  Object.defineProperty(exports, "ReadAloud", {
119
119
  enumerable: true,
120
- get: function () { return chunkNZM3M5IR_cjs.ReadAloud; }
120
+ get: function () { return chunkRT2PP4SO_cjs.ReadAloud; }
121
121
  });
122
122
  Object.defineProperty(exports, "PronunciationFeedback", {
123
123
  enumerable: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0MO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAoCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.cjs","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedgerEntry,\n RecordingRef,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport type { LearnerAi } from '../ai/LkAiProvider.js';\nimport type { RecordedTake } from '../hooks/useSpeechRecorder.js';\nimport type { LkStringsOverride } from '../i18n/strings.js';\nimport type { ReadAloudAssessResult } from './ReadAloud/ReadAloud.js';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n /**\n * Binds this activity's own `data.media` to a play budget the consumer\n * persists. Usually supplied by `<ActivitySequence mediaBudget={…}>`; pass it\n * yourself when rendering an activity standalone.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */\n mediaStrings?: Partial<MediaTransportStrings>;\n /**\n * Overrides the SDK's chrome text for this activity, layered on whatever\n * `LkIntlProvider` supplies. `mediaStrings` still works and is merged after\n * this, so an existing 0.8.x call site keeps behaving as it did.\n */\n strings?: LkStringsOverride;\n onInteraction?: (event: InteractionEvent) => void;\n /**\n * The host's AI ports for this activity — an explanation after grading, hints\n * before submit — overriding `LkAiProvider`'s. Leave a port out to switch\n * that help off. Nothing AI appears in `exam`, and an item whose author\n * switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.\n */\n ai?: LearnerAi;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n /**\n * BCP 47 tag stamped as `lang` on this component's root. This is the\n * INTERFACE language — the SDK's own chrome renders inside that element — so\n * it should carry the same value you give `<LkIntlProvider locale>`. Passing\n * a different one re-declares the language of every SDK string in this\n * subtree without changing the words.\n *\n * It is NOT `data.locale`, which labels xAPI statements — and, on a\n * dictation, places the dictation's own words: `<Dictation>` puts it, and\n * the direction it names, on its title, hints, marks and solution. Other\n * authored content in another language belongs on `stimulus.locale`, which\n * `<StimulusPanel>` puts on the passage alone.\n */\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key, and\n * almost every built-in activity throws at render in the default `practice`\n * mode rather than fail later: `<MultipleChoice>`, `<FillInTheBlanks>`,\n * `<GapSelect>` and `<Dictation>` because they grade locally, and\n * `<WrittenResponse>` because `practice` still runs its local submit path and\n * emits a practice-mode xAPI statement.\n *\n * `<ReadAloud>` is the exception and renders a projection in every mode: a\n * read-aloud item has no answer key to withhold, so `redact()` removes only the\n * authored feedback and leaves the reading, its language and its bounds — which\n * is everything the component puts on screen. It grades nothing locally either;\n * the grade comes back from whatever the application asked to judge the take.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Every word the SDK's audio transport renders.\n *\n * Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and\n * is formatted by the component, because it reads identically in every locale\n * this SDK targets. The scrubber's SPOKEN value does have a word in it and does\n * have a key ({@link MediaTransportStrings.timeValue}).\n *\n * Supply them to translate it. These are the highest-stakes strings on a\n * listening paper — \"No plays remaining\" decides whether a learner believes\n * they may try again — so shipping them as untranslatable English inside a\n * Spanish panel was not acceptable. Supplying any of them also sets `lang` on\n * the transport chrome, so a screen reader does not read the SDK's own words\n * with the passage's phonetics.\n */\nexport interface MediaTransportStrings {\n play: string;\n pause: string;\n preparing: string;\n mute: string;\n unmute: string;\n volume: string;\n speed: string;\n seek: string;\n /**\n * Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.\n * Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to\n * reimplement the clock to change the word between them.\n */\n timeValue: (elapsed: string, duration: string) => string;\n /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */\n playsRemaining: (remaining: number, max: number) => string;\n noPlaysRemaining: string;\n /** Shown before the LAST play is spent, so a stray press cannot cost it. */\n lastPlayConfirm: string;\n lastPlayStart: string;\n lastPlayCancel: string;\n seekBlocked: string;\n rateBlocked: string;\n playFailed: string;\n}\n\n/**\n * Binds ONE media block to a play budget the consumer persists.\n *\n * The SDK refuses a play; it does not remember one. Everything durable here is\n * the consuming application's — see {@link SequenceMediaBudget.onPlayConsumed}.\n */\nexport interface MediaBudgetBinding {\n /** From `slotMediaKey(slotId)` / `stimulusMediaKey(slotId)` in lk-core. */\n key: string;\n /** Plays already spent, and where playback stood. Read at mount only. */\n entry?: MediaPlayLedgerEntry;\n /** Defaults to `renderMode !== 'review'`. An explicit boolean wins either way. */\n enforced?: boolean;\n /** Slot context stamped onto the claim and the interaction event. */\n slotId: string;\n index: number;\n activityId?: string;\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * The pager-level half of a play budget. One prop, because it is one concept.\n */\nexport interface SequenceMediaBudget {\n /**\n * `MediaPlayLedger.entries` goes straight in. An absent key means nothing\n * spent. Read at mount, like `responses`.\n */\n plays?: Readonly<Record<string, MediaPlayLedgerEntry>>;\n /**\n * Re-seed token. Change this string and the budgets re-seed from `plays`\n * WITHOUT remounting the pager — the invigilator path (\"the audio never\n * started, give her the play back\") that would otherwise cost the learner\n * their focus, their scroll position and an unsaved answer.\n */\n resumeKey?: string;\n /** Explicit override of the default (`renderMode !== 'review'`). */\n enforced?: boolean;\n /**\n * Called the instant a play is claimed, BEFORE any audio is audible.\n *\n * Two tiers, chosen by what you return:\n *\n * - **Return nothing (optimistic).** Playback starts immediately and the\n * count is only as durable as your write. **Do not debounce this, and do\n * not batch it with the answer autosave** — an eight-second debounce is\n * exactly long enough to start a third play and hard-reload. A `pagehide`\n * beacon is a backstop, not the mechanism. A crash between this call and\n * your write landing RETURNS the play to the learner; that is the honest\n * description of what you are buying.\n * - **Return a promise (confirmed).** Playback is held — the button reads\n * \"Preparing…\" and is `aria-busy` — until it settles. Resolve with\n * `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays\n * + 1 … RETURNING plays`, or a compare-and-set on\n * `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the\n * play, which is how a second tab is caught: two mounts both seeded at 0\n * both claim 1, and only an atomic increment can tell them apart. A\n * rejection charges nothing and lets the learner retry. This is the only\n * tier in which \"consumed before audible\" is true of storage rather than\n * only of memory; use it for summative papers.\n *\n * Never settle the promise and the learner cannot play at all: settle it.\n */\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n /**\n * Called when a charged play produced no audio — the element errored before\n * playback advanced past 0.25 s, an expired signed URL being the realistic\n * cause. Supply it to give the play back, decrementing with a compare-and-set\n * on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not\n * decrement a ledger it has no channel to correct.\n */\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n /**\n * Position reports, so a refresh resumes the play the learner already paid\n * for instead of charging them again. Fires on pause, on end (with 0), and at\n * most once per whole second of playback.\n *\n * **This one you MAY throttle** — the granularity you persist is the\n * granularity of the replay a crash grants. Three seconds is sane; three\n * minutes is not.\n */\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. Defaults are English. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * Which slot a recording belongs to, stamped on every call the pager makes.\n *\n * Exactly the shape {@link ActivitySequenceProps.onSubmit} already passes, so\n * one identity travels with the answer and with the take that answer points at\n * — and a consumer writes `slotId` in both places rather than reconciling two\n * spellings of \"which question was this\".\n */\nexport interface SequenceRecordingSlot {\n /** Identity from `flattenSequence` — stable under shuffling. Store against THIS. */\n slotId: string;\n /** Presented position, which moves under shuffling. */\n index: number;\n activityId: string;\n}\n\n/**\n * The pager-level half of a read-aloud recording binding: where every take in\n * this sequence is stored, and how a judgement comes back.\n *\n * One prop at both levels, because it is one concept — the media-budget\n * precedent ({@link SequenceMediaBudget} beside {@link MediaBudgetBinding}).\n * The difference is the second argument: a per-slot binding knows which slot it\n * belongs to, so a sequence-level one is told.\n *\n * The SDK records and hands over; it never judges. `assess` is `practice` only\n * and optional — a sequence that only stores takes gets the \"assessment is not\n * available\" notice rather than a throw — and `playbackUrl` is `review` only.\n * A slot whose activity is not a read-aloud never calls any of them.\n */\nexport interface SequenceRecordingBinding {\n /**\n * Puts the take in your storage and returns its key. **Required** outside\n * `review`: without it a learner can speak into a control that submits\n * nothing, which the component refuses to render. Settle it.\n */\n upload(take: RecordedTake, slot: SequenceRecordingSlot): Promise<RecordingRef>;\n /** Judges the stored take. `practice` only; an exam never assesses on the client. */\n assess?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<ReadAloudAssessResult>;\n /** A playable link to a stored take. `review` only. */\n playbackUrl?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<string>;\n}\n"]}
1
+ {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmNO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAoCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.cjs","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n DeliveryPolicy,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedgerEntry,\n RecordingRef,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport type { LearnerAi } from '../ai/LkAiProvider.js';\nimport type { RecordedTake } from '../hooks/useSpeechRecorder.js';\nimport type { LkStringsOverride } from '../i18n/strings.js';\nimport type { ReadAloudAssessResult } from './ReadAloud/ReadAloud.js';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n /**\n * Binds this activity's own `data.media` to a play budget the consumer\n * persists. Usually supplied by `<ActivitySequence mediaBudget={…}>`; pass it\n * yourself when rendering an activity standalone.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */\n mediaStrings?: Partial<MediaTransportStrings>;\n /**\n * Overrides the SDK's chrome text for this activity, layered on whatever\n * `LkIntlProvider` supplies. `mediaStrings` still works and is merged after\n * this, so an existing 0.8.x call site keeps behaving as it did.\n */\n strings?: LkStringsOverride;\n onInteraction?: (event: InteractionEvent) => void;\n /**\n * The host's AI ports for this activity — an explanation after grading, hints\n * before submit — overriding `LkAiProvider`'s. Leave a port out to switch\n * that help off. Nothing AI appears in `exam`, and an item whose author\n * switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.\n */\n ai?: LearnerAi;\n /**\n * What the school running this paper lets a learner see: feedback,\n * solutions, hints, and AI help. Every setting only takes something away\n * from what the mode shows, and an absent policy takes nothing away. Inside\n * `<ActivitySequence>` or `<InteractiveVideo>`, the paper's own policy holds\n * as well, whichever is stricter. See `DeliveryPolicy` in lk-core.\n */\n delivery?: DeliveryPolicy | null;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n /**\n * BCP 47 tag stamped as `lang` on this component's root. This is the\n * INTERFACE language — the SDK's own chrome renders inside that element — so\n * it should carry the same value you give `<LkIntlProvider locale>`. Passing\n * a different one re-declares the language of every SDK string in this\n * subtree without changing the words.\n *\n * It is NOT `data.locale`, which labels xAPI statements — and, on a\n * dictation, places the dictation's own words: `<Dictation>` puts it, and\n * the direction it names, on its title, hints, marks and solution. Other\n * authored content in another language belongs on `stimulus.locale`, which\n * `<StimulusPanel>` puts on the passage alone.\n */\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key, and\n * almost every built-in activity throws at render in the default `practice`\n * mode rather than fail later: `<MultipleChoice>`, `<FillInTheBlanks>`,\n * `<GapSelect>` and `<Dictation>` because they grade locally, and\n * `<WrittenResponse>` because `practice` still runs its local submit path and\n * emits a practice-mode xAPI statement.\n *\n * `<ReadAloud>` is the exception and renders a projection in every mode: a\n * read-aloud item has no answer key to withhold, so `redact()` removes only the\n * authored feedback and leaves the reading, its language and its bounds — which\n * is everything the component puts on screen. It grades nothing locally either;\n * the grade comes back from whatever the application asked to judge the take.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Every word the SDK's audio transport renders.\n *\n * Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and\n * is formatted by the component, because it reads identically in every locale\n * this SDK targets. The scrubber's SPOKEN value does have a word in it and does\n * have a key ({@link MediaTransportStrings.timeValue}).\n *\n * Supply them to translate it. These are the highest-stakes strings on a\n * listening paper — \"No plays remaining\" decides whether a learner believes\n * they may try again — so shipping them as untranslatable English inside a\n * Spanish panel was not acceptable. Supplying any of them also sets `lang` on\n * the transport chrome, so a screen reader does not read the SDK's own words\n * with the passage's phonetics.\n */\nexport interface MediaTransportStrings {\n play: string;\n pause: string;\n preparing: string;\n mute: string;\n unmute: string;\n volume: string;\n speed: string;\n seek: string;\n /**\n * Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.\n * Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to\n * reimplement the clock to change the word between them.\n */\n timeValue: (elapsed: string, duration: string) => string;\n /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */\n playsRemaining: (remaining: number, max: number) => string;\n noPlaysRemaining: string;\n /** Shown before the LAST play is spent, so a stray press cannot cost it. */\n lastPlayConfirm: string;\n lastPlayStart: string;\n lastPlayCancel: string;\n seekBlocked: string;\n rateBlocked: string;\n playFailed: string;\n}\n\n/**\n * Binds ONE media block to a play budget the consumer persists.\n *\n * The SDK refuses a play; it does not remember one. Everything durable here is\n * the consuming application's — see {@link SequenceMediaBudget.onPlayConsumed}.\n */\nexport interface MediaBudgetBinding {\n /** From `slotMediaKey(slotId)` / `stimulusMediaKey(slotId)` in lk-core. */\n key: string;\n /** Plays already spent, and where playback stood. Read at mount only. */\n entry?: MediaPlayLedgerEntry;\n /** Defaults to `renderMode !== 'review'`. An explicit boolean wins either way. */\n enforced?: boolean;\n /** Slot context stamped onto the claim and the interaction event. */\n slotId: string;\n index: number;\n activityId?: string;\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * The pager-level half of a play budget. One prop, because it is one concept.\n */\nexport interface SequenceMediaBudget {\n /**\n * `MediaPlayLedger.entries` goes straight in. An absent key means nothing\n * spent. Read at mount, like `responses`.\n */\n plays?: Readonly<Record<string, MediaPlayLedgerEntry>>;\n /**\n * Re-seed token. Change this string and the budgets re-seed from `plays`\n * WITHOUT remounting the pager — the invigilator path (\"the audio never\n * started, give her the play back\") that would otherwise cost the learner\n * their focus, their scroll position and an unsaved answer.\n */\n resumeKey?: string;\n /** Explicit override of the default (`renderMode !== 'review'`). */\n enforced?: boolean;\n /**\n * Called the instant a play is claimed, BEFORE any audio is audible.\n *\n * Two tiers, chosen by what you return:\n *\n * - **Return nothing (optimistic).** Playback starts immediately and the\n * count is only as durable as your write. **Do not debounce this, and do\n * not batch it with the answer autosave** — an eight-second debounce is\n * exactly long enough to start a third play and hard-reload. A `pagehide`\n * beacon is a backstop, not the mechanism. A crash between this call and\n * your write landing RETURNS the play to the learner; that is the honest\n * description of what you are buying.\n * - **Return a promise (confirmed).** Playback is held — the button reads\n * \"Preparing…\" and is `aria-busy` — until it settles. Resolve with\n * `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays\n * + 1 … RETURNING plays`, or a compare-and-set on\n * `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the\n * play, which is how a second tab is caught: two mounts both seeded at 0\n * both claim 1, and only an atomic increment can tell them apart. A\n * rejection charges nothing and lets the learner retry. This is the only\n * tier in which \"consumed before audible\" is true of storage rather than\n * only of memory; use it for summative papers.\n *\n * Never settle the promise and the learner cannot play at all: settle it.\n */\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n /**\n * Called when a charged play produced no audio — the element errored before\n * playback advanced past 0.25 s, an expired signed URL being the realistic\n * cause. Supply it to give the play back, decrementing with a compare-and-set\n * on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not\n * decrement a ledger it has no channel to correct.\n */\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n /**\n * Position reports, so a refresh resumes the play the learner already paid\n * for instead of charging them again. Fires on pause, on end (with 0), and at\n * most once per whole second of playback.\n *\n * **This one you MAY throttle** — the granularity you persist is the\n * granularity of the replay a crash grants. Three seconds is sane; three\n * minutes is not.\n */\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. Defaults are English. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * Which slot a recording belongs to, stamped on every call the pager makes.\n *\n * Exactly the shape {@link ActivitySequenceProps.onSubmit} already passes, so\n * one identity travels with the answer and with the take that answer points at\n * — and a consumer writes `slotId` in both places rather than reconciling two\n * spellings of \"which question was this\".\n */\nexport interface SequenceRecordingSlot {\n /** Identity from `flattenSequence` — stable under shuffling. Store against THIS. */\n slotId: string;\n /** Presented position, which moves under shuffling. */\n index: number;\n activityId: string;\n}\n\n/**\n * The pager-level half of a read-aloud recording binding: where every take in\n * this sequence is stored, and how a judgement comes back.\n *\n * One prop at both levels, because it is one concept — the media-budget\n * precedent ({@link SequenceMediaBudget} beside {@link MediaBudgetBinding}).\n * The difference is the second argument: a per-slot binding knows which slot it\n * belongs to, so a sequence-level one is told.\n *\n * The SDK records and hands over; it never judges. `assess` is `practice` only\n * and optional — a sequence that only stores takes gets the \"assessment is not\n * available\" notice rather than a throw — and `playbackUrl` is `review` only.\n * A slot whose activity is not a read-aloud never calls any of them.\n */\nexport interface SequenceRecordingBinding {\n /**\n * Puts the take in your storage and returns its key. **Required** outside\n * `review`: without it a learner can speak into a control that submits\n * nothing, which the component refuses to render. Settle it.\n */\n upload(take: RecordedTake, slot: SequenceRecordingSlot): Promise<RecordingRef>;\n /** Judges the stored take. `practice` only; an exam never assesses on the client. */\n assess?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<ReadAloudAssessResult>;\n /** A playable link to a stored take. `review` only. */\n playbackUrl?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<string>;\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -10,7 +10,7 @@ export { MultipleChoice, MultipleChoiceProps } from './components/MultipleChoice
10
10
  export { PronunciationFeedback, PronunciationFeedbackProps } from './components/PronunciationFeedback.cjs';
11
11
  export { ReadAloud } from './components/ReadAloud.cjs';
12
12
  export { StimulusPanel, StimulusPanelProps } from './components/StimulusPanel.cjs';
13
- export { A as ActivityProps, C as CaptionSize, D as DEFAULT_STRINGS, H as HtmlSanitizer, L as LkStrings, a as LkStringsOverride, M as MediaBudgetBinding, b as MediaTransportStrings, R as ReadAloudAssessResult, c as ReadAloudProps, d as RecordingBinding, e as RenderMode, f as Renderable, g as RenderableActivity, S as SequenceMediaBudget, h as SequenceRecordingBinding, i as SequenceRecordingSlot, V as VideoPreferences, j as asRenderable, k as asRenderableSequence, m as mergeStrings } from './types-DtPYG1hh.cjs';
13
+ export { A as ActivityProps, C as CaptionSize, D as DEFAULT_STRINGS, H as HtmlSanitizer, L as LkStrings, a as LkStringsOverride, M as MediaBudgetBinding, b as MediaTransportStrings, R as ReadAloudAssessResult, c as ReadAloudProps, d as RecordingBinding, e as RenderMode, f as Renderable, g as RenderableActivity, S as SequenceMediaBudget, h as SequenceRecordingBinding, i as SequenceRecordingSlot, V as VideoPreferences, j as asRenderable, k as asRenderableSequence, m as mergeStrings } from './types-B_PCcqO-.cjs';
14
14
  export { WrittenResponse } from './components/WrittenResponse.cjs';
15
15
  export { ActivityState, UseActivityStateResult, useActivityState } from './hooks/useActivityState.cjs';
16
16
  export { CAPTURE_PROCESSOR_SOURCE, SpeechRecorder, SpeechRecorderOptions, useSpeechRecorder } from './hooks/useSpeechRecorder.cjs';
@@ -18,7 +18,7 @@ export { UseXAPIResult, useXAPI } from './hooks/useXAPI.cjs';
18
18
  export { LkDirection, LkIntlProvider, LkIntlProviderProps, directionForLocale, useLkDirection, useLkStrings } from './i18n/LkIntlProvider.cjs';
19
19
  export { TailwindThemeExtension, ThemeProvider, ThemeProviderProps, createTailwindTheme, darkTheme, defaultTheme, useTheme } from './theme/ThemeProvider.cjs';
20
20
  export { R as RecordedTake, S as SpeechRecorderError, a as SpeechRecorderStatus } from './machine-CuwKqWXC.cjs';
21
- export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-DT_Yqbq1.cjs';
21
+ export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-obIDDYDH.cjs';
22
22
  import '@intellectif/lk-core';
23
23
  import 'react';
24
24
  import 'react/jsx-runtime';
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { MultipleChoice, MultipleChoiceProps } from './components/MultipleChoice
10
10
  export { PronunciationFeedback, PronunciationFeedbackProps } from './components/PronunciationFeedback.js';
11
11
  export { ReadAloud } from './components/ReadAloud.js';
12
12
  export { StimulusPanel, StimulusPanelProps } from './components/StimulusPanel.js';
13
- export { A as ActivityProps, C as CaptionSize, D as DEFAULT_STRINGS, H as HtmlSanitizer, L as LkStrings, a as LkStringsOverride, M as MediaBudgetBinding, b as MediaTransportStrings, R as ReadAloudAssessResult, c as ReadAloudProps, d as RecordingBinding, e as RenderMode, f as Renderable, g as RenderableActivity, S as SequenceMediaBudget, h as SequenceRecordingBinding, i as SequenceRecordingSlot, V as VideoPreferences, j as asRenderable, k as asRenderableSequence, m as mergeStrings } from './types-BY-4S5Rd.js';
13
+ export { A as ActivityProps, C as CaptionSize, D as DEFAULT_STRINGS, H as HtmlSanitizer, L as LkStrings, a as LkStringsOverride, M as MediaBudgetBinding, b as MediaTransportStrings, R as ReadAloudAssessResult, c as ReadAloudProps, d as RecordingBinding, e as RenderMode, f as Renderable, g as RenderableActivity, S as SequenceMediaBudget, h as SequenceRecordingBinding, i as SequenceRecordingSlot, V as VideoPreferences, j as asRenderable, k as asRenderableSequence, m as mergeStrings } from './types-C-GMCJ0g.js';
14
14
  export { WrittenResponse } from './components/WrittenResponse.js';
15
15
  export { ActivityState, UseActivityStateResult, useActivityState } from './hooks/useActivityState.js';
16
16
  export { CAPTURE_PROCESSOR_SOURCE, SpeechRecorder, SpeechRecorderOptions, useSpeechRecorder } from './hooks/useSpeechRecorder.js';
@@ -18,7 +18,7 @@ export { UseXAPIResult, useXAPI } from './hooks/useXAPI.js';
18
18
  export { LkDirection, LkIntlProvider, LkIntlProviderProps, directionForLocale, useLkDirection, useLkStrings } from './i18n/LkIntlProvider.js';
19
19
  export { TailwindThemeExtension, ThemeProvider, ThemeProviderProps, createTailwindTheme, darkTheme, defaultTheme, useTheme } from './theme/ThemeProvider.js';
20
20
  export { R as RecordedTake, S as SpeechRecorderError, a as SpeechRecorderStatus } from './machine-CuwKqWXC.js';
21
- export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-CeHY5u3W.js';
21
+ export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-D2YI8xOK.js';
22
22
  import '@intellectif/lk-core';
23
23
  import 'react';
24
24
  import 'react/jsx-runtime';
package/dist/index.js CHANGED
@@ -1,24 +1,24 @@
1
1
  'use client';
2
2
  export { useXAPI } from './chunk-2TW6WLBC.js';
3
3
  export { ThemeProvider, createTailwindTheme, darkTheme, defaultTheme, useTheme } from './chunk-UHKZXOXD.js';
4
- export { InteractiveVideo, resolveCaptionTracks } from './chunk-6NY7OTOO.js';
5
- export { ActivityPreview } from './chunk-WSPVQR4O.js';
6
- export { ActivitySequence } from './chunk-MKWBLNTD.js';
4
+ export { InteractiveVideo, resolveCaptionTracks } from './chunk-HI6BUUIJ.js';
5
+ export { ActivityPreview } from './chunk-W6DMHN33.js';
6
+ export { ActivitySequence } from './chunk-QKW2GGTV.js';
7
7
  export { StimulusPanel } from './chunk-7K4B2EMU.js';
8
- export { WrittenResponse } from './chunk-4NERCBOW.js';
9
- export { MultipleChoice } from './chunk-JSRKES2Z.js';
10
- export { FillInTheBlanks } from './chunk-IKV2I7KT.js';
11
- export { GapSelect } from './chunk-THHFIP4N.js';
12
- export { Dictation } from './chunk-GM4PXDZ2.js';
13
- import './chunk-BURYHHBM.js';
14
- export { useAiExplanation, useAiHints } from './chunk-YGJE7RSW.js';
8
+ export { WrittenResponse } from './chunk-BGA2IVQY.js';
9
+ export { MultipleChoice } from './chunk-AG7MWZYZ.js';
10
+ export { FillInTheBlanks } from './chunk-3XP6VO4G.js';
11
+ export { GapSelect } from './chunk-IQ55QNBI.js';
12
+ export { Dictation } from './chunk-5GI53C6T.js';
13
+ import './chunk-RUS42NMN.js';
14
+ export { useAiExplanation, useAiHints } from './chunk-AQIAQHJC.js';
15
15
  export { LkAiProvider, useLearnerAi } from './chunk-YNDKAYW3.js';
16
- export { ReadAloud } from './chunk-UXV3V5NH.js';
16
+ export { ReadAloud } from './chunk-NWNQ2PHT.js';
17
17
  export { PronunciationFeedback } from './chunk-G7AGM2Y7.js';
18
18
  export { CAPTURE_PROCESSOR_SOURCE, useSpeechRecorder } from './chunk-MCO52PV5.js';
19
19
  import './chunk-YORQQXGO.js';
20
- import './chunk-TEAVPUMX.js';
21
- import './chunk-RCMYEWBE.js';
20
+ import './chunk-ORBU3XKH.js';
21
+ import './chunk-WBLYD5QA.js';
22
22
  import './chunk-FEOFX4QW.js';
23
23
  import './chunk-PPFVTXGJ.js';
24
24
  import './chunk-BY4UQH2Y.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA0MO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAoCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.js","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedgerEntry,\n RecordingRef,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport type { LearnerAi } from '../ai/LkAiProvider.js';\nimport type { RecordedTake } from '../hooks/useSpeechRecorder.js';\nimport type { LkStringsOverride } from '../i18n/strings.js';\nimport type { ReadAloudAssessResult } from './ReadAloud/ReadAloud.js';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n /**\n * Binds this activity's own `data.media` to a play budget the consumer\n * persists. Usually supplied by `<ActivitySequence mediaBudget={…}>`; pass it\n * yourself when rendering an activity standalone.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */\n mediaStrings?: Partial<MediaTransportStrings>;\n /**\n * Overrides the SDK's chrome text for this activity, layered on whatever\n * `LkIntlProvider` supplies. `mediaStrings` still works and is merged after\n * this, so an existing 0.8.x call site keeps behaving as it did.\n */\n strings?: LkStringsOverride;\n onInteraction?: (event: InteractionEvent) => void;\n /**\n * The host's AI ports for this activity — an explanation after grading, hints\n * before submit — overriding `LkAiProvider`'s. Leave a port out to switch\n * that help off. Nothing AI appears in `exam`, and an item whose author\n * switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.\n */\n ai?: LearnerAi;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n /**\n * BCP 47 tag stamped as `lang` on this component's root. This is the\n * INTERFACE language — the SDK's own chrome renders inside that element — so\n * it should carry the same value you give `<LkIntlProvider locale>`. Passing\n * a different one re-declares the language of every SDK string in this\n * subtree without changing the words.\n *\n * It is NOT `data.locale`, which labels xAPI statements — and, on a\n * dictation, places the dictation's own words: `<Dictation>` puts it, and\n * the direction it names, on its title, hints, marks and solution. Other\n * authored content in another language belongs on `stimulus.locale`, which\n * `<StimulusPanel>` puts on the passage alone.\n */\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key, and\n * almost every built-in activity throws at render in the default `practice`\n * mode rather than fail later: `<MultipleChoice>`, `<FillInTheBlanks>`,\n * `<GapSelect>` and `<Dictation>` because they grade locally, and\n * `<WrittenResponse>` because `practice` still runs its local submit path and\n * emits a practice-mode xAPI statement.\n *\n * `<ReadAloud>` is the exception and renders a projection in every mode: a\n * read-aloud item has no answer key to withhold, so `redact()` removes only the\n * authored feedback and leaves the reading, its language and its bounds — which\n * is everything the component puts on screen. It grades nothing locally either;\n * the grade comes back from whatever the application asked to judge the take.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Every word the SDK's audio transport renders.\n *\n * Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and\n * is formatted by the component, because it reads identically in every locale\n * this SDK targets. The scrubber's SPOKEN value does have a word in it and does\n * have a key ({@link MediaTransportStrings.timeValue}).\n *\n * Supply them to translate it. These are the highest-stakes strings on a\n * listening paper — \"No plays remaining\" decides whether a learner believes\n * they may try again — so shipping them as untranslatable English inside a\n * Spanish panel was not acceptable. Supplying any of them also sets `lang` on\n * the transport chrome, so a screen reader does not read the SDK's own words\n * with the passage's phonetics.\n */\nexport interface MediaTransportStrings {\n play: string;\n pause: string;\n preparing: string;\n mute: string;\n unmute: string;\n volume: string;\n speed: string;\n seek: string;\n /**\n * Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.\n * Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to\n * reimplement the clock to change the word between them.\n */\n timeValue: (elapsed: string, duration: string) => string;\n /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */\n playsRemaining: (remaining: number, max: number) => string;\n noPlaysRemaining: string;\n /** Shown before the LAST play is spent, so a stray press cannot cost it. */\n lastPlayConfirm: string;\n lastPlayStart: string;\n lastPlayCancel: string;\n seekBlocked: string;\n rateBlocked: string;\n playFailed: string;\n}\n\n/**\n * Binds ONE media block to a play budget the consumer persists.\n *\n * The SDK refuses a play; it does not remember one. Everything durable here is\n * the consuming application's — see {@link SequenceMediaBudget.onPlayConsumed}.\n */\nexport interface MediaBudgetBinding {\n /** From `slotMediaKey(slotId)` / `stimulusMediaKey(slotId)` in lk-core. */\n key: string;\n /** Plays already spent, and where playback stood. Read at mount only. */\n entry?: MediaPlayLedgerEntry;\n /** Defaults to `renderMode !== 'review'`. An explicit boolean wins either way. */\n enforced?: boolean;\n /** Slot context stamped onto the claim and the interaction event. */\n slotId: string;\n index: number;\n activityId?: string;\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * The pager-level half of a play budget. One prop, because it is one concept.\n */\nexport interface SequenceMediaBudget {\n /**\n * `MediaPlayLedger.entries` goes straight in. An absent key means nothing\n * spent. Read at mount, like `responses`.\n */\n plays?: Readonly<Record<string, MediaPlayLedgerEntry>>;\n /**\n * Re-seed token. Change this string and the budgets re-seed from `plays`\n * WITHOUT remounting the pager — the invigilator path (\"the audio never\n * started, give her the play back\") that would otherwise cost the learner\n * their focus, their scroll position and an unsaved answer.\n */\n resumeKey?: string;\n /** Explicit override of the default (`renderMode !== 'review'`). */\n enforced?: boolean;\n /**\n * Called the instant a play is claimed, BEFORE any audio is audible.\n *\n * Two tiers, chosen by what you return:\n *\n * - **Return nothing (optimistic).** Playback starts immediately and the\n * count is only as durable as your write. **Do not debounce this, and do\n * not batch it with the answer autosave** — an eight-second debounce is\n * exactly long enough to start a third play and hard-reload. A `pagehide`\n * beacon is a backstop, not the mechanism. A crash between this call and\n * your write landing RETURNS the play to the learner; that is the honest\n * description of what you are buying.\n * - **Return a promise (confirmed).** Playback is held — the button reads\n * \"Preparing…\" and is `aria-busy` — until it settles. Resolve with\n * `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays\n * + 1 … RETURNING plays`, or a compare-and-set on\n * `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the\n * play, which is how a second tab is caught: two mounts both seeded at 0\n * both claim 1, and only an atomic increment can tell them apart. A\n * rejection charges nothing and lets the learner retry. This is the only\n * tier in which \"consumed before audible\" is true of storage rather than\n * only of memory; use it for summative papers.\n *\n * Never settle the promise and the learner cannot play at all: settle it.\n */\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n /**\n * Called when a charged play produced no audio — the element errored before\n * playback advanced past 0.25 s, an expired signed URL being the realistic\n * cause. Supply it to give the play back, decrementing with a compare-and-set\n * on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not\n * decrement a ledger it has no channel to correct.\n */\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n /**\n * Position reports, so a refresh resumes the play the learner already paid\n * for instead of charging them again. Fires on pause, on end (with 0), and at\n * most once per whole second of playback.\n *\n * **This one you MAY throttle** — the granularity you persist is the\n * granularity of the replay a crash grants. Three seconds is sane; three\n * minutes is not.\n */\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. Defaults are English. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * Which slot a recording belongs to, stamped on every call the pager makes.\n *\n * Exactly the shape {@link ActivitySequenceProps.onSubmit} already passes, so\n * one identity travels with the answer and with the take that answer points at\n * — and a consumer writes `slotId` in both places rather than reconciling two\n * spellings of \"which question was this\".\n */\nexport interface SequenceRecordingSlot {\n /** Identity from `flattenSequence` — stable under shuffling. Store against THIS. */\n slotId: string;\n /** Presented position, which moves under shuffling. */\n index: number;\n activityId: string;\n}\n\n/**\n * The pager-level half of a read-aloud recording binding: where every take in\n * this sequence is stored, and how a judgement comes back.\n *\n * One prop at both levels, because it is one concept — the media-budget\n * precedent ({@link SequenceMediaBudget} beside {@link MediaBudgetBinding}).\n * The difference is the second argument: a per-slot binding knows which slot it\n * belongs to, so a sequence-level one is told.\n *\n * The SDK records and hands over; it never judges. `assess` is `practice` only\n * and optional — a sequence that only stores takes gets the \"assessment is not\n * available\" notice rather than a throw — and `playbackUrl` is `review` only.\n * A slot whose activity is not a read-aloud never calls any of them.\n */\nexport interface SequenceRecordingBinding {\n /**\n * Puts the take in your storage and returns its key. **Required** outside\n * `review`: without it a learner can speak into a control that submits\n * nothing, which the component refuses to render. Settle it.\n */\n upload(take: RecordedTake, slot: SequenceRecordingSlot): Promise<RecordingRef>;\n /** Judges the stored take. `practice` only; an exam never assesses on the client. */\n assess?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<ReadAloudAssessResult>;\n /** A playable link to a stored take. `review` only. */\n playbackUrl?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<string>;\n}\n"]}
1
+ {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAmNO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAoCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.js","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n DeliveryPolicy,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedgerEntry,\n RecordingRef,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport type { LearnerAi } from '../ai/LkAiProvider.js';\nimport type { RecordedTake } from '../hooks/useSpeechRecorder.js';\nimport type { LkStringsOverride } from '../i18n/strings.js';\nimport type { ReadAloudAssessResult } from './ReadAloud/ReadAloud.js';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n /**\n * Binds this activity's own `data.media` to a play budget the consumer\n * persists. Usually supplied by `<ActivitySequence mediaBudget={…}>`; pass it\n * yourself when rendering an activity standalone.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */\n mediaStrings?: Partial<MediaTransportStrings>;\n /**\n * Overrides the SDK's chrome text for this activity, layered on whatever\n * `LkIntlProvider` supplies. `mediaStrings` still works and is merged after\n * this, so an existing 0.8.x call site keeps behaving as it did.\n */\n strings?: LkStringsOverride;\n onInteraction?: (event: InteractionEvent) => void;\n /**\n * The host's AI ports for this activity — an explanation after grading, hints\n * before submit — overriding `LkAiProvider`'s. Leave a port out to switch\n * that help off. Nothing AI appears in `exam`, and an item whose author\n * switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.\n */\n ai?: LearnerAi;\n /**\n * What the school running this paper lets a learner see: feedback,\n * solutions, hints, and AI help. Every setting only takes something away\n * from what the mode shows, and an absent policy takes nothing away. Inside\n * `<ActivitySequence>` or `<InteractiveVideo>`, the paper's own policy holds\n * as well, whichever is stricter. See `DeliveryPolicy` in lk-core.\n */\n delivery?: DeliveryPolicy | null;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n /**\n * BCP 47 tag stamped as `lang` on this component's root. This is the\n * INTERFACE language — the SDK's own chrome renders inside that element — so\n * it should carry the same value you give `<LkIntlProvider locale>`. Passing\n * a different one re-declares the language of every SDK string in this\n * subtree without changing the words.\n *\n * It is NOT `data.locale`, which labels xAPI statements — and, on a\n * dictation, places the dictation's own words: `<Dictation>` puts it, and\n * the direction it names, on its title, hints, marks and solution. Other\n * authored content in another language belongs on `stimulus.locale`, which\n * `<StimulusPanel>` puts on the passage alone.\n */\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key, and\n * almost every built-in activity throws at render in the default `practice`\n * mode rather than fail later: `<MultipleChoice>`, `<FillInTheBlanks>`,\n * `<GapSelect>` and `<Dictation>` because they grade locally, and\n * `<WrittenResponse>` because `practice` still runs its local submit path and\n * emits a practice-mode xAPI statement.\n *\n * `<ReadAloud>` is the exception and renders a projection in every mode: a\n * read-aloud item has no answer key to withhold, so `redact()` removes only the\n * authored feedback and leaves the reading, its language and its bounds — which\n * is everything the component puts on screen. It grades nothing locally either;\n * the grade comes back from whatever the application asked to judge the take.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Every word the SDK's audio transport renders.\n *\n * Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and\n * is formatted by the component, because it reads identically in every locale\n * this SDK targets. The scrubber's SPOKEN value does have a word in it and does\n * have a key ({@link MediaTransportStrings.timeValue}).\n *\n * Supply them to translate it. These are the highest-stakes strings on a\n * listening paper — \"No plays remaining\" decides whether a learner believes\n * they may try again — so shipping them as untranslatable English inside a\n * Spanish panel was not acceptable. Supplying any of them also sets `lang` on\n * the transport chrome, so a screen reader does not read the SDK's own words\n * with the passage's phonetics.\n */\nexport interface MediaTransportStrings {\n play: string;\n pause: string;\n preparing: string;\n mute: string;\n unmute: string;\n volume: string;\n speed: string;\n seek: string;\n /**\n * Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.\n * Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to\n * reimplement the clock to change the word between them.\n */\n timeValue: (elapsed: string, duration: string) => string;\n /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */\n playsRemaining: (remaining: number, max: number) => string;\n noPlaysRemaining: string;\n /** Shown before the LAST play is spent, so a stray press cannot cost it. */\n lastPlayConfirm: string;\n lastPlayStart: string;\n lastPlayCancel: string;\n seekBlocked: string;\n rateBlocked: string;\n playFailed: string;\n}\n\n/**\n * Binds ONE media block to a play budget the consumer persists.\n *\n * The SDK refuses a play; it does not remember one. Everything durable here is\n * the consuming application's — see {@link SequenceMediaBudget.onPlayConsumed}.\n */\nexport interface MediaBudgetBinding {\n /** From `slotMediaKey(slotId)` / `stimulusMediaKey(slotId)` in lk-core. */\n key: string;\n /** Plays already spent, and where playback stood. Read at mount only. */\n entry?: MediaPlayLedgerEntry;\n /** Defaults to `renderMode !== 'review'`. An explicit boolean wins either way. */\n enforced?: boolean;\n /** Slot context stamped onto the claim and the interaction event. */\n slotId: string;\n index: number;\n activityId?: string;\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * The pager-level half of a play budget. One prop, because it is one concept.\n */\nexport interface SequenceMediaBudget {\n /**\n * `MediaPlayLedger.entries` goes straight in. An absent key means nothing\n * spent. Read at mount, like `responses`.\n */\n plays?: Readonly<Record<string, MediaPlayLedgerEntry>>;\n /**\n * Re-seed token. Change this string and the budgets re-seed from `plays`\n * WITHOUT remounting the pager — the invigilator path (\"the audio never\n * started, give her the play back\") that would otherwise cost the learner\n * their focus, their scroll position and an unsaved answer.\n */\n resumeKey?: string;\n /** Explicit override of the default (`renderMode !== 'review'`). */\n enforced?: boolean;\n /**\n * Called the instant a play is claimed, BEFORE any audio is audible.\n *\n * Two tiers, chosen by what you return:\n *\n * - **Return nothing (optimistic).** Playback starts immediately and the\n * count is only as durable as your write. **Do not debounce this, and do\n * not batch it with the answer autosave** — an eight-second debounce is\n * exactly long enough to start a third play and hard-reload. A `pagehide`\n * beacon is a backstop, not the mechanism. A crash between this call and\n * your write landing RETURNS the play to the learner; that is the honest\n * description of what you are buying.\n * - **Return a promise (confirmed).** Playback is held — the button reads\n * \"Preparing…\" and is `aria-busy` — until it settles. Resolve with\n * `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays\n * + 1 … RETURNING plays`, or a compare-and-set on\n * `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the\n * play, which is how a second tab is caught: two mounts both seeded at 0\n * both claim 1, and only an atomic increment can tell them apart. A\n * rejection charges nothing and lets the learner retry. This is the only\n * tier in which \"consumed before audible\" is true of storage rather than\n * only of memory; use it for summative papers.\n *\n * Never settle the promise and the learner cannot play at all: settle it.\n */\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n /**\n * Called when a charged play produced no audio — the element errored before\n * playback advanced past 0.25 s, an expired signed URL being the realistic\n * cause. Supply it to give the play back, decrementing with a compare-and-set\n * on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not\n * decrement a ledger it has no channel to correct.\n */\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n /**\n * Position reports, so a refresh resumes the play the learner already paid\n * for instead of charging them again. Fires on pause, on end (with 0), and at\n * most once per whole second of playback.\n *\n * **This one you MAY throttle** — the granularity you persist is the\n * granularity of the replay a crash grants. Three seconds is sane; three\n * minutes is not.\n */\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. Defaults are English. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * Which slot a recording belongs to, stamped on every call the pager makes.\n *\n * Exactly the shape {@link ActivitySequenceProps.onSubmit} already passes, so\n * one identity travels with the answer and with the take that answer points at\n * — and a consumer writes `slotId` in both places rather than reconciling two\n * spellings of \"which question was this\".\n */\nexport interface SequenceRecordingSlot {\n /** Identity from `flattenSequence` — stable under shuffling. Store against THIS. */\n slotId: string;\n /** Presented position, which moves under shuffling. */\n index: number;\n activityId: string;\n}\n\n/**\n * The pager-level half of a read-aloud recording binding: where every take in\n * this sequence is stored, and how a judgement comes back.\n *\n * One prop at both levels, because it is one concept — the media-budget\n * precedent ({@link SequenceMediaBudget} beside {@link MediaBudgetBinding}).\n * The difference is the second argument: a per-slot binding knows which slot it\n * belongs to, so a sequence-level one is told.\n *\n * The SDK records and hands over; it never judges. `assess` is `practice` only\n * and optional — a sequence that only stores takes gets the \"assessment is not\n * available\" notice rather than a throw — and `playbackUrl` is `review` only.\n * A slot whose activity is not a read-aloud never calls any of them.\n */\nexport interface SequenceRecordingBinding {\n /**\n * Puts the take in your storage and returns its key. **Required** outside\n * `review`: without it a learner can speak into a control that submits\n * nothing, which the component refuses to render. Settle it.\n */\n upload(take: RecordedTake, slot: SequenceRecordingSlot): Promise<RecordingRef>;\n /** Judges the stored take. `practice` only; an exam never assesses on the client. */\n assess?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<ReadAloudAssessResult>;\n /** A playable link to a stored take. `review` only. */\n playbackUrl?(ref: RecordingRef, slot: SequenceRecordingSlot): Promise<string>;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { ReadAloudDimension, ReadAloudWordState, Stimulus, SpeechAssessment, GradeRecord, ReadAloudData, RecordingRef, LearnerResponse, ActivityData, ActivityResult, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
1
+ import { ReadAloudDimension, ReadAloudWordState, Stimulus, SpeechAssessment, GradeRecord, ReadAloudData, RecordingRef, LearnerResponse, ActivityData, ActivityResult, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, DeliveryPolicy, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
2
2
  import { LearnerAi } from './ai/LkAiProvider.cjs';
3
3
  import { S as SpeechRecorderError, R as RecordedTake } from './machine-CuwKqWXC.cjs';
4
4
 
@@ -640,6 +640,14 @@ interface ActivityProps<TData extends ActivityData = ActivityData> {
640
640
  * switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.
641
641
  */
642
642
  ai?: LearnerAi;
643
+ /**
644
+ * What the school running this paper lets a learner see: feedback,
645
+ * solutions, hints, and AI help. Every setting only takes something away
646
+ * from what the mode shows, and an absent policy takes nothing away. Inside
647
+ * `<ActivitySequence>` or `<InteractiveVideo>`, the paper's own policy holds
648
+ * as well, whichever is stricter. See `DeliveryPolicy` in lk-core.
649
+ */
650
+ delivery?: DeliveryPolicy | null;
643
651
  /** Per-instance token overrides, applied as inline CSS vars on the root. */
644
652
  theme?: Partial<ThemeTokens>;
645
653
  /**
@@ -1,4 +1,4 @@
1
- import { ReadAloudDimension, ReadAloudWordState, Stimulus, SpeechAssessment, GradeRecord, ReadAloudData, RecordingRef, LearnerResponse, ActivityData, ActivityResult, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
1
+ import { ReadAloudDimension, ReadAloudWordState, Stimulus, SpeechAssessment, GradeRecord, ReadAloudData, RecordingRef, LearnerResponse, ActivityData, ActivityResult, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, DeliveryPolicy, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
2
2
  import { LearnerAi } from './ai/LkAiProvider.js';
3
3
  import { S as SpeechRecorderError, R as RecordedTake } from './machine-CuwKqWXC.js';
4
4
 
@@ -640,6 +640,14 @@ interface ActivityProps<TData extends ActivityData = ActivityData> {
640
640
  * switched a feature off (`data.ai`) never shows it. See {@link LearnerAi}.
641
641
  */
642
642
  ai?: LearnerAi;
643
+ /**
644
+ * What the school running this paper lets a learner see: feedback,
645
+ * solutions, hints, and AI help. Every setting only takes something away
646
+ * from what the mode shows, and an absent policy takes nothing away. Inside
647
+ * `<ActivitySequence>` or `<InteractiveVideo>`, the paper's own policy holds
648
+ * as well, whichever is stricter. See `DeliveryPolicy` in lk-core.
649
+ */
650
+ delivery?: DeliveryPolicy | null;
643
651
  /** Per-instance token overrides, applied as inline CSS vars on the root. */
644
652
  theme?: Partial<ThemeTokens>;
645
653
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectif/lk-react",
3
- "version": "18.1.0",
3
+ "version": "20.0.0",
4
4
  "description": "Accessible React 19 assessment components — multiple choice, fill-in-the-blanks, written response — with a resumable exam/review pager, theming, and xAPI delivery.",
5
5
  "keywords": [
6
6
  "react",
@@ -237,7 +237,7 @@
237
237
  "peerDependencies": {
238
238
  "react": "^19",
239
239
  "react-dom": "^19",
240
- "@intellectif/lk-core": "^0.18.0"
240
+ "@intellectif/lk-core": "^0.20.0"
241
241
  },
242
242
  "devDependencies": {
243
243
  "@axe-core/react": "^4.10.0",
@@ -256,7 +256,7 @@
256
256
  "tsup": "^8.3.0",
257
257
  "vitest": "^4.1.6",
258
258
  "vitest-axe": "^0.1.0",
259
- "@intellectif/lk-core": "0.18.0"
259
+ "@intellectif/lk-core": "0.20.0"
260
260
  },
261
261
  "license": "MIT",
262
262
  "engines": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/GapSelect/GapSelect.tsx","../src/components/GapSelect/index.tsx"],"names":["useLkStrings","useLearnerAi","useMemo","isDevelopment","assertRedacted","result","validateActivity","ActivitySchemaError","useActivityState","useState","useRef","useEffect","randomSessionId","seededShuffle","score","xAPIBuilder","ANONYMOUS_ACTOR","objectIdFor","xapiDefinitionFor","jsxs","jsx","ActivityMedia","AiHints","FeedbackRegion","AiExplanation","GapSelect","ActivityErrorBoundary"],"mappings":"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","file":"chunk-3SJ7TVL6.cjs","sourcesContent":["'use client';\n\nimport {\n ActivitySchemaError,\n assertRedacted,\n type GapSelectBank,\n type GapSelectChoice,\n type GapSelectData,\n type GapSelectGap,\n type GapSelectLearnerResponse,\n type LearnerResponse,\n type ScoringDetail,\n type ScoringResult,\n score,\n seededShuffle,\n validateActivity,\n xAPIBuilder,\n xapiDefinitionFor,\n} from '@intellectif/lk-core';\nimport { type CSSProperties, useEffect, useMemo, useRef, useState } from 'react';\nimport { useLearnerAi } from '../../ai/LkAiProvider.js';\nimport { useActivityState } from '../../hooks/useActivityState.js';\nimport { useLkStrings } from '../../i18n/LkIntlProvider.js';\nimport { ANONYMOUS_ACTOR, isDevelopment, objectIdFor, randomSessionId } from '../_internal.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport { AiExplanation, AiHints } from '../shared/AiHelp.js';\nimport { FeedbackRegion } from '../shared/FeedbackRegion.js';\nimport type { ActivityProps } from '../types.js';\n\nexport interface GapSelectProps extends ActivityProps<GapSelectData> {\n /**\n * Seeds the per-gap choice shuffle when `data.shuffleChoices` is set. Pass\n * the attempt id, exactly as on `<MultipleChoice>`: an order nobody can\n * reproduce cannot be reconciled with the attempt a server recorded. Without\n * one, a random per-mount seed is used — stable within the mount, and\n * deliberately not reproducible.\n */\n shuffleSeed?: string;\n}\n\nconst PLACEHOLDER = /\\{\\{\\s*([^{}]+?)\\s*\\}\\}/g;\n\ntype Segment = { kind: 'text'; value: string } | { kind: 'gap'; id: string; ordinal: number };\n\n/** Splits a passage into literal text and its `{{id}}` gaps, in order. */\nfunction parsePassage(passage: string): Segment[] {\n const segments: Segment[] = [];\n const re = new RegExp(PLACEHOLDER);\n let last = 0;\n let ordinal = 0;\n let match = re.exec(passage);\n while (match !== null) {\n if (match.index > last) {\n segments.push({ kind: 'text', value: passage.slice(last, match.index) });\n }\n ordinal += 1;\n segments.push({ kind: 'gap', id: match[1] as string, ordinal });\n last = match.index + match[0].length;\n match = re.exec(passage);\n }\n if (last < passage.length) {\n segments.push({ kind: 'text', value: passage.slice(last) });\n }\n return segments;\n}\n\nconst NO_SELECTIONS: Record<string, string> = {};\n\n/**\n * The selections out of a `LearnerResponse`. A response of another activity\n * type reads as \"nothing selected\" rather than throwing — a controlled host\n * that has not yet swapped its state on an activity change would otherwise\n * crash the attempt.\n */\nfunction selectionsOf(response: LearnerResponse | undefined): Record<string, string> {\n return response !== undefined && response.type === 'gap-select'\n ? response.selections\n : NO_SELECTIONS;\n}\n\n/** Per-gap correctness keyed by gap id, preferring the unambiguous `outcome`. */\nfunction correctnessByItem(details: readonly ScoringDetail[]): Map<string, boolean> {\n const map = new Map<string, boolean>();\n for (const detail of details) {\n map.set(\n detail.itemId,\n detail.outcome !== undefined ? detail.outcome === 'correct' : detail.correct,\n );\n }\n return map;\n}\n\n/**\n * The choices a gap offers: its own list, or the bank it names.\n *\n * Both survive redaction — they are what the learner picks from — so this is\n * the one resolution that works identically on full and redacted data.\n */\nfunction choicesFor(\n banks: readonly GapSelectBank[] | undefined,\n gap: GapSelectGap,\n): readonly GapSelectChoice[] {\n if (gap.choices !== undefined) {\n return gap.choices;\n }\n return banks?.find((bank) => bank.id === gap.bankId)?.choices ?? [];\n}\n\nexport function GapSelect({\n data,\n onComplete,\n onSubmit,\n onChange,\n onInteraction,\n value,\n defaultValue,\n defaultSubmitted,\n renderMode = 'practice',\n outcome,\n mediaBudget,\n mediaStrings,\n strings,\n theme,\n locale,\n disabled,\n shuffleSeed,\n ai: aiProp,\n}: GapSelectProps) {\n const isExam = renderMode === 'exam';\n const isReview = renderMode === 'review';\n const s = useLkStrings(strings);\n const ai = useLearnerAi(aiProp);\n\n const devError = useMemo(() => {\n if (!isDevelopment()) {\n return null;\n }\n if (data.redacted === true) {\n try {\n assertRedacted(data);\n return null;\n } catch (error) {\n return error instanceof Error ? error : new Error(String(error));\n }\n }\n const result = validateActivity('gap-select', data);\n return result.success ? null : new ActivitySchemaError('gap-select', result.errors);\n }, [data]);\n\n const { state, start, complete, getTimeSpent, reset } = useActivityState(\n defaultSubmitted === true ? 'completed' : 'idle',\n );\n const isControlled = value !== undefined;\n const [internalSelections, setInternalSelections] = useState<Record<string, string>>(() =>\n selectionsOf(defaultValue),\n );\n const [result, setResult] = useState<ScoringResult | null>(null);\n const [summary, setSummary] = useState<string | null>(null);\n const [feedbackHidden, setFeedbackHidden] = useState(false);\n\n const selections = isControlled ? selectionsOf(value) : internalSelections;\n\n const defaultValueRef = useRef(defaultValue);\n useEffect(() => {\n defaultValueRef.current = defaultValue;\n }, [defaultValue]);\n\n const defaultSubmittedRef = useRef(defaultSubmitted);\n useEffect(() => {\n defaultSubmittedRef.current = defaultSubmitted;\n }, [defaultSubmitted]);\n\n // Identity-guarded so the mount run is a no-op: without it this fires after\n // the first paint and undoes every seed it was just given (Req 3.7).\n const lastDataRef = useRef(data);\n useEffect(() => {\n if (lastDataRef.current === data) {\n return;\n }\n lastDataRef.current = data;\n setInternalSelections(selectionsOf(defaultValueRef.current));\n setResult(null);\n setSummary(null);\n setFeedbackHidden(false);\n reset(defaultSubmittedRef.current === true ? 'completed' : 'idle');\n }, [data, reset]);\n\n const segments = useMemo(() => parsePassage(data.passage), [data.passage]);\n const gapById = useMemo(() => new Map(data.gaps.map((gap) => [gap.id, gap])), [data.gaps]);\n\n const sessionIdRef = useRef<string | null>(null);\n /**\n * The choices each gap shows, in the order it shows them.\n *\n * Seeded PER GAP, not once for the activity: two gaps drawing on the same\n * word bank must not be dealt the same order, or the bank's distractors line\n * up column-wise and the second gap becomes easier than the first.\n */\n const choicesByGap = useMemo(() => {\n const map = new Map<string, readonly GapSelectChoice[]>();\n // biome-ignore lint/suspicious/noAssignInExpressions: sanctioned lazy ref initialization\n const seedSource = shuffleSeed ?? (sessionIdRef.current ??= randomSessionId());\n for (const gap of data.gaps) {\n const choices = choicesFor(data.banks, gap);\n map.set(\n gap.id,\n data.shuffleChoices === true\n ? seededShuffle(choices, `${seedSource}:${data.id}:${gap.id}`)\n : choices,\n );\n }\n return map;\n }, [data, shuffleSeed]);\n\n // All hooks are called before these throws, so hook order stays stable.\n if (devError) {\n throw devError;\n }\n // Practice grades locally, and a redacted projection has no answer key to\n // grade against — `score()` would throw RedactedScoringError from the submit\n // handler, where no error boundary can reach it and after the learner has\n // answered. Fail at render instead, in production too, exactly as every other\n // built-in does.\n if (data.redacted === true && renderMode === 'practice') {\n throw new Error(\n `Gap Select \"${data.id}\" received redacted activity data in renderMode \"practice\", ` +\n 'which grades locally and has no answer key to grade against. ' +\n 'Render redacted data with renderMode=\"exam\" (server grades) or \"review\" (pass `outcome`).',\n );\n }\n\n const submitted = state === 'completed';\n const inactive = disabled === true || submitted || isReview;\n\n /**\n * Whether answer-key-derived information may be shown. Never in `exam`; in\n * `practice` only once this component has scored; in `review` only once the\n * caller supplies a SCORED outcome.\n */\n const revealing = isReview ? outcome?.status === 'scored' : !isExam && submitted;\n\n const fireInteraction = (\n type: 'gap-selected' | 'submitted',\n payload: Record<string, unknown>,\n ): void => {\n onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });\n };\n\n const handleSelect = (gapId: string, choiceId: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n const next = { ...selections, [gapId]: choiceId };\n if (!isControlled) {\n setInternalSelections(next);\n }\n onChange?.({ type: 'gap-select', selections: next });\n fireInteraction('gap-selected', { gapId, choiceId });\n };\n\n const handleSubmit = (event: React.FormEvent): void => {\n event.preventDefault();\n if (inactive) {\n return;\n }\n const response: GapSelectLearnerResponse = { type: 'gap-select', selections };\n onSubmit?.(response);\n\n if (isExam) {\n // The client neither grades nor reveals: no score(), no read of any\n // answer-key field, no onComplete, and no xAPI statement — its\n // correctResponsesPattern IS the answer key.\n complete();\n setSummary(s.answerSubmitted);\n fireInteraction('submitted', { selections });\n return;\n }\n\n const scoringResult = score('gap-select', data as GapSelectData, response);\n complete();\n const timeSpent = getTimeSpent();\n const xapiStatement = xAPIBuilder.buildAnsweredStatement({\n actor: ANONYMOUS_ACTOR,\n object: {\n id: objectIdFor(data.id),\n name: { [data.locale ?? 'en-US']: data.title },\n ...xapiDefinitionFor(data),\n },\n scoringResult,\n timeSpentMs: timeSpent,\n response: JSON.stringify(selections),\n });\n setResult(scoringResult);\n onComplete?.({\n score: scoringResult.score,\n maxScore: scoringResult.maxScore,\n passed: scoringResult.passed,\n timeSpent,\n xapiStatement,\n });\n const overall = scoringResult.feedback;\n setSummary(\n `${s.answerSubmitted} ${s.scoreAnnouncement(\n Math.round(scoringResult.score * 100),\n scoringResult.passed,\n )}${overall ? ` ${overall}` : ''}`,\n );\n fireInteraction('submitted', { selections, score: scoringResult.score });\n };\n\n const correctByGap = useMemo(() => {\n if (isExam) {\n return new Map<string, boolean>();\n }\n if (isReview) {\n return outcome?.status === 'scored'\n ? correctnessByItem(outcome.details)\n : new Map<string, boolean>();\n }\n return correctnessByItem(result?.details ?? []);\n }, [isExam, isReview, outcome, result]);\n\n const reviewSummary = useMemo(() => {\n if (!isReview || outcome === undefined) {\n return null;\n }\n if (outcome.status === 'scored') {\n return `${s.scoreAnnouncement(Math.round(outcome.score * 100), outcome.passed)}${\n outcome.feedback ? ` ${outcome.feedback}` : ''\n }`;\n }\n if (outcome.status === 'deferred') {\n return s.notGradedYet;\n }\n if (outcome.status === 'graded') {\n const percent =\n outcome.maxScore > 0\n ? Math.round((outcome.score / outcome.maxScore) * 100)\n : Math.round(outcome.score * 100);\n return `${s.scoreAnnouncement(percent, outcome.passed)}${\n outcome.feedback ? ` ${outcome.feedback}` : ''\n }`;\n }\n return s.noGradeAvailable;\n }, [isReview, outcome, s]);\n\n const anyFeedback = data.gaps.some((gap) => gap.feedback !== undefined);\n\n return (\n <form\n className=\"lk-gs\"\n aria-label={data.title}\n lang={locale}\n data-render-mode={renderMode}\n style={theme as CSSProperties | undefined}\n onSubmit={handleSubmit}\n >\n {data.media ? (\n <ActivityMedia\n media={data.media}\n renderMode={renderMode}\n {...(mediaBudget !== undefined ? { mediaBudget } : {})}\n {...(mediaStrings !== undefined ? { mediaStrings } : {})}\n {...(strings !== undefined ? { strings } : {})}\n {...(onInteraction !== undefined ? { onInteraction } : {})}\n {...(locale !== undefined ? { locale } : {})}\n />\n ) : null}\n <fieldset disabled={inactive}>\n <p className=\"lk-gs-passage\">\n {segments.map((seg, i) => {\n if (seg.kind === 'text') {\n // biome-ignore lint/suspicious/noArrayIndexKey: passage segments are positional and static\n return <span key={`t${i}`}>{seg.value}</span>;\n }\n const gap = gapById.get(seg.id);\n if (!gap) {\n return null;\n }\n const choices = choicesByGap.get(seg.id) ?? [];\n const correctness = correctByGap.get(seg.id);\n // Read defensively and never in exam: a redact() projection omits\n // both at runtime while the type still says they are there.\n const gapFeedback = isExam ? undefined : gap.feedback;\n return (\n <span key={seg.id} className=\"lk-gs-gap\">\n <select\n className=\"lk-gs-select\"\n aria-label={s.gapLabel(seg.ordinal)}\n value={selections[seg.id] ?? ''}\n disabled={inactive}\n aria-disabled={inactive || undefined}\n data-correct={\n revealing && correctness !== undefined ? String(correctness) : undefined\n }\n onChange={(e) => handleSelect(seg.id, e.target.value)}\n >\n {/*\n The empty entry is part of the contract, not a decoration.\n It is how a learner leaves a gap alone — and how they take\n an answer back — so \"not answered\" stays distinguishable\n from \"answered wrongly\" all the way into ItemOutcome, where\n the scorer reports it as `incorrect-omission`.\n */}\n <option value=\"\">{s.gapPlaceholder}</option>\n {choices.map((choice) => (\n <option key={choice.id} value={choice.id}>\n {choice.text}\n </option>\n ))}\n </select>\n {revealing && !feedbackHidden && gapFeedback ? (\n <span\n className=\"lk-gs-gap-feedback\"\n role=\"note\"\n data-correct={correctness !== undefined ? String(correctness) : undefined}\n >\n {gapFeedback}\n </span>\n ) : null}\n </span>\n );\n })}\n </p>\n <AiHints\n ai={ai}\n data={data}\n renderMode={renderMode}\n submitted={submitted}\n disabled={disabled === true}\n response={{ type: 'gap-select', selections }}\n locale={locale}\n onInteraction={onInteraction}\n strings={s}\n />\n {isReview ? null : (\n <button type=\"submit\" disabled={inactive}>\n {isExam ? s.submitAnswers : s.checkAnswers}\n </button>\n )}\n </fieldset>\n {revealing && anyFeedback ? (\n <button\n type=\"button\"\n className=\"lk-gs-feedback-toggle\"\n aria-expanded={!feedbackHidden}\n onClick={() => setFeedbackHidden((h) => !h)}\n >\n {feedbackHidden ? s.showFeedback : s.hideFeedback}\n </button>\n ) : null}\n <FeedbackRegion id={`${data.id}-feedback`}>\n {isReview ? reviewSummary : summary}\n </FeedbackRegion>\n <AiExplanation\n ai={ai}\n data={data}\n renderMode={renderMode}\n submitted={submitted}\n response={{ type: 'gap-select', selections }}\n outcome={outcome}\n locale={locale}\n onInteraction={onInteraction}\n strings={s}\n />\n </form>\n );\n}\n","'use client';\n\nimport { useLkStrings } from '../../i18n/LkIntlProvider.js';\nimport { ActivityErrorBoundary } from '../ActivityErrorBoundary.js';\nimport { GapSelect as GapSelectCore, type GapSelectProps } from './GapSelect.js';\n\nexport type { GapSelectProps } from './GapSelect.js';\n\n/**\n * Public Gap Select activity: the core wrapped in `ActivityErrorBoundary` so a\n * render failure (including the dev schema-validation throw and the\n * redacted-in-`practice` guard) degrades to an accessible fallback instead of\n * crashing the host.\n */\nexport function GapSelect(props: GapSelectProps) {\n const strings = useLkStrings(props.strings);\n return (\n <ActivityErrorBoundary activityTitle={props.data?.title} strings={strings}>\n <GapSelectCore {...props} />\n </ActivityErrorBoundary>\n );\n}\n"]}