@intellectif/lk-react 20.0.0 → 21.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 (168) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +30 -2
  3. package/dist/{WrittenResponse-obIDDYDH.d.cts → WrittenResponse-BVPmrmQL.d.cts} +1 -1
  4. package/dist/{WrittenResponse-D2YI8xOK.d.ts → WrittenResponse-IAJvvL-g.d.ts} +1 -1
  5. package/dist/ai/LkAiProvider.cjs +3 -3
  6. package/dist/ai/LkAiProvider.d.cts +5 -3
  7. package/dist/ai/LkAiProvider.d.ts +5 -3
  8. package/dist/ai/LkAiProvider.js +1 -1
  9. package/dist/ai/useAiHelp.cjs +5 -5
  10. package/dist/ai/useAiHelp.d.cts +1 -1
  11. package/dist/ai/useAiHelp.d.ts +1 -1
  12. package/dist/ai/useAiHelp.js +3 -3
  13. package/dist/{chunk-W6DMHN33.js → chunk-5MO7ACSB.js} +10 -10
  14. package/dist/{chunk-W6DMHN33.js.map → chunk-5MO7ACSB.js.map} +1 -1
  15. package/dist/{chunk-7K4B2EMU.js → chunk-5Q2KFIZQ.js} +4 -4
  16. package/dist/{chunk-7K4B2EMU.js.map → chunk-5Q2KFIZQ.js.map} +1 -1
  17. package/dist/{chunk-APZ6JFN6.cjs → chunk-6SNNMVBV.cjs} +5 -5
  18. package/dist/{chunk-APZ6JFN6.cjs.map → chunk-6SNNMVBV.cjs.map} +1 -1
  19. package/dist/{chunk-L7WY5YFQ.cjs → chunk-7JBXDQTD.cjs} +5 -5
  20. package/dist/{chunk-L7WY5YFQ.cjs.map → chunk-7JBXDQTD.cjs.map} +1 -1
  21. package/dist/{chunk-NWNQ2PHT.js → chunk-AY3WN4FB.js} +8 -8
  22. package/dist/{chunk-NWNQ2PHT.js.map → chunk-AY3WN4FB.js.map} +1 -1
  23. package/dist/{chunk-CEUEXISZ.cjs → chunk-CB2A56Q7.cjs} +131 -50
  24. package/dist/chunk-CB2A56Q7.cjs.map +1 -0
  25. package/dist/chunk-CQ37UD4T.cjs +251 -0
  26. package/dist/chunk-CQ37UD4T.cjs.map +1 -0
  27. package/dist/{chunk-YRNEOBXA.js → chunk-EYLBSS4Y.js} +11 -2
  28. package/dist/chunk-EYLBSS4Y.js.map +1 -0
  29. package/dist/{chunk-AHGB5HH6.cjs → chunk-GHKMX5UT.cjs} +11 -2
  30. package/dist/chunk-GHKMX5UT.cjs.map +1 -0
  31. package/dist/chunk-GND3OBVE.js +241 -0
  32. package/dist/chunk-GND3OBVE.js.map +1 -0
  33. package/dist/{chunk-DZVF3IGB.cjs → chunk-GPGCBDBN.cjs} +59 -25
  34. package/dist/chunk-GPGCBDBN.cjs.map +1 -0
  35. package/dist/{chunk-LK5VXNMR.cjs → chunk-GW3YIPDE.cjs} +18 -18
  36. package/dist/{chunk-LK5VXNMR.cjs.map → chunk-GW3YIPDE.cjs.map} +1 -1
  37. package/dist/{chunk-BJLQKJZX.cjs → chunk-J2QIHZ3H.cjs} +2 -2
  38. package/dist/chunk-J2QIHZ3H.cjs.map +1 -0
  39. package/dist/{chunk-3XP6VO4G.js → chunk-KRM6DX47.js} +139 -45
  40. package/dist/chunk-KRM6DX47.js.map +1 -0
  41. package/dist/{chunk-PWLJDP3X.cjs → chunk-LHFC2BCM.cjs} +95 -27
  42. package/dist/chunk-LHFC2BCM.cjs.map +1 -0
  43. package/dist/{chunk-G7AGM2Y7.js → chunk-M3K6BF5M.js} +4 -4
  44. package/dist/{chunk-G7AGM2Y7.js.map → chunk-M3K6BF5M.js.map} +1 -1
  45. package/dist/{chunk-IXMT7X3E.cjs → chunk-M4UJBLVR.cjs} +17 -6
  46. package/dist/chunk-M4UJBLVR.cjs.map +1 -0
  47. package/dist/{chunk-YNDKAYW3.js → chunk-MCTNLUKO.js} +2 -2
  48. package/dist/{chunk-YNDKAYW3.js.map → chunk-MCTNLUKO.js.map} +1 -1
  49. package/dist/{chunk-ZDYNAWDZ.cjs → chunk-MPHX3Y42.cjs} +2 -2
  50. package/dist/{chunk-ZDYNAWDZ.cjs.map → chunk-MPHX3Y42.cjs.map} +1 -1
  51. package/dist/{chunk-AQIAQHJC.js → chunk-NDIKHMLJ.js} +4 -4
  52. package/dist/{chunk-AQIAQHJC.js.map → chunk-NDIKHMLJ.js.map} +1 -1
  53. package/dist/{chunk-LX2X4KC7.cjs → chunk-NILHU555.cjs} +7 -7
  54. package/dist/{chunk-LX2X4KC7.cjs.map → chunk-NILHU555.cjs.map} +1 -1
  55. package/dist/{chunk-JLDSCHY5.cjs → chunk-OKEN6NJL.cjs} +150 -61
  56. package/dist/chunk-OKEN6NJL.cjs.map +1 -0
  57. package/dist/{chunk-ORBU3XKH.js → chunk-PJMXHDY5.js} +16 -5
  58. package/dist/chunk-PJMXHDY5.js.map +1 -0
  59. package/dist/{chunk-QKW2GGTV.js → chunk-U2BX2RWY.js} +84 -16
  60. package/dist/chunk-U2BX2RWY.js.map +1 -0
  61. package/dist/{chunk-WBLYD5QA.js → chunk-U4KTOH7C.js} +2 -2
  62. package/dist/chunk-U4KTOH7C.js.map +1 -0
  63. package/dist/{chunk-FEOFX4QW.js → chunk-U6SEII5V.js} +3 -3
  64. package/dist/{chunk-FEOFX4QW.js.map → chunk-U6SEII5V.js.map} +1 -1
  65. package/dist/{chunk-6S2CKZ4H.cjs → chunk-W2P5RILE.cjs} +145 -51
  66. package/dist/chunk-W2P5RILE.cjs.map +1 -0
  67. package/dist/{chunk-AG7MWZYZ.js → chunk-W6TWQY6S.js} +144 -55
  68. package/dist/chunk-W6TWQY6S.js.map +1 -0
  69. package/dist/{chunk-HREA5SMB.cjs → chunk-WGHMWBEJ.cjs} +116 -47
  70. package/dist/chunk-WGHMWBEJ.cjs.map +1 -0
  71. package/dist/{chunk-5GI53C6T.js → chunk-WNDNQ6NR.js} +109 -40
  72. package/dist/chunk-WNDNQ6NR.js.map +1 -0
  73. package/dist/{chunk-RT2PP4SO.cjs → chunk-WXESKL75.cjs} +30 -30
  74. package/dist/{chunk-RT2PP4SO.cjs.map → chunk-WXESKL75.cjs.map} +1 -1
  75. package/dist/{chunk-BY4UQH2Y.js → chunk-X4YWXZ4J.js} +3 -3
  76. package/dist/{chunk-BY4UQH2Y.js.map → chunk-X4YWXZ4J.js.map} +1 -1
  77. package/dist/{chunk-HI6BUUIJ.js → chunk-XB2AHEU5.js} +48 -14
  78. package/dist/chunk-XB2AHEU5.js.map +1 -0
  79. package/dist/{chunk-IQ55QNBI.js → chunk-XM5Q2ZCH.js} +125 -44
  80. package/dist/chunk-XM5Q2ZCH.js.map +1 -0
  81. package/dist/{chunk-UHIOVOFK.cjs → chunk-XZCMUDFP.cjs} +12 -12
  82. package/dist/{chunk-UHIOVOFK.cjs.map → chunk-XZCMUDFP.cjs.map} +1 -1
  83. package/dist/{chunk-TRMWBJWT.cjs → chunk-Y4IDF5UO.cjs} +8 -8
  84. package/dist/{chunk-TRMWBJWT.cjs.map → chunk-Y4IDF5UO.cjs.map} +1 -1
  85. package/dist/{chunk-3FBBP5QR.cjs → chunk-YWJ5XJDU.cjs} +6 -6
  86. package/dist/{chunk-3FBBP5QR.cjs.map → chunk-YWJ5XJDU.cjs.map} +1 -1
  87. package/dist/{chunk-BGA2IVQY.js → chunk-ZDDBWUUA.js} +6 -6
  88. package/dist/{chunk-BGA2IVQY.js.map → chunk-ZDDBWUUA.js.map} +1 -1
  89. package/dist/components/ActivityPreview.cjs +17 -17
  90. package/dist/components/ActivityPreview.d.cts +2 -2
  91. package/dist/components/ActivityPreview.d.ts +2 -2
  92. package/dist/components/ActivityPreview.js +16 -16
  93. package/dist/components/ActivitySequence.cjs +18 -18
  94. package/dist/components/ActivitySequence.d.cts +22 -4
  95. package/dist/components/ActivitySequence.d.ts +22 -4
  96. package/dist/components/ActivitySequence.js +17 -17
  97. package/dist/components/Dictation.cjs +10 -10
  98. package/dist/components/Dictation.d.cts +1 -1
  99. package/dist/components/Dictation.d.ts +1 -1
  100. package/dist/components/Dictation.js +9 -9
  101. package/dist/components/FillInTheBlanks.cjs +10 -10
  102. package/dist/components/FillInTheBlanks.d.cts +1 -1
  103. package/dist/components/FillInTheBlanks.d.ts +1 -1
  104. package/dist/components/FillInTheBlanks.js +9 -9
  105. package/dist/components/GapSelect.cjs +10 -10
  106. package/dist/components/GapSelect.d.cts +1 -1
  107. package/dist/components/GapSelect.d.ts +1 -1
  108. package/dist/components/GapSelect.js +9 -9
  109. package/dist/components/InteractiveVideo.cjs +17 -17
  110. package/dist/components/InteractiveVideo.d.cts +19 -3
  111. package/dist/components/InteractiveVideo.d.ts +19 -3
  112. package/dist/components/InteractiveVideo.js +15 -15
  113. package/dist/components/MultipleChoice.cjs +10 -10
  114. package/dist/components/MultipleChoice.d.cts +1 -1
  115. package/dist/components/MultipleChoice.d.ts +1 -1
  116. package/dist/components/MultipleChoice.js +9 -9
  117. package/dist/components/PronunciationFeedback.cjs +4 -4
  118. package/dist/components/PronunciationFeedback.d.cts +1 -1
  119. package/dist/components/PronunciationFeedback.d.ts +1 -1
  120. package/dist/components/PronunciationFeedback.js +3 -3
  121. package/dist/components/ReadAloud.cjs +8 -8
  122. package/dist/components/ReadAloud.d.cts +2 -2
  123. package/dist/components/ReadAloud.d.ts +2 -2
  124. package/dist/components/ReadAloud.js +7 -7
  125. package/dist/components/StimulusPanel.cjs +4 -4
  126. package/dist/components/StimulusPanel.d.cts +1 -1
  127. package/dist/components/StimulusPanel.d.ts +1 -1
  128. package/dist/components/StimulusPanel.js +3 -3
  129. package/dist/components/WrittenResponse.cjs +7 -7
  130. package/dist/components/WrittenResponse.d.cts +3 -3
  131. package/dist/components/WrittenResponse.d.ts +3 -3
  132. package/dist/components/WrittenResponse.js +6 -6
  133. package/dist/i18n/LkIntlProvider.cjs +7 -7
  134. package/dist/i18n/LkIntlProvider.d.cts +2 -2
  135. package/dist/i18n/LkIntlProvider.d.ts +2 -2
  136. package/dist/i18n/LkIntlProvider.js +1 -1
  137. package/dist/index.cjs +41 -41
  138. package/dist/index.cjs.map +1 -1
  139. package/dist/index.d.cts +2 -2
  140. package/dist/index.d.ts +2 -2
  141. package/dist/index.js +19 -19
  142. package/dist/index.js.map +1 -1
  143. package/dist/theme/skin.css +55 -0
  144. package/dist/{types-C-GMCJ0g.d.ts → types-7lGlo0zE.d.ts} +48 -1
  145. package/dist/{types-B_PCcqO-.d.cts → types-Botf4Wqj.d.cts} +48 -1
  146. package/package.json +3 -3
  147. package/dist/chunk-3XP6VO4G.js.map +0 -1
  148. package/dist/chunk-5GI53C6T.js.map +0 -1
  149. package/dist/chunk-6S2CKZ4H.cjs.map +0 -1
  150. package/dist/chunk-AG7MWZYZ.js.map +0 -1
  151. package/dist/chunk-AHGB5HH6.cjs.map +0 -1
  152. package/dist/chunk-BJLQKJZX.cjs.map +0 -1
  153. package/dist/chunk-CEUEXISZ.cjs.map +0 -1
  154. package/dist/chunk-DZVF3IGB.cjs.map +0 -1
  155. package/dist/chunk-HI6BUUIJ.js.map +0 -1
  156. package/dist/chunk-HREA5SMB.cjs.map +0 -1
  157. package/dist/chunk-IQ55QNBI.js.map +0 -1
  158. package/dist/chunk-IXMT7X3E.cjs.map +0 -1
  159. package/dist/chunk-JLDSCHY5.cjs.map +0 -1
  160. package/dist/chunk-NOC2M6LU.cjs +0 -118
  161. package/dist/chunk-NOC2M6LU.cjs.map +0 -1
  162. package/dist/chunk-ORBU3XKH.js.map +0 -1
  163. package/dist/chunk-PWLJDP3X.cjs.map +0 -1
  164. package/dist/chunk-QKW2GGTV.js.map +0 -1
  165. package/dist/chunk-RUS42NMN.js +0 -115
  166. package/dist/chunk-RUS42NMN.js.map +0 -1
  167. package/dist/chunk-WBLYD5QA.js.map +0 -1
  168. package/dist/chunk-YRNEOBXA.js.map +0 -1
@@ -1,14 +1,15 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkNOC2M6LU_cjs = require('./chunk-NOC2M6LU.cjs');
5
- var chunkZDYNAWDZ_cjs = require('./chunk-ZDYNAWDZ.cjs');
6
- var chunkIXMT7X3E_cjs = require('./chunk-IXMT7X3E.cjs');
7
- var chunkAPZ6JFN6_cjs = require('./chunk-APZ6JFN6.cjs');
4
+ var chunkCQ37UD4T_cjs = require('./chunk-CQ37UD4T.cjs');
5
+ var chunkMPHX3Y42_cjs = require('./chunk-MPHX3Y42.cjs');
6
+ var chunkM4UJBLVR_cjs = require('./chunk-M4UJBLVR.cjs');
7
+ var chunkJ2QIHZ3H_cjs = require('./chunk-J2QIHZ3H.cjs');
8
+ var chunk6SNNMVBV_cjs = require('./chunk-6SNNMVBV.cjs');
8
9
  var chunkEANGKAKG_cjs = require('./chunk-EANGKAKG.cjs');
9
- var chunkL7WY5YFQ_cjs = require('./chunk-L7WY5YFQ.cjs');
10
+ var chunk7JBXDQTD_cjs = require('./chunk-7JBXDQTD.cjs');
10
11
  var chunkXOR4MPEN_cjs = require('./chunk-XOR4MPEN.cjs');
11
- var chunkAHGB5HH6_cjs = require('./chunk-AHGB5HH6.cjs');
12
+ var chunkGHKMX5UT_cjs = require('./chunk-GHKMX5UT.cjs');
12
13
  var lkCore = require('@intellectif/lk-core');
13
14
  var react = require('react');
14
15
  var jsxRuntime = require('react/jsx-runtime');
@@ -64,11 +65,13 @@ function MultipleChoice({
64
65
  shuffleSeed,
65
66
  strings,
66
67
  ai: aiProp,
67
- delivery
68
+ delivery,
69
+ scoring
68
70
  }) {
69
- const s = chunkAHGB5HH6_cjs.useLkStrings(strings);
70
- const ai = chunkZDYNAWDZ_cjs.useLearnerAi(aiProp);
71
- const policy = chunkIXMT7X3E_cjs.useDeliveryPolicy(delivery);
71
+ const s = chunkGHKMX5UT_cjs.useLkStrings(strings);
72
+ const ai = chunkMPHX3Y42_cjs.useLearnerAi(aiProp);
73
+ const policy = chunkM4UJBLVR_cjs.useDeliveryPolicy(delivery);
74
+ const scoringPolicy = chunkCQ37UD4T_cjs.useItemScoringPolicy(scoring);
72
75
  const devError = react.useMemo(() => {
73
76
  if (!chunkEANGKAKG_cjs.isDevelopment()) {
74
77
  return null;
@@ -83,10 +86,23 @@ function MultipleChoice({
83
86
  const { state, start, complete, getTimeSpent, reset } = chunkXOR4MPEN_cjs.useActivityState(
84
87
  defaultSubmitted === true ? "completed" : "idle"
85
88
  );
89
+ const tries = chunkCQ37UD4T_cjs.useTries({
90
+ policy: scoringPolicy,
91
+ graded: renderMode === "practice" && policy.feedback,
92
+ submitted: state === "completed",
93
+ disabled: disabled === true
94
+ });
95
+ const resetTries = tries.reset;
96
+ const closeQuestion = tries.close;
86
97
  const [internalSelection, setInternalSelection] = react.useState(
87
98
  () => selectionOf(defaultValue)
88
99
  );
89
100
  const [summary, setSummary] = react.useState(null);
101
+ const [overall, setOverall] = react.useState(null);
102
+ const seedHintsRef = react.useRef(chunkCQ37UD4T_cjs.hintsOf(value ?? defaultValue));
103
+ const rootRef = react.useRef(null);
104
+ const feedbackRef = react.useRef(null);
105
+ const focusAfterRef = react.useRef(null);
90
106
  const defaultValueRef = react.useRef(defaultValue);
91
107
  react.useEffect(() => {
92
108
  defaultValueRef.current = defaultValue;
@@ -103,8 +119,11 @@ function MultipleChoice({
103
119
  lastDataRef.current = data;
104
120
  setInternalSelection(selectionOf(defaultValueRef.current));
105
121
  setSummary(null);
122
+ setOverall(null);
123
+ seedHintsRef.current = chunkCQ37UD4T_cjs.hintsOf(defaultValueRef.current);
124
+ resetTries();
106
125
  reset(defaultSubmittedRef.current === true ? "completed" : "idle");
107
- }, [data, reset]);
126
+ }, [data, reset, resetTries]);
108
127
  const displayedOptions = react.useMemo(() => {
109
128
  if (!data.shuffle) {
110
129
  return [...data.options];
@@ -118,6 +137,48 @@ function MultipleChoice({
118
137
  }
119
138
  return new Map(outcome.details.map((detail) => [detail.itemId, detail]));
120
139
  }, [renderMode, outcome]);
140
+ const answered = state === "completed";
141
+ const chosen = value !== void 0 ? selectionOf(value) : internalSelection;
142
+ const aiHints = chunkCQ37UD4T_cjs.useComponentAiHints({
143
+ ai,
144
+ data,
145
+ renderMode,
146
+ submitted: answered,
147
+ disabled: disabled === true,
148
+ response: { type: "multiple-choice", selectedOptionIds: chosen },
149
+ locale,
150
+ onInteraction,
151
+ strings: s,
152
+ delivery: policy
153
+ });
154
+ const hintsRevealed = renderMode === "practice" ? seedHintsRef.current + aiHints.used : 0;
155
+ const responseOf = (selectedOptionIds) => ({
156
+ type: "multiple-choice",
157
+ selectedOptionIds,
158
+ ...hintsRevealed > 0 ? { hintsRevealed } : {}
159
+ });
160
+ const hintsSeenRef = react.useRef(aiHints.used);
161
+ react.useEffect(() => {
162
+ if (hintsSeenRef.current === aiHints.used) {
163
+ return;
164
+ }
165
+ hintsSeenRef.current = aiHints.used;
166
+ if (aiHints.used > 0) {
167
+ onChange?.(responseOf(chosen));
168
+ }
169
+ }, [aiHints.used]);
170
+ react.useEffect(() => {
171
+ const target = focusAfterRef.current;
172
+ if (target === null) {
173
+ return;
174
+ }
175
+ focusAfterRef.current = null;
176
+ if (target === "answer") {
177
+ rootRef.current?.querySelector("input:not(:disabled)")?.focus();
178
+ } else {
179
+ feedbackRef.current?.focus();
180
+ }
181
+ });
121
182
  if (devError) {
122
183
  throw devError;
123
184
  }
@@ -135,7 +196,7 @@ function MultipleChoice({
135
196
  const selected = isControlled ? selectionOf(value) : internalSelection;
136
197
  const reveal = isReview || renderMode === "practice" && submitted;
137
198
  const marks = reveal && policy.feedback;
138
- const solutions = marks && policy.solutions;
199
+ const solutions = marks && policy.solutions && !tries.open;
139
200
  const fireInteraction = (type, payload) => {
140
201
  onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });
141
202
  };
@@ -143,7 +204,7 @@ function MultipleChoice({
143
204
  if (!isControlled) {
144
205
  setInternalSelection(selectedOptionIds);
145
206
  }
146
- onChange?.({ type: "multiple-choice", selectedOptionIds });
207
+ onChange?.(responseOf(selectedOptionIds));
147
208
  };
148
209
  const selectSingle = (optionId) => {
149
210
  if (inactive) {
@@ -170,10 +231,7 @@ function MultipleChoice({
170
231
  if (inactive) {
171
232
  return;
172
233
  }
173
- const response = {
174
- type: "multiple-choice",
175
- selectedOptionIds: selected
176
- };
234
+ const response = responseOf(selected);
177
235
  onSubmit?.(response);
178
236
  if (isExam) {
179
237
  complete();
@@ -181,9 +239,22 @@ function MultipleChoice({
181
239
  fireInteraction("submitted", { selectedOptionIds: selected });
182
240
  return;
183
241
  }
184
- const scoringResult = lkCore.score("multiple-choice", data, response);
242
+ const answer = lkCore.score("multiple-choice", data, response);
185
243
  complete();
186
244
  const timeSpent = getTimeSpent();
245
+ const counted = tries.record({
246
+ score: answer.score,
247
+ maxScore: answer.maxScore,
248
+ hintsRevealed
249
+ });
250
+ const mine = counted.tries[counted.tries.length - 1];
251
+ const costed = mine === void 0 ? answer.score : mine.scored;
252
+ const scoringResult = costed === answer.score ? answer : {
253
+ ...answer,
254
+ score: costed,
255
+ passed: lkCore.computePassThreshold(data, costed)
256
+ };
257
+ const passed = counted.score === scoringResult.score ? scoringResult.passed : lkCore.computePassThreshold(data, counted.score);
187
258
  const xapiStatement = lkCore.xAPIBuilder.buildAnsweredStatement({
188
259
  actor: chunkEANGKAKG_cjs.ANONYMOUS_ACTOR,
189
260
  object: {
@@ -202,25 +273,37 @@ function MultipleChoice({
202
273
  timeSpentMs: timeSpent,
203
274
  response: selected.join(",")
204
275
  });
205
- onComplete?.({
206
- score: scoringResult.score,
207
- maxScore: scoringResult.maxScore,
208
- passed: scoringResult.passed,
209
- timeSpent,
210
- xapiStatement
211
- });
212
- const overall = scoringResult.feedback;
276
+ onComplete?.(
277
+ chunkJ2QIHZ3H_cjs.stampTake(
278
+ {
279
+ score: counted.score,
280
+ maxScore: counted.maxScore,
281
+ passed,
282
+ timeSpent,
283
+ xapiStatement
284
+ },
285
+ chunkJ2QIHZ3H_cjs.mintTake()
286
+ )
287
+ );
288
+ setOverall(answer.feedback);
213
289
  setSummary(
214
- `${s.answerSubmitted} ${s.scoreAnnouncement(
215
- Math.round(scoringResult.score * 100),
216
- scoringResult.passed
217
- )}${overall ? ` ${overall}` : ""}`
290
+ `${s.answerSubmitted} ${s.scoreAnnouncement(Math.round(counted.score * 100), passed)}`
218
291
  );
219
292
  fireInteraction("submitted", {
220
293
  selectedOptionIds: selected,
221
294
  score: scoringResult.score
222
295
  });
223
296
  };
297
+ const retry = () => {
298
+ reset("idle");
299
+ setSummary(null);
300
+ setOverall(null);
301
+ focusAfterRef.current = "answer";
302
+ };
303
+ const closeTries = () => {
304
+ closeQuestion();
305
+ focusAfterRef.current = "feedback";
306
+ };
224
307
  const questionId = `${data.id}-question`;
225
308
  const questionHtml = sanitizeHtml !== void 0 && data.questionHtml !== void 0 ? sanitizeHtml(data.questionHtml) : null;
226
309
  const optionList = displayedOptions.map((option) => {
@@ -270,9 +353,11 @@ function MultipleChoice({
270
353
  )
271
354
  ] }, option.id);
272
355
  });
273
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-mc", lang: locale, style: theme, children: [
356
+ const extra = !isReview && policy.feedback && submitted && tries.counted !== null ? chunkCQ37UD4T_cjs.triesSummary(s, scoringPolicy, tries.counted, tries.open) : "";
357
+ const announced = summary === null ? null : `${summary}${extra ? ` ${extra}` : ""}${overall ? ` ${overall}` : ""}`;
358
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-mc", lang: locale, style: theme, ref: rootRef, children: [
274
359
  data.media ? /* @__PURE__ */ jsxRuntime.jsx(
275
- chunkL7WY5YFQ_cjs.ActivityMedia,
360
+ chunk7JBXDQTD_cjs.ActivityMedia,
276
361
  {
277
362
  media: data.media,
278
363
  renderMode,
@@ -292,33 +377,37 @@ function MultipleChoice({
292
377
  }
293
378
  ),
294
379
  isSingle ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "radiogroup", "aria-labelledby": questionId, children: optionList }) : /* @__PURE__ */ jsxRuntime.jsx("div", { children: optionList }),
295
- /* @__PURE__ */ jsxRuntime.jsx(
296
- chunkNOC2M6LU_cjs.AiHints,
297
- {
298
- ai,
299
- data,
300
- renderMode,
301
- submitted,
302
- disabled: disabled === true,
303
- response: { type: "multiple-choice", selectedOptionIds: selected },
304
- locale,
305
- onInteraction,
306
- strings: s,
307
- delivery: policy
308
- }
309
- ),
380
+ aiHints.offered && renderMode === "practice" ? /* @__PURE__ */ jsxRuntime.jsx(chunkCQ37UD4T_cjs.HintCost, { policy: scoringPolicy, strings: s }) : null,
381
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCQ37UD4T_cjs.AiHints, { help: aiHints, ai, strings: s }),
310
382
  isReview ? null : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: s.submit })
311
383
  ] }) }),
312
- /* @__PURE__ */ jsxRuntime.jsx(chunkIXMT7X3E_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: isReview ? (
313
- // A grade read back is feedback; "not graded yet" is not.
314
- policy.feedback || !chunkIXMT7X3E_cjs.outcomeShowsMarks(outcome) ? reviewAnnouncement(outcome, s) : null
315
- ) : (
316
- // Without feedback a submit says only that it was received, as
317
- // an exam's does — the score still reaches `onComplete`.
318
- policy.feedback || summary === "" ? summary : s.answerSubmitted
319
- ) }),
320
384
  /* @__PURE__ */ jsxRuntime.jsx(
321
- chunkNOC2M6LU_cjs.AiExplanation,
385
+ chunkM4UJBLVR_cjs.FeedbackRegion,
386
+ {
387
+ id: `${data.id}-feedback`,
388
+ ...scoringPolicy.retries > 0 ? { ref: feedbackRef } : {},
389
+ children: isReview ? (
390
+ // A grade read back is feedback; "not graded yet" is not.
391
+ policy.feedback || !chunkM4UJBLVR_cjs.outcomeShowsMarks(outcome) ? reviewAnnouncement(outcome, s) : null
392
+ ) : (
393
+ // Without feedback a submit says only that it was received, as
394
+ // an exam's does — the score still reaches `onComplete`.
395
+ policy.feedback || summary === "" ? announced : s.answerSubmitted
396
+ )
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsxRuntime.jsx(
400
+ chunkCQ37UD4T_cjs.TryActions,
401
+ {
402
+ tries,
403
+ solutions: policy.solutions,
404
+ strings: s,
405
+ onRetry: retry,
406
+ onClose: closeTries
407
+ }
408
+ ),
409
+ /* @__PURE__ */ jsxRuntime.jsx(
410
+ chunkCQ37UD4T_cjs.AiExplanation,
322
411
  {
323
412
  ai,
324
413
  data,
@@ -329,16 +418,16 @@ function MultipleChoice({
329
418
  locale,
330
419
  onInteraction,
331
420
  strings: s,
332
- delivery: policy
421
+ delivery: tries.open ? { ...policy, solutions: false } : policy
333
422
  }
334
423
  )
335
424
  ] });
336
425
  }
337
426
  function MultipleChoice2(props) {
338
- const strings = chunkAHGB5HH6_cjs.useLkStrings(props.strings);
339
- return /* @__PURE__ */ jsxRuntime.jsx(chunkAPZ6JFN6_cjs.ActivityErrorBoundary, { activityTitle: props.data?.title, strings, children: /* @__PURE__ */ jsxRuntime.jsx(MultipleChoice, { ...props }) });
427
+ const strings = chunkGHKMX5UT_cjs.useLkStrings(props.strings);
428
+ return /* @__PURE__ */ jsxRuntime.jsx(chunk6SNNMVBV_cjs.ActivityErrorBoundary, { activityTitle: props.data?.title, strings, children: /* @__PURE__ */ jsxRuntime.jsx(MultipleChoice, { ...props }) });
340
429
  }
341
430
 
342
431
  exports.MultipleChoice = MultipleChoice2;
343
- //# sourceMappingURL=chunk-JLDSCHY5.cjs.map
344
- //# sourceMappingURL=chunk-JLDSCHY5.cjs.map
432
+ //# sourceMappingURL=chunk-OKEN6NJL.cjs.map
433
+ //# sourceMappingURL=chunk-OKEN6NJL.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/MultipleChoice/MultipleChoice.tsx","../src/components/MultipleChoice/index.tsx"],"names":["useLkStrings","useLearnerAi","useDeliveryPolicy","useItemScoringPolicy","useMemo","isDevelopment","validateActivity","ActivitySchemaError","useRef","useActivityState","useTries","useState","hintsOf","useEffect","randomSessionId","seededShuffle","useComponentAiHints","score","computePassThreshold","xAPIBuilder","ANONYMOUS_ACTOR","objectIdFor","xapiDefinitionFor","stampTake","mintTake","jsxs","jsx","Fragment","triesSummary","ActivityMedia","HintCost","AiHints","FeedbackRegion","outcomeShowsMarks","TryActions","AiExplanation","MultipleChoice","ActivityErrorBoundary"],"mappings":"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","file":"chunk-OKEN6NJL.cjs","sourcesContent":["'use client';\n\nimport {\n ActivitySchemaError,\n computePassThreshold,\n type ItemOutcome,\n type LearnerResponse,\n type MultipleChoiceData,\n type MultipleChoiceLearnerResponse,\n type MultipleChoiceOption,\n type ScoringDetail,\n score,\n seededShuffle,\n validateActivity,\n xAPIBuilder,\n xapiDefinitionFor,\n} from '@intellectif/lk-core';\nimport { type CSSProperties, Fragment, 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 type { LkStrings } from '../../i18n/strings.js';\nimport { ANONYMOUS_ACTOR, isDevelopment, objectIdFor, randomSessionId } from '../_internal.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport { AiExplanation, AiHints, useComponentAiHints } from '../shared/AiHelp.js';\nimport { outcomeShowsMarks, useDeliveryPolicy } from '../shared/delivery.js';\nimport { FeedbackRegion } from '../shared/FeedbackRegion.js';\nimport { mintTake, stampTake } from '../shared/sequence-slot.js';\nimport {\n HintCost,\n hintsOf,\n TryActions,\n triesSummary,\n useItemScoringPolicy,\n useTries,\n} from '../shared/tries.js';\nimport type { ActivityProps } from '../types.js';\n\n/**\n * Selected option ids carried by a learner response. Any other response shape\n * (a caller wiring the wrong item's state through `value`) reads as \"nothing\n * selected\" rather than throwing mid-exam.\n */\nfunction selectionOf(response: LearnerResponse | undefined): string[] {\n return response?.type === 'multiple-choice' ? response.selectedOptionIds : [];\n}\n\n/**\n * Whether an option is part of the correct answer, derived from the SERVER's\n * outcome — review mode never holds an answer key of its own.\n * {@link ScoringDetail.outcome} states this unambiguously. For 0.2-era details\n * that carry only the deprecated `correct` flag (which means \"the learner\n * ACTED correctly on this option\", not \"this option is the answer\") the fact\n * is still recoverable, because we know whether the learner selected it:\n * `isCorrect = wasSelected ? correct : !correct`.\n */\nfunction isAnswerOption(detail: ScoringDetail, wasSelected: boolean): boolean {\n switch (detail.outcome) {\n case 'correct':\n case 'incorrect-omission':\n return true;\n case 'incorrect':\n case 'correct-omission':\n return false;\n default:\n return wasSelected ? detail.correct : !detail.correct;\n }\n}\n\n/**\n * Announcement for `review` mode, built only from the server-supplied outcome.\n *\n * The authored overall feedback is appended by this function rather than being\n * baked into the score sentence, so a translation of \"Score 80%. Passed.\" never\n * has to carry the author's words with it.\n */\nfunction reviewAnnouncement(outcome: ItemOutcome | undefined, s: LkStrings): string | null {\n if (outcome === undefined) {\n return null;\n }\n if (outcome.status === 'scored') {\n const overall = outcome.feedback;\n return `${s.scoreAnnouncement(Math.round(outcome.score * 100), outcome.passed)}${\n overall ? ` ${overall}` : ''\n }`;\n }\n if (outcome.status === 'deferred') {\n // Never \"0%\": ungraded is not the same as wrong.\n return s.notGradedYet;\n }\n if (outcome.status === 'unscorable') {\n return s.noGradeAvailable;\n }\n // `graded` is left silent here deliberately: a returned GradeRecord on a\n // multiple-choice item has no rubric to render, and announcing a bare\n // percentage would duplicate what the marking already shows.\n return null;\n}\n\nexport interface MultipleChoiceProps extends ActivityProps<MultipleChoiceData> {\n /**\n * Seed for the deterministic option shuffle. Supply one (e.g. the attempt\n * id) to make the shuffled order reproducible server-side, stable across\n * page reloads, and identical between SSR and hydration. When absent, a\n * random per-mount session seed is used (order stable within the mount\n * only — the behaviour before `shuffleSeed` existed).\n */\n shuffleSeed?: string;\n}\n\nexport function MultipleChoice({\n data,\n onComplete,\n onSubmit,\n value,\n defaultValue,\n defaultSubmitted,\n onChange,\n renderMode = 'practice',\n outcome,\n sanitizeHtml,\n mediaBudget,\n mediaStrings,\n onInteraction,\n theme,\n locale,\n disabled,\n shuffleSeed,\n strings,\n ai: aiProp,\n delivery,\n scoring,\n}: MultipleChoiceProps) {\n const s = useLkStrings(strings);\n const ai = useLearnerAi(aiProp);\n const policy = useDeliveryPolicy(delivery);\n const scoringPolicy = useItemScoringPolicy(scoring);\n\n // Dev-only boundary validation (Req 2.3). Throwing during render lets\n // ActivityErrorBoundary catch it. Memoised so it only re-runs on data change.\n const devError = useMemo(() => {\n if (!isDevelopment()) {\n return null;\n }\n // A `redact()` projection cannot satisfy the full content schema — the\n // answer key is gone by design — and must not be checked against the\n // strict redacted schema either, since a `reveal: 'after-submit'`\n // projection legitimately carries answer-key fields for review renders.\n if (data.redacted === true) {\n return null;\n }\n const result = validateActivity('multiple-choice', data);\n return result.success ? null : new ActivitySchemaError('multiple-choice', result.errors);\n }, [data]);\n\n // Lazily created only when shuffling without a caller seed: avoids calling\n // crypto.randomUUID (absent on non-secure http origins) unless needed.\n const sessionIdRef = useRef<string | null>(null);\n const { state, start, complete, getTimeSpent, reset } = useActivityState(\n defaultSubmitted === true ? 'completed' : 'idle',\n );\n // Before anything that resets them: a new question starts with no tries.\n const tries = useTries({\n policy: scoringPolicy,\n graded: renderMode === 'practice' && policy.feedback,\n submitted: state === 'completed',\n disabled: disabled === true,\n });\n const resetTries = tries.reset;\n const closeQuestion = tries.close;\n // Uncontrolled state. `defaultValue` seeds the mount only (React convention);\n // to re-seed later, remount with a `key` or drive the component with `value`.\n const [internalSelection, setInternalSelection] = useState<string[]>(() =>\n selectionOf(defaultValue),\n );\n const [summary, setSummary] = useState<string | null>(null);\n // The authored overall feedback on the last graded answer, said after the\n // score and what the policy made of it.\n const [overall, setOverall] = useState<string | null>(null);\n // Hints the learner had been shown before this mount: a restored answer's\n // count stands, and the hints shown here add to it.\n const seedHintsRef = useRef(hintsOf(value ?? defaultValue));\n const rootRef = useRef<HTMLDivElement>(null);\n const feedbackRef = useRef<HTMLDivElement>(null);\n const focusAfterRef = useRef<'answer' | 'feedback' | null>(null);\n\n // Reset on data-prop CHANGE (Req 3.7). The identity guard makes the mount\n // run a no-op, which it always was before `defaultValue` existed — without\n // it this effect would wipe the seed immediately after the first render.\n // (It also makes the reset StrictMode-safe: a remount with unchanged data\n // no longer discards the learner's selection.)\n // Resets back to `defaultValue`, not to empty. Clearing looked safer — it\n // cannot carry a stale answer into a different question — but `data`\n // identity is a poor proxy for \"different question\": a parent that builds\n // entries in render (`activities={raw.map(redact)}`, the documented exam\n // pattern) hands over a new object every render, and clearing wiped every\n // RESTORED answer on the first unrelated re-render, silently. `defaultValue`\n // is by definition the caller's seed for the CURRENT data, which is what\n // FillInTheBlanks already reset to; the two now agree.\n const defaultValueRef = useRef(defaultValue);\n useEffect(() => {\n defaultValueRef.current = defaultValue;\n }, [defaultValue]);\n // A data change must return the item to its SEED, submitted state included —\n // resetting to idle unlocked work the learner had already committed.\n const defaultSubmittedRef = useRef(defaultSubmitted);\n useEffect(() => {\n defaultSubmittedRef.current = defaultSubmitted;\n }, [defaultSubmitted]);\n const lastDataRef = useRef(data);\n useEffect(() => {\n if (lastDataRef.current === data) {\n return;\n }\n lastDataRef.current = data;\n setInternalSelection(selectionOf(defaultValueRef.current));\n setSummary(null);\n setOverall(null);\n seedHintsRef.current = hintsOf(defaultValueRef.current);\n resetTries();\n reset(defaultSubmittedRef.current === true ? 'completed' : 'idle');\n }, [data, reset, resetTries]);\n\n const displayedOptions = useMemo<MultipleChoiceOption[]>(() => {\n if (!data.shuffle) {\n return [...data.options];\n }\n // biome-ignore lint/suspicious/noAssignInExpressions: sanctioned lazy ref initialization\n const seedSource = shuffleSeed ?? (sessionIdRef.current ??= randomSessionId());\n // The seed string is unchanged from when the algorithm lived in this file,\n // so an order a consumer recorded against a seed still reproduces.\n return seededShuffle(data.options, `${seedSource}:${data.id}`);\n }, [data, shuffleSeed]);\n\n // Per-option correctness for `review`, indexed by option id. Null unless the\n // caller supplied a SCORED outcome — the only source of truth this mode has.\n const reviewDetails = useMemo(() => {\n if (renderMode !== 'review' || outcome === undefined || outcome.status !== 'scored') {\n return null;\n }\n return new Map(outcome.details.map((detail) => [detail.itemId, detail]));\n }, [renderMode, outcome]);\n\n // Hints are a hook, so they are read here, before the throws below.\n const answered = state === 'completed';\n const chosen = value !== undefined ? selectionOf(value) : internalSelection;\n const aiHints = useComponentAiHints({\n ai,\n data,\n renderMode,\n submitted: answered,\n disabled: disabled === true,\n response: { type: 'multiple-choice', selectedOptionIds: chosen },\n locale,\n onInteraction,\n strings: s,\n delivery: policy,\n });\n // Every hint the learner has been shown on this question, counted from its\n // start: what a scoring policy charges for. Hints exist only in practice.\n const hintsRevealed = renderMode === 'practice' ? seedHintsRef.current + aiHints.used : 0;\n const responseOf = (selectedOptionIds: string[]): MultipleChoiceLearnerResponse => ({\n type: 'multiple-choice',\n selectedOptionIds,\n ...(hintsRevealed > 0 ? { hintsRevealed } : {}),\n });\n // A hint shown is part of the answer's record, so a host that restores an\n // answer restores what it cost: say so as it happens, not at the next click.\n const hintsSeenRef = useRef(aiHints.used);\n // biome-ignore lint/correctness/useExhaustiveDependencies: a new hint is the trigger, and the response it reports is this render's\n useEffect(() => {\n if (hintsSeenRef.current === aiHints.used) {\n return;\n }\n hintsSeenRef.current = aiHints.used;\n if (aiHints.used > 0) {\n onChange?.(responseOf(chosen));\n }\n }, [aiHints.used]);\n // After \"Try again\" the learner is back at their answer; after \"Show answer\"\n // the buttons they pressed are gone, and the result is what is left.\n useEffect(() => {\n const target = focusAfterRef.current;\n if (target === null) {\n return;\n }\n focusAfterRef.current = null;\n if (target === 'answer') {\n rootRef.current?.querySelector<HTMLElement>('input:not(:disabled)')?.focus();\n } else {\n feedbackRef.current?.focus();\n }\n });\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, which a redacted projection cannot support:\n // `score()` would throw from the submit handler — where no error boundary\n // can catch it — AFTER the learner has answered. Fail loudly at render\n // instead, in production too. Wiring an exam item into the self-grading\n // mode is precisely the accident `renderMode` exists to prevent.\n if (data.redacted === true && renderMode === 'practice') {\n throw new Error(\n `Multiple Choice \"${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 isSingle = data.mode === 'single';\n const isExam = renderMode === 'exam';\n const isReview = renderMode === 'review';\n const submitted = state === 'completed';\n const inactive = disabled === true || submitted || isReview;\n const isControlled = value !== undefined;\n const selected = isControlled ? selectionOf(value) : internalSelection;\n /**\n * Whether correctness and authored feedback may be shown. Practice earns it\n * by grading locally after submit; review shows what the server already\n * graded. Exam never reveals anything — not before, not after submit.\n */\n const reveal = isReview || (renderMode === 'practice' && submitted);\n /**\n * What the delivery policy lets that reveal show. `marks`: right and wrong at\n * all, and the author's feedback. `solutions`: the right answer where the\n * learner did not choose it — without it, only the options the learner chose\n * are marked, so a missed correct option is not given away.\n */\n const marks = reveal && policy.feedback;\n // While the learner is offered another try, the right answer stays hidden:\n // the next try is for finding it.\n const solutions = marks && policy.solutions && !tries.open;\n\n const fireInteraction = (\n type: 'option-selected' | 'option-deselected' | 'submitted',\n payload: Record<string, unknown>,\n ): void => {\n onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });\n };\n\n /** Single funnel for every response change: owns state only when uncontrolled. */\n const emitChange = (selectedOptionIds: string[]): void => {\n if (!isControlled) {\n setInternalSelection(selectedOptionIds);\n }\n onChange?.(responseOf(selectedOptionIds));\n };\n\n const selectSingle = (optionId: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n emitChange([optionId]);\n fireInteraction('option-selected', { optionId });\n };\n\n const toggleMulti = (optionId: string, checked: boolean): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n emitChange(checked ? [...selected, optionId] : selected.filter((id) => id !== optionId));\n fireInteraction(checked ? 'option-selected' : 'option-deselected', { optionId });\n };\n\n const handleSubmit = (event: React.FormEvent): void => {\n event.preventDefault();\n if (inactive) {\n return;\n }\n const response = responseOf(selected);\n // Always first, and before anything that can throw: an exam runner must be\n // able to persist the raw response no matter what happens after.\n onSubmit?.(response);\n\n if (isExam) {\n // No score(), no answer key read, no onComplete, no xAPI — the server\n // grades. The announcement deliberately carries no correctness signal.\n complete();\n setSummary(s.answerSubmitted);\n fireInteraction('submitted', { selectedOptionIds: selected });\n return;\n }\n\n const answer = score('multiple-choice', data as MultipleChoiceData, response);\n complete();\n const timeSpent = getTimeSpent();\n // The question's score under the policy: this try's answer, less what its\n // hints and the tries before it cost, or an earlier try's. With no policy\n // it is exactly what the answer scored.\n const counted = tries.record({\n score: answer.score,\n maxScore: answer.maxScore,\n hintsRevealed,\n });\n const mine = counted.tries[counted.tries.length - 1];\n const costed = mine === undefined ? answer.score : mine.scored;\n // The statement records this try, at what it scored after its costs.\n const scoringResult =\n costed === answer.score\n ? answer\n : {\n ...answer,\n score: costed,\n passed: computePassThreshold(data as MultipleChoiceData, costed),\n };\n const passed =\n counted.score === scoringResult.score\n ? scoringResult.passed\n : computePassThreshold(data as MultipleChoiceData, counted.score);\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 // Activity-type IRI, interaction type and correct-responses pattern\n // come from the REGISTERED descriptor, so a consumer-registered type\n // gets correct interop without touching this component.\n ...xapiDefinitionFor(data),\n choices: data.options.map((option) => ({\n id: option.id,\n description: { [data.locale ?? 'en-US']: option.text },\n })),\n },\n scoringResult,\n timeSpentMs: timeSpent,\n response: selected.join(','),\n });\n // Stamped, so a set this question sits in knows a later try replaces it.\n onComplete?.(\n stampTake(\n {\n score: counted.score,\n maxScore: counted.maxScore,\n passed,\n timeSpent,\n xapiStatement,\n },\n mintTake(),\n ),\n );\n // Core selects the authored overall feedback on `passed` (B3 fix): it is\n // the answer's, so it follows the answer's own grade.\n setOverall(answer.feedback);\n setSummary(\n `${s.answerSubmitted} ${s.scoreAnnouncement(Math.round(counted.score * 100), passed)}`,\n );\n fireInteraction('submitted', {\n selectedOptionIds: selected,\n score: scoringResult.score,\n });\n };\n\n /** \"Try again\": the answer stays, to be changed; the marks go. */\n const retry = (): void => {\n reset('idle');\n setSummary(null);\n setOverall(null);\n focusAfterRef.current = 'answer';\n };\n\n /** \"Show answer\": no more tries; the question shows what a finished one shows. */\n const closeTries = (): void => {\n closeQuestion();\n focusAfterRef.current = 'feedback';\n };\n\n const questionId = `${data.id}-question`;\n // Rich text renders ONLY through a caller-supplied sanitiser; without one we\n // fall back to the plain-text field, escaped by React (types.ts HtmlSanitizer).\n const questionHtml =\n sanitizeHtml !== undefined && data.questionHtml !== undefined\n ? sanitizeHtml(data.questionHtml)\n : null;\n\n const optionList = displayedOptions.map((option) => {\n const checked = selected.includes(option.id);\n // `data-correct` means \"this option is part of the correct answer\".\n let correctness: string | undefined;\n // An option the learner did not choose is marked only where solutions\n // show: marking the right one they missed would hand them the answer.\n const markable = marks && (solutions || checked);\n if (isReview) {\n const detail = reviewDetails?.get(option.id);\n correctness =\n !markable || detail === undefined ? undefined : String(isAnswerOption(detail, checked));\n } else if (markable) {\n correctness = String(option.isCorrect);\n }\n const media = option.media;\n // A picture goes INSIDE the label: it has no controls of its own, clicking\n // it is how a learner picks that option, and its `alt` joins the option's\n // accessible name. A recording cannot — see below.\n const picture =\n media?.type === 'image' ? (\n <img className=\"lk-mc-option-image\" src={media.url} alt={media.alt ?? ''} />\n ) : null;\n\n const label = (\n <label className=\"lk-mc-option\" data-correct={correctness}>\n <input\n type={isSingle ? 'radio' : 'checkbox'}\n name={isSingle ? `${data.id}-options` : undefined}\n value={option.id}\n checked={checked}\n disabled={inactive}\n aria-disabled={inactive || undefined}\n onChange={(e) =>\n isSingle ? selectSingle(option.id) : toggleMulti(option.id, e.target.checked)\n }\n />\n <span>{option.text}</span>\n {picture}\n {markable && option.feedback ? (\n <span className=\"lk-mc-option-feedback\" role=\"note\">\n {option.feedback}\n </span>\n ) : null}\n </label>\n );\n\n if (media?.type !== 'audio') {\n return <Fragment key={option.id}>{label}</Fragment>;\n }\n\n // A recording sits OUTSIDE the label, deliberately. A <label> activates its\n // control for any click inside it, so an <audio> nested in one would select\n // the option the moment the learner pressed play — and a listening item is\n // answered by comparing all the recordings before choosing any of them.\n // Selecting an option must stay something the learner does on purpose.\n return (\n <div className=\"lk-mc-option-media\" key={option.id}>\n {label}\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio\n className=\"lk-mc-option-audio\"\n src={media.url}\n controls\n preload=\"none\"\n aria-label={media.alt ?? option.text}\n >\n {media.captionsUrl ? (\n <track kind=\"captions\" src={media.captionsUrl} label={media.alt ?? option.text} />\n ) : null}\n </audio>\n </div>\n );\n });\n\n const extra =\n !isReview && policy.feedback && submitted && tries.counted !== null\n ? triesSummary(s, scoringPolicy, tries.counted, tries.open)\n : '';\n const announced =\n summary === null\n ? null\n : `${summary}${extra ? ` ${extra}` : ''}${overall ? ` ${overall}` : ''}`;\n\n return (\n <div className=\"lk-mc\" lang={locale} style={theme as CSSProperties | undefined} ref={rootRef}>\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 <form onSubmit={handleSubmit}>\n <fieldset disabled={inactive}>\n {questionHtml === null ? (\n <legend id={questionId}>{data.question}</legend>\n ) : (\n <legend\n id={questionId}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is the output of the caller-supplied sanitizeHtml (types.ts HtmlSanitizer contract)\n dangerouslySetInnerHTML={{ __html: questionHtml }}\n />\n )}\n {/*\n single: an explicit radiogroup is meaningful (fieldset's implicit\n role is `group`, not `radiogroup`). multi: the fieldset + legend\n already provide a named group — an explicit role=\"group\" here would\n be a redundant, duplicate same-named group in the a11y tree, so we\n use a plain layout div. (Refines the design ARIA sketch.)\n */}\n {isSingle ? (\n <div role=\"radiogroup\" aria-labelledby={questionId}>\n {optionList}\n </div>\n ) : (\n <div>{optionList}</div>\n )}\n {aiHints.offered && renderMode === 'practice' ? (\n <HintCost policy={scoringPolicy} strings={s} />\n ) : null}\n <AiHints help={aiHints} ai={ai} strings={s} />\n {/* review is read-only: there is nothing left to submit. */}\n {isReview ? null : (\n <button type=\"submit\" disabled={inactive}>\n {s.submit}\n </button>\n )}\n </fieldset>\n </form>\n <FeedbackRegion\n id={`${data.id}-feedback`}\n {...(scoringPolicy.retries > 0 ? { ref: feedbackRef } : {})}\n >\n {isReview\n ? // A grade read back is feedback; \"not graded yet\" is not.\n policy.feedback || !outcomeShowsMarks(outcome)\n ? reviewAnnouncement(outcome, s)\n : null\n : // Without feedback a submit says only that it was received, as\n // an exam's does — the score still reaches `onComplete`.\n policy.feedback || summary === ''\n ? announced\n : s.answerSubmitted}\n </FeedbackRegion>\n <TryActions\n tries={tries}\n solutions={policy.solutions}\n strings={s}\n onRetry={retry}\n onClose={closeTries}\n />\n <AiExplanation\n ai={ai}\n data={data}\n renderMode={renderMode}\n submitted={submitted}\n response={{ type: 'multiple-choice', selectedOptionIds: selected }}\n outcome={outcome}\n locale={locale}\n onInteraction={onInteraction}\n strings={s}\n // An explanation all but always names the answer: not while another\n // try is on offer.\n delivery={tries.open ? { ...policy, solutions: false } : policy}\n />\n </div>\n );\n}\n","'use client';\n\nimport { useLkStrings } from '../../i18n/LkIntlProvider.js';\nimport { ActivityErrorBoundary } from '../ActivityErrorBoundary.js';\nimport {\n MultipleChoice as MultipleChoiceCore,\n type MultipleChoiceProps,\n} from './MultipleChoice.js';\n\nexport type { MultipleChoiceProps } from './MultipleChoice.js';\n\n/**\n * Public Multiple Choice activity: the core component wrapped in\n * `ActivityErrorBoundary` so a render failure (incl. the dev schema-validation\n * throw) degrades to an accessible fallback instead of crashing the host.\n */\nexport function MultipleChoice(props: MultipleChoiceProps) {\n // Read here rather than in the boundary: a class component cannot call a\n // hook, and the fallback it renders is learner-facing text like any other.\n const strings = useLkStrings(props.strings);\n return (\n <ActivityErrorBoundary activityTitle={props.data?.title} strings={strings}>\n <MultipleChoiceCore {...props} />\n </ActivityErrorBoundary>\n );\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { SequenceSlotContext } from './chunk-WBLYD5QA.js';
2
+ import { SequenceSlotContext } from './chunk-U4KTOH7C.js';
3
3
  import { isDevelopment } from './chunk-PPFVTXGJ.js';
4
4
  import { validateDeliveryPolicy, combineDeliveryPolicies } from '@intellectif/lk-core';
5
5
  import { useContext, useEffect } from 'react';
@@ -31,10 +31,21 @@ function policyKey(given) {
31
31
  function outcomeShowsMarks(outcome) {
32
32
  return outcome?.status === "scored" || outcome?.status === "graded";
33
33
  }
34
- function FeedbackRegion({ id, className, children }) {
35
- return /* @__PURE__ */ jsx("div", { id, className, "aria-live": "polite", "aria-atomic": "true", children });
34
+ function FeedbackRegion({ id, className, children, ref }) {
35
+ return /* @__PURE__ */ jsx(
36
+ "div",
37
+ {
38
+ id,
39
+ className,
40
+ "aria-live": "polite",
41
+ "aria-atomic": "true",
42
+ ref,
43
+ ...ref !== void 0 ? { tabIndex: -1 } : {},
44
+ children
45
+ }
46
+ );
36
47
  }
37
48
 
38
49
  export { FeedbackRegion, outcomeShowsMarks, useDeliveryPolicy };
39
- //# sourceMappingURL=chunk-ORBU3XKH.js.map
40
- //# sourceMappingURL=chunk-ORBU3XKH.js.map
50
+ //# sourceMappingURL=chunk-PJMXHDY5.js.map
51
+ //# sourceMappingURL=chunk-PJMXHDY5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/shared/delivery.ts","../src/components/shared/FeedbackRegion.tsx"],"names":[],"mappings":";;;;;;AAyBO,SAAS,kBACd,KAAA,EACwB;AACxB,EAAA,MAAM,MAAA,GAAS,UAAA,CAAW,mBAAmB,CAAA,EAAG,QAAA;AAChD,EAAA,MAAM,GAAA,GAAM,UAAU,KAAK,CAAA;AAE3B,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,aAAA,EAAc,IAAK,KAAA,KAAU,MAAA,IAAa,UAAU,IAAA,EAAM;AAC7D,MAAA;AAAA,IACF;AACA,IAAA,MAAM,OAAA,GAAU,uBAAuB,KAAK,CAAA;AAC5C,IAAA,IAAI,CAAC,QAAQ,OAAA,EAAS;AACpB,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN,6GAA6G,OAAA,CAAQ,MAAA,CAClH,GAAA,CAAI,CAAC,UAAU,CAAA,EAAG,KAAA,CAAM,IAAA,IAAQ,QAAQ,WAAM,KAAA,CAAM,OAAO,EAAE,CAAA,CAC7D,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,OACf;AAAA,IACF;AAAA,EACF,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AACR,EAAA,OAAO,uBAAA,CAAwB,OAAO,MAAM,CAAA;AAC9C;AAGA,SAAS,UAAU,KAAA,EAAwB;AACzC,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA,IAAK,EAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,OAAO,KAAK,CAAA;AAAA,EACrB;AACF;AAOO,SAAS,kBAAkB,OAAA,EAA2C;AAC3E,EAAA,OAAO,OAAA,EAAS,MAAA,KAAW,QAAA,IAAY,OAAA,EAAS,MAAA,KAAW,QAAA;AAC7D;AC3CO,SAAS,eAAe,EAAE,EAAA,EAAI,SAAA,EAAW,QAAA,EAAU,KAAI,EAAwB;AACpF,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,EAAA;AAAA,MACA,SAAA;AAAA,MACA,WAAA,EAAU,QAAA;AAAA,MACV,aAAA,EAAY,MAAA;AAAA,MACZ,GAAA;AAAA,MACC,GAAI,GAAA,KAAQ,MAAA,GAAY,EAAE,QAAA,EAAU,EAAA,KAAO,EAAC;AAAA,MAE5C;AAAA;AAAA,GACH;AAEJ","file":"chunk-PJMXHDY5.js","sourcesContent":["'use client';\n\nimport {\n combineDeliveryPolicies,\n type DeliveryPolicy,\n type ItemOutcome,\n type ResolvedDeliveryPolicy,\n validateDeliveryPolicy,\n} from '@intellectif/lk-core';\nimport { useContext, useEffect } from 'react';\nimport { isDevelopment } from '../_internal.js';\nimport { SequenceSlotContext } from './sequence-slot.js';\n\n/**\n * The delivery policy a question is under: the one it was handed, and the one\n * of the paper around it, whichever is stricter setting by setting.\n *\n * The paper's comes from the slot channel, never from a prop, for the reason\n * the mode does: a host that draws its own question is expected to pass\n * `delivery` on and nothing makes it. A paper that switched hints off keeps\n * them off in every question it holds, however that question was drawn.\n *\n * A policy that would not pass `validateDeliveryPolicy` is still applied — a\n * setting nobody can read restricts — and a development build says so once.\n */\nexport function useDeliveryPolicy(\n given: DeliveryPolicy | null | undefined,\n): ResolvedDeliveryPolicy {\n const around = useContext(SequenceSlotContext)?.delivery;\n const key = policyKey(given);\n // biome-ignore lint/correctness/useExhaustiveDependencies: the policy's content is the trigger, not its identity\n useEffect(() => {\n if (!isDevelopment() || given === undefined || given === null) {\n return;\n }\n const checked = validateDeliveryPolicy(given);\n if (!checked.success) {\n console.warn(\n `learning-kit: this delivery policy is not valid, and its unreadable settings are applied as restrictions: ${checked.issues\n .map((issue) => `${issue.path || 'policy'} — ${issue.message}`)\n .join('; ')}`,\n );\n }\n }, [key]);\n return combineDeliveryPolicies(given, around);\n}\n\n/** A policy's content as a string, so a new object with the same settings is the same policy. */\nfunction policyKey(given: unknown): string {\n try {\n return JSON.stringify(given) ?? '';\n } catch {\n return String(given);\n }\n}\n\n/**\n * Whether reading an outcome back says how the answer was marked — a score, a\n * pass or a fail — which a policy without `feedback` does not show. \"Not graded\n * yet\" and \"No grade available\" say nothing about the answer, and stay.\n */\nexport function outcomeShowsMarks(outcome: ItemOutcome | undefined): boolean {\n return outcome?.status === 'scored' || outcome?.status === 'graded';\n}\n","import type { ReactNode, Ref } from 'react';\n\nexport interface FeedbackRegionProps {\n /** Identifier so activity inputs can reference this region via aria. */\n id?: string;\n className?: string;\n children?: ReactNode;\n /**\n * Where focus can be put, when what focus was on goes away: \"Show answer\",\n * pressed, leaves the page. Given, the region takes `tabIndex={-1}` — focus\n * by script, never a stop in the tab order.\n */\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * A polite ARIA live region. Screen readers announce content injected here\n * (score, feedback, state changes) without moving focus. Pure/presentational\n * — safe to render in any environment.\n */\nexport function FeedbackRegion({ id, className, children, ref }: FeedbackRegionProps) {\n return (\n <div\n id={id}\n className={className}\n aria-live=\"polite\"\n aria-atomic=\"true\"\n ref={ref}\n {...(ref !== undefined ? { tabIndex: -1 } : {})}\n >\n {children}\n </div>\n );\n}\n"]}
@@ -1,15 +1,15 @@
1
1
  'use client';
2
- import { StimulusPanel } from './chunk-7K4B2EMU.js';
3
- import { WrittenResponse } from './chunk-BGA2IVQY.js';
4
- import { MultipleChoice } from './chunk-AG7MWZYZ.js';
5
- import { FillInTheBlanks } from './chunk-3XP6VO4G.js';
6
- import { GapSelect } from './chunk-IQ55QNBI.js';
7
- import { Dictation } from './chunk-5GI53C6T.js';
8
- import { ReadAloud, stopCaptureGroup } from './chunk-NWNQ2PHT.js';
9
- import { takeOf, mintTake, SequenceSlotContext } from './chunk-WBLYD5QA.js';
2
+ import { StimulusPanel } from './chunk-5Q2KFIZQ.js';
3
+ import { WrittenResponse } from './chunk-ZDDBWUUA.js';
4
+ import { MultipleChoice } from './chunk-W6TWQY6S.js';
5
+ import { FillInTheBlanks } from './chunk-KRM6DX47.js';
6
+ import { GapSelect } from './chunk-XM5Q2ZCH.js';
7
+ import { Dictation } from './chunk-WNDNQ6NR.js';
8
+ import { ReadAloud, stopCaptureGroup } from './chunk-AY3WN4FB.js';
9
+ import { takeOf, mintTake, SequenceSlotContext } from './chunk-U4KTOH7C.js';
10
10
  import { randomSessionId } from './chunk-PPFVTXGJ.js';
11
- import { useLkStrings } from './chunk-YRNEOBXA.js';
12
- import { isItemGroup, flattenSequence, resolveDeliveryPolicy, resolvePlaybackPolicy, slotMediaKey, stimulusMediaKey } from '@intellectif/lk-core';
11
+ import { useLkStrings } from './chunk-EYLBSS4Y.js';
12
+ import { isItemGroup, flattenSequence, resolveDeliveryPolicy, resolveItemScoringPolicy, resolvePlaybackPolicy, slotMediaKey, stimulusMediaKey } from '@intellectif/lk-core';
13
13
  import { useRef, useState, useMemo, useEffect } from 'react';
14
14
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
15
 
@@ -78,7 +78,8 @@ function ActivitySequence({
78
78
  workletUrl,
79
79
  strings,
80
80
  ai,
81
- delivery
81
+ delivery,
82
+ scoring
82
83
  }) {
83
84
  const sessionIdRef = useRef(null);
84
85
  const s = useLkStrings(strings);
@@ -126,6 +127,16 @@ function ActivitySequence({
126
127
  modeRef.current = renderMode;
127
128
  const deliveryRef = useRef(resolveDeliveryPolicy(delivery));
128
129
  deliveryRef.current = resolveDeliveryPolicy(delivery);
130
+ const scoringKey = JSON.stringify(scoring ?? null);
131
+ const scoringInForce = useMemo(
132
+ () => scoring === void 0 || scoring === null ? void 0 : resolveItemScoringPolicy(scoring),
133
+ [scoringKey]
134
+ );
135
+ const scoringRef = useRef(scoringInForce);
136
+ scoringRef.current = scoringInForce;
137
+ const [, setReported] = useState(false);
138
+ const triesRef = useRef(/* @__PURE__ */ new Map());
139
+ const triedRef = useRef(/* @__PURE__ */ new Map());
129
140
  const latestRef = useRef(null);
130
141
  const aliveRef = useRef(true);
131
142
  useEffect(() => {
@@ -173,6 +184,9 @@ function ActivitySequence({
173
184
  hostPendingRef.current = /* @__PURE__ */ new Set();
174
185
  portalRefsRef.current = /* @__PURE__ */ new Map();
175
186
  reportedRef.current = false;
187
+ triesRef.current = /* @__PURE__ */ new Map();
188
+ triedRef.current = /* @__PURE__ */ new Map();
189
+ setReported(false);
176
190
  }
177
191
  const setGeneration = setGenerationRef.current;
178
192
  useEffect(() => {
@@ -231,6 +245,11 @@ function ActivitySequence({
231
245
  if (hostPendingRef.current.size > 0) {
232
246
  return;
233
247
  }
248
+ for (const [at, state] of triesRef.current) {
249
+ if (state === "retrying" || at === indexRef.current) {
250
+ return;
251
+ }
252
+ }
234
253
  const items = [];
235
254
  for (const [at, outcome] of held.entries()) {
236
255
  const take = takesRef.current.get(at);
@@ -252,6 +271,7 @@ function ActivitySequence({
252
271
  }
253
272
  }
254
273
  reportedRef.current = true;
274
+ setReported(true);
255
275
  onFinished?.(items);
256
276
  const scored = [];
257
277
  for (const item of items) {
@@ -262,7 +282,7 @@ function ActivitySequence({
262
282
  }
263
283
  onComplete?.(scored);
264
284
  };
265
- const record = (outcome, generation, take) => {
285
+ const record = (outcome, generation, take, tried) => {
266
286
  const held = outcomesRef.current;
267
287
  if (held === null || generation !== setGenerationRef.current) {
268
288
  return;
@@ -270,7 +290,16 @@ function ActivitySequence({
270
290
  const at = outcome.index;
271
291
  const holding = held[at];
272
292
  let accepted = outcome;
273
- if (take === void 0) {
293
+ if (tried !== void 0) {
294
+ const latest = triedRef.current.get(at);
295
+ if (holding?.kind === "restored") {
296
+ return;
297
+ }
298
+ if (holding != null && (reportedRef.current || latest === void 0 || tried <= latest)) {
299
+ return;
300
+ }
301
+ triedRef.current.set(at, tried);
302
+ } else if (take === void 0) {
274
303
  if (holding != null) {
275
304
  return;
276
305
  }
@@ -344,8 +373,20 @@ function ActivitySequence({
344
373
  };
345
374
  const completeSlot = (generation, at, result) => {
346
375
  const takesAnswers = renderMode === "practice" && at.type === "read-aloud";
376
+ const outcome = {
377
+ kind: "scored",
378
+ index: at.index,
379
+ slotId: at.slotId,
380
+ activityId: at.activityId,
381
+ result
382
+ };
383
+ const tried = takesAnswers ? void 0 : takeOf(result);
384
+ if (tried !== void 0 && renderMode === "practice") {
385
+ record(outcome, generation, void 0, tried);
386
+ return;
387
+ }
347
388
  record(
348
- { kind: "scored", index: at.index, slotId: at.slotId, activityId: at.activityId, result },
389
+ outcome,
349
390
  generation,
350
391
  // The take the grade is for: stamped by `<ReadAloud>`, and for a
351
392
  // consumer's renderer, which cannot stamp one, the slot's latest.
@@ -401,8 +442,23 @@ function ActivitySequence({
401
442
  takesRef.current.set(at, latest?.take === take ? { ...latest, state } : { take, state });
402
443
  reportIfFinished();
403
444
  };
445
+ const updateTries = (generation, at, state) => {
446
+ if (!aliveRef.current || generation !== setGenerationRef.current) {
447
+ return;
448
+ }
449
+ const was = triesRef.current.get(at);
450
+ if (state === "none") {
451
+ triesRef.current.delete(at);
452
+ } else {
453
+ triesRef.current.set(at, state);
454
+ }
455
+ if (was !== void 0 && was !== state) {
456
+ reportIfFinished();
457
+ }
458
+ };
404
459
  latestRef.current = {
405
460
  updateTake,
461
+ updateTries,
406
462
  reportIfFinished,
407
463
  submitSlot,
408
464
  completeSlot,
@@ -474,6 +530,15 @@ function ActivitySequence({
474
530
  get delivery() {
475
531
  return deliveryRef.current;
476
532
  },
533
+ get scoring() {
534
+ return scoringRef.current;
535
+ },
536
+ get triesClosed() {
537
+ return reportedRef.current;
538
+ },
539
+ triesState: (state) => {
540
+ latestRef.current?.updateTries(generation, at, state);
541
+ },
477
542
  takeState: (take, state) => {
478
543
  latestRef.current?.updateTake(generation, at, take, state);
479
544
  }
@@ -569,6 +634,9 @@ function ActivitySequence({
569
634
  // As given: each question combines it with the paper's through its
570
635
  // channel, so what it is handed and what holds cannot disagree.
571
636
  ...delivery !== void 0 && delivery !== null ? { delivery } : {},
637
+ // As given, for a `renderers` override to score by. The SDK's own questions
638
+ // read the paper's from their channel, which wins over it.
639
+ ...scoring !== void 0 && scoring !== null ? { scoring } : {},
572
640
  ...theme ? { theme } : {},
573
641
  ...locale ? { locale } : {},
574
642
  ...disabled ? { disabled } : {}
@@ -752,5 +820,5 @@ function ActivitySequence({
752
820
  }
753
821
 
754
822
  export { ActivitySequence };
755
- //# sourceMappingURL=chunk-QKW2GGTV.js.map
756
- //# sourceMappingURL=chunk-QKW2GGTV.js.map
823
+ //# sourceMappingURL=chunk-U2BX2RWY.js.map
824
+ //# sourceMappingURL=chunk-U2BX2RWY.js.map