@intellectif/lk-react 23.1.2 → 23.2.1

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 (91) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/{ActivityErrorBoundary-Dw6SbWv3.js → ActivityErrorBoundary-BBPjMsqG.js} +2 -2
  3. package/dist/{ActivityErrorBoundary-Dw6SbWv3.js.map → ActivityErrorBoundary-BBPjMsqG.js.map} +1 -1
  4. package/dist/{ActivityErrorBoundary-B379hvtL.cjs → ActivityErrorBoundary-DEHmEXQw.cjs} +2 -2
  5. package/dist/{ActivityErrorBoundary-B379hvtL.cjs.map → ActivityErrorBoundary-DEHmEXQw.cjs.map} +1 -1
  6. package/dist/{ActivityPreview-1hhTkYuj.js → ActivityPreview-CiQ31WX8.js} +9 -9
  7. package/dist/{ActivityPreview-1hhTkYuj.js.map → ActivityPreview-CiQ31WX8.js.map} +1 -1
  8. package/dist/{ActivityPreview-BKr34exz.cjs → ActivityPreview-DWT4nSAN.cjs} +9 -9
  9. package/dist/{ActivityPreview-BKr34exz.cjs.map → ActivityPreview-DWT4nSAN.cjs.map} +1 -1
  10. package/dist/{ActivitySequence-CkX87pfB.js → ActivitySequence-D6bRMUWU.js} +13 -8
  11. package/dist/ActivitySequence-D6bRMUWU.js.map +1 -0
  12. package/dist/{ActivitySequence-CmoSyDnp.cjs → ActivitySequence-OlZpo7WW.cjs} +13 -8
  13. package/dist/ActivitySequence-OlZpo7WW.cjs.map +1 -0
  14. package/dist/{Dictation-jECzL88E.cjs → Dictation-Bq6Fn001.cjs} +5 -5
  15. package/dist/{Dictation-jECzL88E.cjs.map → Dictation-Bq6Fn001.cjs.map} +1 -1
  16. package/dist/{Dictation-BlxRp-3k.js → Dictation-KM4BboAS.js} +5 -5
  17. package/dist/{Dictation-BlxRp-3k.js.map → Dictation-KM4BboAS.js.map} +1 -1
  18. package/dist/{FeedbackRegion-CyPSp8bA.js → FeedbackRegion-DuVolWLX.js} +2 -2
  19. package/dist/{FeedbackRegion-CyPSp8bA.js.map → FeedbackRegion-DuVolWLX.js.map} +1 -1
  20. package/dist/{FeedbackRegion-BNNMtTY-.cjs → FeedbackRegion-wfTuezJo.cjs} +2 -2
  21. package/dist/{FeedbackRegion-BNNMtTY-.cjs.map → FeedbackRegion-wfTuezJo.cjs.map} +1 -1
  22. package/dist/{FillInTheBlanks-B6xzqYXx.js → FillInTheBlanks-CRA7MPyq.js} +5 -5
  23. package/dist/{FillInTheBlanks-B6xzqYXx.js.map → FillInTheBlanks-CRA7MPyq.js.map} +1 -1
  24. package/dist/{FillInTheBlanks-BpKhdHg2.cjs → FillInTheBlanks-Cr_QOukp.cjs} +5 -5
  25. package/dist/{FillInTheBlanks-BpKhdHg2.cjs.map → FillInTheBlanks-Cr_QOukp.cjs.map} +1 -1
  26. package/dist/{GapSelect-DUSiaEQH.js → GapSelect-CddiUhaR.js} +5 -5
  27. package/dist/{GapSelect-DUSiaEQH.js.map → GapSelect-CddiUhaR.js.map} +1 -1
  28. package/dist/{GapSelect-payTq_Zn.cjs → GapSelect-DlL8eggv.cjs} +5 -5
  29. package/dist/{GapSelect-payTq_Zn.cjs.map → GapSelect-DlL8eggv.cjs.map} +1 -1
  30. package/dist/{InteractiveVideo-Cg2jbU3j.js → InteractiveVideo-CZHhtRX1.js} +9 -8
  31. package/dist/{InteractiveVideo-Cg2jbU3j.js.map → InteractiveVideo-CZHhtRX1.js.map} +1 -1
  32. package/dist/{InteractiveVideo-BguGv-HA.cjs → InteractiveVideo-DJK6xago.cjs} +9 -8
  33. package/dist/{InteractiveVideo-BguGv-HA.cjs.map → InteractiveVideo-DJK6xago.cjs.map} +1 -1
  34. package/dist/{MultipleChoice-COn5MtKY.cjs → MultipleChoice-BJMAgxQd.cjs} +5 -5
  35. package/dist/{MultipleChoice-COn5MtKY.cjs.map → MultipleChoice-BJMAgxQd.cjs.map} +1 -1
  36. package/dist/{MultipleChoice-Dra0_KYD.js → MultipleChoice-PlJ_hL_E.js} +5 -5
  37. package/dist/{MultipleChoice-Dra0_KYD.js.map → MultipleChoice-PlJ_hL_E.js.map} +1 -1
  38. package/dist/{PronunciationFeedback-DlMPGjkR.cjs → PronunciationFeedback-B3iErah7.cjs} +3 -3
  39. package/dist/{PronunciationFeedback-DlMPGjkR.cjs.map → PronunciationFeedback-B3iErah7.cjs.map} +1 -1
  40. package/dist/{PronunciationFeedback-Dxd6XPkO.js → PronunciationFeedback-CGGgPA7i.js} +3 -3
  41. package/dist/{PronunciationFeedback-Dxd6XPkO.js.map → PronunciationFeedback-CGGgPA7i.js.map} +1 -1
  42. package/dist/{ReadAloud-C8RhaUqV.js → ReadAloud-NMSLzstD.js} +5 -5
  43. package/dist/{ReadAloud-C8RhaUqV.js.map → ReadAloud-NMSLzstD.js.map} +1 -1
  44. package/dist/{ReadAloud-akDehitT.cjs → ReadAloud-xbRcLkro.cjs} +5 -5
  45. package/dist/{ReadAloud-akDehitT.cjs.map → ReadAloud-xbRcLkro.cjs.map} +1 -1
  46. package/dist/{WrittenResponse-Bj3eFmGl.cjs → WrittenResponse-BOsB3DKv.cjs} +4 -4
  47. package/dist/{WrittenResponse-Bj3eFmGl.cjs.map → WrittenResponse-BOsB3DKv.cjs.map} +1 -1
  48. package/dist/{WrittenResponse-Cl8G6nO_.js → WrittenResponse-DU10_GFF.js} +4 -4
  49. package/dist/{WrittenResponse-Cl8G6nO_.js.map → WrittenResponse-DU10_GFF.js.map} +1 -1
  50. package/dist/ai/useAiHelp.cjs +1 -1
  51. package/dist/ai/useAiHelp.d.cts +9 -1
  52. package/dist/ai/useAiHelp.d.cts.map +1 -1
  53. package/dist/ai/useAiHelp.d.ts +9 -1
  54. package/dist/ai/useAiHelp.d.ts.map +1 -1
  55. package/dist/ai/useAiHelp.js +1 -1
  56. package/dist/components/ActivityPreview.cjs +1 -1
  57. package/dist/components/ActivityPreview.js +1 -1
  58. package/dist/components/ActivitySequence.cjs +1 -1
  59. package/dist/components/ActivitySequence.js +1 -1
  60. package/dist/components/Dictation.cjs +1 -1
  61. package/dist/components/Dictation.js +1 -1
  62. package/dist/components/FillInTheBlanks.cjs +1 -1
  63. package/dist/components/FillInTheBlanks.js +1 -1
  64. package/dist/components/GapSelect.cjs +1 -1
  65. package/dist/components/GapSelect.js +1 -1
  66. package/dist/components/InteractiveVideo.cjs +1 -1
  67. package/dist/components/InteractiveVideo.js +1 -1
  68. package/dist/components/MultipleChoice.cjs +1 -1
  69. package/dist/components/MultipleChoice.js +1 -1
  70. package/dist/components/PronunciationFeedback.cjs +1 -1
  71. package/dist/components/PronunciationFeedback.js +1 -1
  72. package/dist/components/ReadAloud.cjs +1 -1
  73. package/dist/components/ReadAloud.js +1 -1
  74. package/dist/components/WrittenResponse.cjs +1 -1
  75. package/dist/components/WrittenResponse.js +1 -1
  76. package/dist/index.cjs +11 -11
  77. package/dist/index.js +11 -11
  78. package/dist/theme/skin.css +19 -1
  79. package/dist/{tries-CbbtZkdG.cjs → tries-5JLYRKCa.cjs} +2 -2
  80. package/dist/{tries-CbbtZkdG.cjs.map → tries-5JLYRKCa.cjs.map} +1 -1
  81. package/dist/{tries-DTHSrLCm.js → tries-CTT1URXx.js} +2 -2
  82. package/dist/{tries-DTHSrLCm.js.map → tries-CTT1URXx.js.map} +1 -1
  83. package/dist/{useAiHelp-B-ccDL5Q.js → useAiHelp-BJy05hqV.js} +18 -1
  84. package/dist/useAiHelp-BJy05hqV.js.map +1 -0
  85. package/dist/{useAiHelp-h9yOLFqt.cjs → useAiHelp-MBQiNgst.cjs} +18 -1
  86. package/dist/useAiHelp-MBQiNgst.cjs.map +1 -0
  87. package/package.json +11 -11
  88. package/dist/ActivitySequence-CkX87pfB.js.map +0 -1
  89. package/dist/ActivitySequence-CmoSyDnp.cjs.map +0 -1
  90. package/dist/useAiHelp-B-ccDL5Q.js.map +0 -1
  91. package/dist/useAiHelp-h9yOLFqt.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
1
  'use client';
2
2
  "use client";
3
- import { t as MultipleChoice } from "../MultipleChoice-Dra0_KYD.js";
3
+ import { t as MultipleChoice } from "../MultipleChoice-PlJ_hL_E.js";
4
4
  export { MultipleChoice };
@@ -1,5 +1,5 @@
1
1
  'use client';
2
2
  "use client";
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_PronunciationFeedback = require("../PronunciationFeedback-DlMPGjkR.cjs");
4
+ const require_PronunciationFeedback = require("../PronunciationFeedback-B3iErah7.cjs");
5
5
  exports.PronunciationFeedback = require_PronunciationFeedback.PronunciationFeedback;
@@ -1,4 +1,4 @@
1
1
  'use client';
2
2
  "use client";
3
- import { t as PronunciationFeedback } from "../PronunciationFeedback-Dxd6XPkO.js";
3
+ import { t as PronunciationFeedback } from "../PronunciationFeedback-CGGgPA7i.js";
4
4
  export { PronunciationFeedback };
@@ -1,5 +1,5 @@
1
1
  'use client';
2
2
  "use client";
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_ReadAloud = require("../ReadAloud-akDehitT.cjs");
4
+ const require_ReadAloud = require("../ReadAloud-xbRcLkro.cjs");
5
5
  exports.ReadAloud = require_ReadAloud.ReadAloud;
@@ -1,4 +1,4 @@
1
1
  'use client';
2
2
  "use client";
3
- import { t as ReadAloud } from "../ReadAloud-C8RhaUqV.js";
3
+ import { t as ReadAloud } from "../ReadAloud-NMSLzstD.js";
4
4
  export { ReadAloud };
@@ -1,5 +1,5 @@
1
1
  'use client';
2
2
  "use client";
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_WrittenResponse = require("../WrittenResponse-Bj3eFmGl.cjs");
4
+ const require_WrittenResponse = require("../WrittenResponse-BOsB3DKv.cjs");
5
5
  exports.WrittenResponse = require_WrittenResponse.WrittenResponse;
@@ -1,4 +1,4 @@
1
1
  'use client';
2
2
  "use client";
3
- import { t as WrittenResponse } from "../WrittenResponse-Cl8G6nO_.js";
3
+ import { t as WrittenResponse } from "../WrittenResponse-DU10_GFF.js";
4
4
  export { WrittenResponse };
package/dist/index.cjs CHANGED
@@ -1,21 +1,21 @@
1
1
  'use client';
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_ai_LkAiProvider = require("./ai/LkAiProvider.cjs");
4
- const require_useAiHelp = require("./useAiHelp-h9yOLFqt.cjs");
4
+ const require_useAiHelp = require("./useAiHelp-MBQiNgst.cjs");
5
5
  const require_LkIntlProvider = require("./LkIntlProvider-oi57lYB0.cjs");
6
6
  const require_hooks_useActivityState = require("./hooks/useActivityState.cjs");
7
- const require_Dictation = require("./Dictation-jECzL88E.cjs");
8
- const require_FillInTheBlanks = require("./FillInTheBlanks-BpKhdHg2.cjs");
9
- const require_GapSelect = require("./GapSelect-payTq_Zn.cjs");
10
- const require_MultipleChoice = require("./MultipleChoice-COn5MtKY.cjs");
7
+ const require_Dictation = require("./Dictation-Bq6Fn001.cjs");
8
+ const require_FillInTheBlanks = require("./FillInTheBlanks-Cr_QOukp.cjs");
9
+ const require_GapSelect = require("./GapSelect-DlL8eggv.cjs");
10
+ const require_MultipleChoice = require("./MultipleChoice-BJMAgxQd.cjs");
11
11
  const require_useSpeechRecorder = require("./useSpeechRecorder--O_f-xvt.cjs");
12
- const require_PronunciationFeedback = require("./PronunciationFeedback-DlMPGjkR.cjs");
13
- const require_ReadAloud = require("./ReadAloud-akDehitT.cjs");
14
- const require_WrittenResponse = require("./WrittenResponse-Bj3eFmGl.cjs");
15
- const require_ActivityPreview = require("./ActivityPreview-BKr34exz.cjs");
12
+ const require_PronunciationFeedback = require("./PronunciationFeedback-B3iErah7.cjs");
13
+ const require_ReadAloud = require("./ReadAloud-xbRcLkro.cjs");
14
+ const require_WrittenResponse = require("./WrittenResponse-BOsB3DKv.cjs");
15
+ const require_ActivityPreview = require("./ActivityPreview-DWT4nSAN.cjs");
16
16
  const require_StimulusPanel = require("./StimulusPanel-b66gaN3i.cjs");
17
- const require_ActivitySequence = require("./ActivitySequence-CmoSyDnp.cjs");
18
- const require_InteractiveVideo = require("./InteractiveVideo-BguGv-HA.cjs");
17
+ const require_ActivitySequence = require("./ActivitySequence-OlZpo7WW.cjs");
18
+ const require_InteractiveVideo = require("./InteractiveVideo-DJK6xago.cjs");
19
19
  const require_hooks_useXAPI = require("./hooks/useXAPI.cjs");
20
20
  const require_ThemeProvider = require("./ThemeProvider-BRN1tj4n.cjs");
21
21
  //#region src/components/types.ts
package/dist/index.js CHANGED
@@ -1,20 +1,20 @@
1
1
  'use client';
2
2
  import { LkAiProvider, useLearnerAi } from "./ai/LkAiProvider.js";
3
- import { i as useAiWritingFeedback, n as useAiExplanation, r as useAiHints, t as useAiCoaching } from "./useAiHelp-B-ccDL5Q.js";
3
+ import { i as useAiWritingFeedback, n as useAiExplanation, r as useAiHints, t as useAiCoaching } from "./useAiHelp-BJy05hqV.js";
4
4
  import { a as DEFAULT_STRINGS, i as useLkStrings, n as directionForLocale, o as mergeStrings, r as useLkDirection, t as LkIntlProvider } from "./LkIntlProvider-D_FdYXjQ.js";
5
5
  import { useActivityState } from "./hooks/useActivityState.js";
6
- import { t as Dictation } from "./Dictation-BlxRp-3k.js";
7
- import { t as FillInTheBlanks } from "./FillInTheBlanks-B6xzqYXx.js";
8
- import { t as GapSelect } from "./GapSelect-DUSiaEQH.js";
9
- import { t as MultipleChoice } from "./MultipleChoice-Dra0_KYD.js";
6
+ import { t as Dictation } from "./Dictation-KM4BboAS.js";
7
+ import { t as FillInTheBlanks } from "./FillInTheBlanks-CRA7MPyq.js";
8
+ import { t as GapSelect } from "./GapSelect-CddiUhaR.js";
9
+ import { t as MultipleChoice } from "./MultipleChoice-PlJ_hL_E.js";
10
10
  import { n as CAPTURE_PROCESSOR_SOURCE, t as useSpeechRecorder } from "./useSpeechRecorder-zIi9Dlw8.js";
11
- import { t as PronunciationFeedback } from "./PronunciationFeedback-Dxd6XPkO.js";
12
- import { t as ReadAloud } from "./ReadAloud-C8RhaUqV.js";
13
- import { t as WrittenResponse } from "./WrittenResponse-Cl8G6nO_.js";
14
- import { t as ActivityPreview } from "./ActivityPreview-1hhTkYuj.js";
11
+ import { t as PronunciationFeedback } from "./PronunciationFeedback-CGGgPA7i.js";
12
+ import { t as ReadAloud } from "./ReadAloud-NMSLzstD.js";
13
+ import { t as WrittenResponse } from "./WrittenResponse-DU10_GFF.js";
14
+ import { t as ActivityPreview } from "./ActivityPreview-CiQ31WX8.js";
15
15
  import { t as StimulusPanel } from "./StimulusPanel-BgzByfkb.js";
16
- import { t as ActivitySequence } from "./ActivitySequence-CkX87pfB.js";
17
- import { n as resolveCaptionTracks, t as InteractiveVideo } from "./InteractiveVideo-Cg2jbU3j.js";
16
+ import { t as ActivitySequence } from "./ActivitySequence-D6bRMUWU.js";
17
+ import { n as resolveCaptionTracks, t as InteractiveVideo } from "./InteractiveVideo-CZHhtRX1.js";
18
18
  import { useXAPI } from "./hooks/useXAPI.js";
19
19
  import { a as darkTheme, i as defaultTheme, n as useTheme, r as createTailwindTheme, t as ThemeProvider } from "./ThemeProvider-C7K9iMLX.js";
20
20
  //#region src/components/types.ts
@@ -49,7 +49,11 @@
49
49
  margin: 0 0 var(--lk-spacing-lg);
50
50
  }
51
51
 
52
+ /* Border-box of its own: a written response, a gap select and a stimulus
53
+ panel do not make what they hold border-box, and the border would sit
54
+ outside the 100% and past the frame's edge. */
52
55
  .lk-media-el {
56
+ box-sizing: border-box;
53
57
  display: block;
54
58
  max-width: 100%;
55
59
  height: auto;
@@ -111,11 +115,14 @@
111
115
  cursor: default;
112
116
  }
113
117
 
114
- /* ── Shared stimulus (an item group's passage / recording / image) ──── */
118
+ /* ── Shared stimulus (an item group's passage / recording / image) ────
119
+ It paints its own surface, so it sets its own text: the pager around it
120
+ has no card, and the passage would take the host page's colour. */
115
121
  .lk-stimulus {
116
122
  margin: 0 0 var(--lk-spacing-lg);
117
123
  padding: var(--lk-spacing-md) var(--lk-spacing-lg);
118
124
  background: var(--lk-color-surface-raised);
125
+ color: var(--lk-color-text);
119
126
  border: 1px solid var(--lk-color-border);
120
127
  border-radius: var(--lk-radius-base);
121
128
  }
@@ -578,7 +585,10 @@
578
585
  line-height: 1.6;
579
586
  }
580
587
 
588
+ /* `.lk-wr` makes nothing it holds border-box, so the answer box does: the
589
+ padding and border then sit inside the 100%, not past the form's edge. */
581
590
  .lk-wr-textarea {
591
+ box-sizing: border-box;
582
592
  width: 100%;
583
593
  padding: var(--lk-spacing-sm) var(--lk-spacing-md);
584
594
  border: 1px solid var(--lk-color-border);
@@ -659,6 +669,7 @@
659
669
  border: 1px solid var(--lk-color-border);
660
670
  border-radius: var(--lk-radius-base);
661
671
  background: var(--lk-color-surface);
672
+ color: var(--lk-color-text);
662
673
  }
663
674
 
664
675
  .lk-wr-outcome[data-passed="true"] {
@@ -735,6 +746,7 @@
735
746
  margin: 0 0 var(--lk-spacing-md);
736
747
  padding: var(--lk-spacing-sm) var(--lk-spacing-md);
737
748
  background: var(--lk-color-surface-raised);
749
+ color: var(--lk-color-text);
738
750
  border: 1px solid var(--lk-color-border);
739
751
  border-radius: var(--lk-radius-base);
740
752
  }
@@ -3382,12 +3394,17 @@
3382
3394
  cursor: progress;
3383
3395
  }
3384
3396
 
3397
+ /* A panel or a hint paints its own surface, so it sets the text on it as
3398
+ well. A component with no card of its own, a written response or a gap
3399
+ select, would otherwise draw the host page's text colour on the theme's
3400
+ surface: black on the dark surface when the page stays light. */
3385
3401
  .lk-ai-panel {
3386
3402
  align-self: stretch;
3387
3403
  padding: var(--lk-spacing-sm) var(--lk-spacing-md);
3388
3404
  border: 1px dashed var(--lk-color-border);
3389
3405
  border-radius: var(--lk-radius-base);
3390
3406
  background: var(--lk-color-surface);
3407
+ color: var(--lk-color-text);
3391
3408
  }
3392
3409
 
3393
3410
  .lk-ai-heading {
@@ -3423,6 +3440,7 @@
3423
3440
  padding: var(--lk-spacing-xs) var(--lk-spacing-sm);
3424
3441
  border-inline-start: 3px dashed var(--lk-color-primary);
3425
3442
  background: var(--lk-color-surface);
3443
+ color: var(--lk-color-text);
3426
3444
  }
3427
3445
 
3428
3446
  .lk-ai-hint-label {
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- const require_useAiHelp = require("./useAiHelp-h9yOLFqt.cjs");
2
+ const require_useAiHelp = require("./useAiHelp-MBQiNgst.cjs");
3
3
  let react = require("react");
4
4
  let react_jsx_runtime = require("react/jsx-runtime");
5
5
  let _intellectif_lk_core = require("@intellectif/lk-core");
@@ -360,4 +360,4 @@ Object.defineProperty(exports, "useTries", {
360
360
  }
361
361
  });
362
362
 
363
- //# sourceMappingURL=tries-CbbtZkdG.cjs.map
363
+ //# sourceMappingURL=tries-5JLYRKCa.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tries-CbbtZkdG.cjs","names":["useAiExplanation","useRef","useAiHints","useContext","SequenceSlotContext","useMemo","resolveItemScoringPolicy","useState","useRef","useCallback","scoreTries"],"sources":["../src/components/shared/AiHelp.tsx","../src/components/shared/tries.tsx"],"sourcesContent":["'use client';\n\nimport type {\n InteractionEvent,\n ItemOutcome,\n LearnerResponse,\n ResolvedDeliveryPolicy,\n} from '@intellectif/lk-core';\nimport { useEffect, useRef } from 'react';\nimport type { LearnerAi } from '../../ai/LkAiProvider.js';\nimport { type AiHintsHelp, useAiExplanation, useAiHints } from '../../ai/useAiHelp.js';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport type { RenderableActivity, RenderMode } from '../types.js';\n\ninterface Situation {\n ai: LearnerAi | undefined;\n data: RenderableActivity;\n renderMode: RenderMode;\n submitted: boolean;\n /** The learner's answer as it stands: submitted, restored, or being written. */\n response: LearnerResponse;\n /** The component's `locale`, the default language for what is written. */\n locale: string | undefined;\n onInteraction: ((event: InteractionEvent) => void) | undefined;\n strings: LkStrings;\n /** The policy the component resolved, the paper's included. */\n delivery: ResolvedDeliveryPolicy;\n}\n\n/** What the hooks take, from what a component holds: the optional half, without `undefined` values. */\nfunction situation(input: Situation): {\n data: RenderableActivity;\n response: LearnerResponse;\n submitted: boolean;\n renderMode: RenderMode;\n ai?: LearnerAi;\n locale?: string;\n onInteraction?: (event: InteractionEvent) => void;\n delivery: ResolvedDeliveryPolicy;\n} {\n const { data, response, submitted, renderMode, ai, locale, onInteraction, delivery } = input;\n return {\n data,\n response,\n submitted,\n renderMode,\n delivery,\n ...(ai !== undefined ? { ai } : {}),\n ...(locale !== undefined ? { locale } : {}),\n ...(onInteraction !== undefined ? { onInteraction } : {}),\n };\n}\n\n/**\n * \"Explain my answer\": the SDK's own surface over {@link useAiExplanation} — a\n * button, the explanation with who wrote it, and \"not available\" when a call\n * fails or its answer is refused.\n *\n * Nothing renders without a port, or where the rules say no explanation.\n */\nexport function AiExplanation({\n outcome,\n ...rest\n}: Situation & { outcome: ItemOutcome | undefined }) {\n const s = rest.strings;\n const help = useAiExplanation({\n ...situation(rest),\n ...(outcome !== undefined ? { outcome } : {}),\n });\n const buttonRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLElement>(null);\n const refocus = useRef(false);\n\n // The button that asked goes away when the explanation arrives; focus moves\n // to what it asked for rather than falling to the page.\n useEffect(() => {\n if (help.status === 'shown' && refocus.current) {\n refocus.current = false;\n panelRef.current?.focus();\n }\n if (help.status === 'unavailable') {\n refocus.current = false;\n }\n }, [help.status]);\n\n if (!help.offered) {\n return null;\n }\n\n const ask = (): void => {\n refocus.current =\n typeof document !== 'undefined' && document.activeElement === buttonRef.current;\n help.ask();\n };\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai\" data-state={help.status}>\n {help.explanation === null ? (\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={ask}\n >\n {loading ? s.aiExplaining : s.aiExplain}\n </button>\n ) : null}\n <div aria-live=\"polite\">\n {help.explanation !== null ? (\n <section\n ref={panelRef}\n className=\"lk-ai-panel\"\n tabIndex={-1}\n aria-label={s.aiExplanation}\n >\n <p className=\"lk-ai-heading\">{s.aiExplanation}</p>\n <p className=\"lk-ai-text\">{help.explanation.text}</p>\n <p className=\"lk-ai-notice\">{s.aiNotice}</p>\n </section>\n ) : help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiExplanationUnavailable}</p>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The AI hints of one of the SDK's own questions: {@link useAiHints}, called\n * by the component itself so it can count the hints its learner was shown.\n */\nexport function useComponentAiHints({\n disabled,\n ...rest\n}: Situation & { disabled: boolean }): AiHintsHelp {\n return useAiHints({ ...situation(rest), disabled });\n}\n\n/**\n * \"Get a hint\": the SDK's own surface over {@link useAiHints} — a button, the\n * hints given so far as a numbered list, and who wrote them.\n *\n * The hints stay listed after submit, so the learner can see what they were\n * given, but no more can be asked for — until a \"Try again\" reopens the\n * answer, when the same list carries on. Another question starts with none.\n */\nexport function AiHints({\n help,\n ai,\n strings: s,\n}: {\n help: AiHintsHelp;\n ai: LearnerAi | undefined;\n strings: LkStrings;\n}) {\n const used = help.used;\n\n if (ai?.hint === undefined || (!help.offered && used === 0)) {\n return null;\n }\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai lk-ai-hint-area\" data-state={help.status}>\n <div aria-live=\"polite\">\n {used > 0 ? (\n <ol className=\"lk-ai-hints\" aria-label={s.aiHints}>\n {help.hints.map((hint, index) => (\n // Hints are only ever appended, so a position is a stable key.\n // biome-ignore lint/suspicious/noArrayIndexKey: an append-only list\n <li key={index} className=\"lk-ai-hint\">\n <span className=\"lk-ai-hint-label\">{s.aiHintNumber(index + 1)}</span>{' '}\n <span className=\"lk-ai-text\">{hint.text}</span>\n </li>\n ))}\n </ol>\n ) : null}\n {help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiHintUnavailable}</p>\n ) : null}\n </div>\n {help.offered ? (\n used < help.limit ? (\n <button\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={help.ask}\n >\n {loading ? s.aiHintLoading : s.aiHint}\n </button>\n ) : (\n <p className=\"lk-ai-note\">{s.aiNoMoreHints}</p>\n )\n ) : null}\n {used > 0 ? <p className=\"lk-ai-notice\">{s.aiNotice}</p> : null}\n </div>\n );\n}\n","'use client';\n\nimport {\n type ItemScoringPolicy,\n type ItemTriesScore,\n type ItemTry,\n type ResolvedItemScoringPolicy,\n resolveItemScoringPolicy,\n scoreTries,\n} from '@intellectif/lk-core';\nimport { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport { SequenceSlotContext, type TriesState } from './sequence-slot.js';\n\n/**\n * The scoring policy a question is under: the paper's, when the paper around\n * it was given one, else its own.\n *\n * Not combined, as delivery policies are: a cost and a count of tries have no\n * \"stricter\" to pick. The paper decides, as it decides the mode — so a\n * question drawn inside a set is scored by the set's rules whatever it was\n * handed.\n *\n * A policy `validateItemScoringPolicy` refuses throws here, at render. Every\n * setting in it moves a grade, and there is no safe way to read one wrongly:\n * the question fails loudly rather than grade by a guess.\n */\nexport function useItemScoringPolicy(\n given: ItemScoringPolicy | null | undefined,\n): ResolvedItemScoringPolicy {\n const around = useContext(SequenceSlotContext)?.scoring;\n const key = policyKey(given);\n // biome-ignore lint/correctness/useExhaustiveDependencies: the policy's content is the trigger, not its identity\n const own = useMemo(\n () => (around === undefined ? resolveItemScoringPolicy(given) : null),\n [around, key],\n );\n return around ?? (own as ResolvedItemScoringPolicy);\n}\n\nfunction policyKey(given: unknown): string {\n try {\n return JSON.stringify(given) ?? '';\n } catch {\n return String(given);\n }\n}\n\n/** A hint count as a response carries it: anything but a whole number above 0 is none. */\nexport function hintsOf(response: unknown): number {\n const count = (response as { hintsRevealed?: unknown } | null | undefined)?.hintsRevealed;\n return typeof count === 'number' && Number.isSafeInteger(count) && count > 0 ? count : 0;\n}\n\n/** Whether a try earned every mark there was: float noise above the maximum included. */\nconst fullMarks = (one: ItemTry): boolean => one.score / one.maxScore >= 1 - 1e-9;\n\n/** Whether, after the tries `made`, a question offers another: the rule `open` reads. */\nfunction offersAfter(\n now: {\n policy: ResolvedItemScoringPolicy;\n graded: boolean;\n disabled: boolean;\n channel: { triesClosed: boolean } | null;\n },\n made: readonly ItemTry[],\n shut: boolean,\n): boolean {\n const last = made[made.length - 1];\n return (\n now.graded &&\n !now.disabled &&\n !shut &&\n now.channel?.triesClosed !== true &&\n last !== undefined &&\n made.length < 1 + now.policy.retries &&\n !fullMarks(last)\n );\n}\n\nexport interface TriesInput {\n policy: ResolvedItemScoringPolicy;\n /**\n * Whether the component grades this answer itself and the learner may see\n * how it went: `practice`, with `feedback` on. \"Try again\" anywhere else\n * would be meaningless, or would say an answer was wrong on a paper that\n * does not say so.\n */\n graded: boolean;\n submitted: boolean;\n disabled: boolean;\n}\n\nexport interface Tries {\n /** Every graded try so far, in order. */\n tries: readonly ItemTry[];\n /** The question's score under the policy, once a try has been graded. */\n counted: ItemTriesScore | null;\n /**\n * Whether the learner is being offered another try at a graded answer: the\n * last try earned less than full marks, the policy has tries left, and\n * nothing has closed the question. While it is, the question shows no right\n * answer and no explanation — either would hand over the answer the next try\n * is for.\n */\n open: boolean;\n /** Tries still to come after the last graded one. */\n left: number;\n /** Records a graded try and returns the question's score under the policy. */\n record: (graded: ItemTry) => ItemTriesScore;\n /** Ends the question's tries: \"Show answer\". */\n close: () => void;\n /** Forgets every try: a different question. */\n reset: () => void;\n}\n\n/**\n * The tries a question has had and whether it offers another: the SDK's own\n * components' rules for \"Try again\", in one place.\n *\n * Reports whether a try is on offer to the paper around the question, which\n * waits for it before reporting the set while the question is on screen — and\n * closes every question's tries once it has reported.\n */\nexport function useTries({ policy, graded, submitted, disabled }: TriesInput): Tries {\n const channel = useContext(SequenceSlotContext);\n const [tries, setTries] = useState<readonly ItemTry[]>([]);\n const [closed, setClosed] = useState(false);\n // Written synchronously, so a submit that records a try and reports it reads\n // every try before it, whatever React has rendered.\n const triesRef = useRef<readonly ItemTry[]>([]);\n const closedRef = useRef(false);\n const latest = useRef({ policy, graded, disabled, channel });\n latest.current = { policy, graded, disabled, channel };\n\n const record = useCallback((one: ItemTry): ItemTriesScore => {\n const next = [...triesRef.current, one];\n triesRef.current = next;\n setTries(next);\n // An offer of another try is reported before the grade is handed on, not\n // after the next render: the paper reports its set when a grade fills its\n // last slot, and must already know to wait. The end of the tries is not:\n // said now, it would let the paper report the grade this try is replacing.\n // The effect below says it once this grade has landed.\n if (offersAfter(latest.current, next, closedRef.current)) {\n latest.current.channel?.triesState('offered');\n }\n return scoreTries(next, latest.current.policy);\n }, []);\n const close = useCallback(() => {\n closedRef.current = true;\n setClosed(true);\n }, []);\n const reset = useCallback(() => {\n triesRef.current = [];\n closedRef.current = false;\n setTries([]);\n setClosed(false);\n }, []);\n\n const allowed = 1 + policy.retries;\n const left = Math.max(0, allowed - tries.length);\n const open = submitted && offersAfter(latest.current, tries, closed);\n // A try the learner is making now — after \"Try again\", before the answer is\n // in again. Reporting the set then would hand on the grade the learner is in\n // the middle of replacing.\n const retrying =\n !submitted &&\n tries.length > 0 &&\n !closed &&\n graded &&\n !disabled &&\n channel?.triesClosed !== true;\n const state: TriesState = open ? 'offered' : retrying ? 'retrying' : 'none';\n\n const report = channel?.triesState;\n useEffect(() => {\n report?.(state);\n }, [report, state]);\n // A question that goes away offers nothing: the paper must not wait for it.\n useEffect(() => () => report?.('none'), [report]);\n\n const counted = useMemo(\n () => (tries.length === 0 ? null : scoreTries(tries, policy)),\n [tries, policy],\n );\n return { tries, counted, open, left, record, close, reset };\n}\n\n/** A fraction of the marks as the whole percentage a learner reads. */\nexport const percentOf = (fraction: number): number => Math.round(fraction * 100);\n\n/**\n * The sentences after the score, on a question whose policy changed it: which\n * try counts when it is not the one just made, what this answer scored before\n * its costs, and how many tries are left.\n */\nexport function triesSummary(\n s: LkStrings,\n policy: ResolvedItemScoringPolicy,\n counted: ItemTriesScore,\n offered: boolean,\n): string {\n const parts: string[] = [];\n const latest = counted.tries.length - 1;\n const mine = counted.tries[latest];\n if (mine !== undefined) {\n if (counted.counted !== latest) {\n parts.push(\n s.countedTry(\n policy.counts === 'best' ? 'best' : 'first',\n percentOf(mine.scored / mine.maxScore),\n ),\n );\n } else if (mine.scored !== mine.score) {\n parts.push(s.scoreBeforeCosts(percentOf(mine.score / mine.maxScore)));\n }\n }\n if (offered) {\n parts.push(\n s.triesLeft(policy.retries + 1 - counted.tries.length, percentOf(policy.retryPenalty)),\n );\n }\n return parts.join(' ');\n}\n\n/**\n * \"Try again\" and \"Show answer\" — or \"Keep this answer\", on a paper that shows\n * no right answers — after a graded try that did not earn full marks.\n */\nexport function TryActions({\n tries,\n solutions,\n strings: s,\n onRetry,\n onClose,\n}: {\n tries: Tries;\n /** Whether closing the question will show the right answer. */\n solutions: boolean;\n strings: LkStrings;\n onRetry: () => void;\n onClose: () => void;\n}): React.JSX.Element | null {\n if (!tries.open) {\n return null;\n }\n return (\n <div className=\"lk-tries\">\n <button type=\"button\" className=\"lk-tries-again\" onClick={onRetry}>\n {s.tryAgain}\n </button>\n <button type=\"button\" className=\"lk-tries-close\" onClick={onClose}>\n {solutions ? s.showAnswer : s.keepAnswer}\n </button>\n </div>\n );\n}\n\n/** What a hint costs, said before the learner asks for one. */\nexport function HintCost({\n policy,\n strings: s,\n}: {\n policy: ResolvedItemScoringPolicy;\n strings: LkStrings;\n}): React.JSX.Element | null {\n if (policy.hintPenalty <= 0) {\n return null;\n }\n return <p className=\"lk-hint-cost\">{s.hintCost(percentOf(policy.hintPenalty))}</p>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tries-5JLYRKCa.cjs","names":["useAiExplanation","useRef","useAiHints","useContext","SequenceSlotContext","useMemo","resolveItemScoringPolicy","useState","useRef","useCallback","scoreTries"],"sources":["../src/components/shared/AiHelp.tsx","../src/components/shared/tries.tsx"],"sourcesContent":["'use client';\n\nimport type {\n InteractionEvent,\n ItemOutcome,\n LearnerResponse,\n ResolvedDeliveryPolicy,\n} from '@intellectif/lk-core';\nimport { useEffect, useRef } from 'react';\nimport type { LearnerAi } from '../../ai/LkAiProvider.js';\nimport { type AiHintsHelp, useAiExplanation, useAiHints } from '../../ai/useAiHelp.js';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport type { RenderableActivity, RenderMode } from '../types.js';\n\ninterface Situation {\n ai: LearnerAi | undefined;\n data: RenderableActivity;\n renderMode: RenderMode;\n submitted: boolean;\n /** The learner's answer as it stands: submitted, restored, or being written. */\n response: LearnerResponse;\n /** The component's `locale`, the default language for what is written. */\n locale: string | undefined;\n onInteraction: ((event: InteractionEvent) => void) | undefined;\n strings: LkStrings;\n /** The policy the component resolved, the paper's included. */\n delivery: ResolvedDeliveryPolicy;\n}\n\n/** What the hooks take, from what a component holds: the optional half, without `undefined` values. */\nfunction situation(input: Situation): {\n data: RenderableActivity;\n response: LearnerResponse;\n submitted: boolean;\n renderMode: RenderMode;\n ai?: LearnerAi;\n locale?: string;\n onInteraction?: (event: InteractionEvent) => void;\n delivery: ResolvedDeliveryPolicy;\n} {\n const { data, response, submitted, renderMode, ai, locale, onInteraction, delivery } = input;\n return {\n data,\n response,\n submitted,\n renderMode,\n delivery,\n ...(ai !== undefined ? { ai } : {}),\n ...(locale !== undefined ? { locale } : {}),\n ...(onInteraction !== undefined ? { onInteraction } : {}),\n };\n}\n\n/**\n * \"Explain my answer\": the SDK's own surface over {@link useAiExplanation} — a\n * button, the explanation with who wrote it, and \"not available\" when a call\n * fails or its answer is refused.\n *\n * Nothing renders without a port, or where the rules say no explanation.\n */\nexport function AiExplanation({\n outcome,\n ...rest\n}: Situation & { outcome: ItemOutcome | undefined }) {\n const s = rest.strings;\n const help = useAiExplanation({\n ...situation(rest),\n ...(outcome !== undefined ? { outcome } : {}),\n });\n const buttonRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLElement>(null);\n const refocus = useRef(false);\n\n // The button that asked goes away when the explanation arrives; focus moves\n // to what it asked for rather than falling to the page.\n useEffect(() => {\n if (help.status === 'shown' && refocus.current) {\n refocus.current = false;\n panelRef.current?.focus();\n }\n if (help.status === 'unavailable') {\n refocus.current = false;\n }\n }, [help.status]);\n\n if (!help.offered) {\n return null;\n }\n\n const ask = (): void => {\n refocus.current =\n typeof document !== 'undefined' && document.activeElement === buttonRef.current;\n help.ask();\n };\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai\" data-state={help.status}>\n {help.explanation === null ? (\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={ask}\n >\n {loading ? s.aiExplaining : s.aiExplain}\n </button>\n ) : null}\n <div aria-live=\"polite\">\n {help.explanation !== null ? (\n <section\n ref={panelRef}\n className=\"lk-ai-panel\"\n tabIndex={-1}\n aria-label={s.aiExplanation}\n >\n <p className=\"lk-ai-heading\">{s.aiExplanation}</p>\n <p className=\"lk-ai-text\">{help.explanation.text}</p>\n <p className=\"lk-ai-notice\">{s.aiNotice}</p>\n </section>\n ) : help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiExplanationUnavailable}</p>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The AI hints of one of the SDK's own questions: {@link useAiHints}, called\n * by the component itself so it can count the hints its learner was shown.\n */\nexport function useComponentAiHints({\n disabled,\n ...rest\n}: Situation & { disabled: boolean }): AiHintsHelp {\n return useAiHints({ ...situation(rest), disabled });\n}\n\n/**\n * \"Get a hint\": the SDK's own surface over {@link useAiHints} — a button, the\n * hints given so far as a numbered list, and who wrote them.\n *\n * The hints stay listed after submit, so the learner can see what they were\n * given, but no more can be asked for — until a \"Try again\" reopens the\n * answer, when the same list carries on. Another question starts with none.\n */\nexport function AiHints({\n help,\n ai,\n strings: s,\n}: {\n help: AiHintsHelp;\n ai: LearnerAi | undefined;\n strings: LkStrings;\n}) {\n const used = help.used;\n\n if (ai?.hint === undefined || (!help.offered && used === 0)) {\n return null;\n }\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai lk-ai-hint-area\" data-state={help.status}>\n <div aria-live=\"polite\">\n {used > 0 ? (\n <ol className=\"lk-ai-hints\" aria-label={s.aiHints}>\n {help.hints.map((hint, index) => (\n // Hints are only ever appended, so a position is a stable key.\n // biome-ignore lint/suspicious/noArrayIndexKey: an append-only list\n <li key={index} className=\"lk-ai-hint\">\n <span className=\"lk-ai-hint-label\">{s.aiHintNumber(index + 1)}</span>{' '}\n <span className=\"lk-ai-text\">{hint.text}</span>\n </li>\n ))}\n </ol>\n ) : null}\n {help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiHintUnavailable}</p>\n ) : null}\n </div>\n {help.offered ? (\n used < help.limit ? (\n <button\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={help.ask}\n >\n {loading ? s.aiHintLoading : s.aiHint}\n </button>\n ) : (\n <p className=\"lk-ai-note\">{s.aiNoMoreHints}</p>\n )\n ) : null}\n {used > 0 ? <p className=\"lk-ai-notice\">{s.aiNotice}</p> : null}\n </div>\n );\n}\n","'use client';\n\nimport {\n type ItemScoringPolicy,\n type ItemTriesScore,\n type ItemTry,\n type ResolvedItemScoringPolicy,\n resolveItemScoringPolicy,\n scoreTries,\n} from '@intellectif/lk-core';\nimport { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport { SequenceSlotContext, type TriesState } from './sequence-slot.js';\n\n/**\n * The scoring policy a question is under: the paper's, when the paper around\n * it was given one, else its own.\n *\n * Not combined, as delivery policies are: a cost and a count of tries have no\n * \"stricter\" to pick. The paper decides, as it decides the mode — so a\n * question drawn inside a set is scored by the set's rules whatever it was\n * handed.\n *\n * A policy `validateItemScoringPolicy` refuses throws here, at render. Every\n * setting in it moves a grade, and there is no safe way to read one wrongly:\n * the question fails loudly rather than grade by a guess.\n */\nexport function useItemScoringPolicy(\n given: ItemScoringPolicy | null | undefined,\n): ResolvedItemScoringPolicy {\n const around = useContext(SequenceSlotContext)?.scoring;\n const key = policyKey(given);\n // biome-ignore lint/correctness/useExhaustiveDependencies: the policy's content is the trigger, not its identity\n const own = useMemo(\n () => (around === undefined ? resolveItemScoringPolicy(given) : null),\n [around, key],\n );\n return around ?? (own as ResolvedItemScoringPolicy);\n}\n\nfunction policyKey(given: unknown): string {\n try {\n return JSON.stringify(given) ?? '';\n } catch {\n return String(given);\n }\n}\n\n/** A hint count as a response carries it: anything but a whole number above 0 is none. */\nexport function hintsOf(response: unknown): number {\n const count = (response as { hintsRevealed?: unknown } | null | undefined)?.hintsRevealed;\n return typeof count === 'number' && Number.isSafeInteger(count) && count > 0 ? count : 0;\n}\n\n/** Whether a try earned every mark there was: float noise above the maximum included. */\nconst fullMarks = (one: ItemTry): boolean => one.score / one.maxScore >= 1 - 1e-9;\n\n/** Whether, after the tries `made`, a question offers another: the rule `open` reads. */\nfunction offersAfter(\n now: {\n policy: ResolvedItemScoringPolicy;\n graded: boolean;\n disabled: boolean;\n channel: { triesClosed: boolean } | null;\n },\n made: readonly ItemTry[],\n shut: boolean,\n): boolean {\n const last = made[made.length - 1];\n return (\n now.graded &&\n !now.disabled &&\n !shut &&\n now.channel?.triesClosed !== true &&\n last !== undefined &&\n made.length < 1 + now.policy.retries &&\n !fullMarks(last)\n );\n}\n\nexport interface TriesInput {\n policy: ResolvedItemScoringPolicy;\n /**\n * Whether the component grades this answer itself and the learner may see\n * how it went: `practice`, with `feedback` on. \"Try again\" anywhere else\n * would be meaningless, or would say an answer was wrong on a paper that\n * does not say so.\n */\n graded: boolean;\n submitted: boolean;\n disabled: boolean;\n}\n\nexport interface Tries {\n /** Every graded try so far, in order. */\n tries: readonly ItemTry[];\n /** The question's score under the policy, once a try has been graded. */\n counted: ItemTriesScore | null;\n /**\n * Whether the learner is being offered another try at a graded answer: the\n * last try earned less than full marks, the policy has tries left, and\n * nothing has closed the question. While it is, the question shows no right\n * answer and no explanation — either would hand over the answer the next try\n * is for.\n */\n open: boolean;\n /** Tries still to come after the last graded one. */\n left: number;\n /** Records a graded try and returns the question's score under the policy. */\n record: (graded: ItemTry) => ItemTriesScore;\n /** Ends the question's tries: \"Show answer\". */\n close: () => void;\n /** Forgets every try: a different question. */\n reset: () => void;\n}\n\n/**\n * The tries a question has had and whether it offers another: the SDK's own\n * components' rules for \"Try again\", in one place.\n *\n * Reports whether a try is on offer to the paper around the question, which\n * waits for it before reporting the set while the question is on screen — and\n * closes every question's tries once it has reported.\n */\nexport function useTries({ policy, graded, submitted, disabled }: TriesInput): Tries {\n const channel = useContext(SequenceSlotContext);\n const [tries, setTries] = useState<readonly ItemTry[]>([]);\n const [closed, setClosed] = useState(false);\n // Written synchronously, so a submit that records a try and reports it reads\n // every try before it, whatever React has rendered.\n const triesRef = useRef<readonly ItemTry[]>([]);\n const closedRef = useRef(false);\n const latest = useRef({ policy, graded, disabled, channel });\n latest.current = { policy, graded, disabled, channel };\n\n const record = useCallback((one: ItemTry): ItemTriesScore => {\n const next = [...triesRef.current, one];\n triesRef.current = next;\n setTries(next);\n // An offer of another try is reported before the grade is handed on, not\n // after the next render: the paper reports its set when a grade fills its\n // last slot, and must already know to wait. The end of the tries is not:\n // said now, it would let the paper report the grade this try is replacing.\n // The effect below says it once this grade has landed.\n if (offersAfter(latest.current, next, closedRef.current)) {\n latest.current.channel?.triesState('offered');\n }\n return scoreTries(next, latest.current.policy);\n }, []);\n const close = useCallback(() => {\n closedRef.current = true;\n setClosed(true);\n }, []);\n const reset = useCallback(() => {\n triesRef.current = [];\n closedRef.current = false;\n setTries([]);\n setClosed(false);\n }, []);\n\n const allowed = 1 + policy.retries;\n const left = Math.max(0, allowed - tries.length);\n const open = submitted && offersAfter(latest.current, tries, closed);\n // A try the learner is making now — after \"Try again\", before the answer is\n // in again. Reporting the set then would hand on the grade the learner is in\n // the middle of replacing.\n const retrying =\n !submitted &&\n tries.length > 0 &&\n !closed &&\n graded &&\n !disabled &&\n channel?.triesClosed !== true;\n const state: TriesState = open ? 'offered' : retrying ? 'retrying' : 'none';\n\n const report = channel?.triesState;\n useEffect(() => {\n report?.(state);\n }, [report, state]);\n // A question that goes away offers nothing: the paper must not wait for it.\n useEffect(() => () => report?.('none'), [report]);\n\n const counted = useMemo(\n () => (tries.length === 0 ? null : scoreTries(tries, policy)),\n [tries, policy],\n );\n return { tries, counted, open, left, record, close, reset };\n}\n\n/** A fraction of the marks as the whole percentage a learner reads. */\nexport const percentOf = (fraction: number): number => Math.round(fraction * 100);\n\n/**\n * The sentences after the score, on a question whose policy changed it: which\n * try counts when it is not the one just made, what this answer scored before\n * its costs, and how many tries are left.\n */\nexport function triesSummary(\n s: LkStrings,\n policy: ResolvedItemScoringPolicy,\n counted: ItemTriesScore,\n offered: boolean,\n): string {\n const parts: string[] = [];\n const latest = counted.tries.length - 1;\n const mine = counted.tries[latest];\n if (mine !== undefined) {\n if (counted.counted !== latest) {\n parts.push(\n s.countedTry(\n policy.counts === 'best' ? 'best' : 'first',\n percentOf(mine.scored / mine.maxScore),\n ),\n );\n } else if (mine.scored !== mine.score) {\n parts.push(s.scoreBeforeCosts(percentOf(mine.score / mine.maxScore)));\n }\n }\n if (offered) {\n parts.push(\n s.triesLeft(policy.retries + 1 - counted.tries.length, percentOf(policy.retryPenalty)),\n );\n }\n return parts.join(' ');\n}\n\n/**\n * \"Try again\" and \"Show answer\" — or \"Keep this answer\", on a paper that shows\n * no right answers — after a graded try that did not earn full marks.\n */\nexport function TryActions({\n tries,\n solutions,\n strings: s,\n onRetry,\n onClose,\n}: {\n tries: Tries;\n /** Whether closing the question will show the right answer. */\n solutions: boolean;\n strings: LkStrings;\n onRetry: () => void;\n onClose: () => void;\n}): React.JSX.Element | null {\n if (!tries.open) {\n return null;\n }\n return (\n <div className=\"lk-tries\">\n <button type=\"button\" className=\"lk-tries-again\" onClick={onRetry}>\n {s.tryAgain}\n </button>\n <button type=\"button\" className=\"lk-tries-close\" onClick={onClose}>\n {solutions ? s.showAnswer : s.keepAnswer}\n </button>\n </div>\n );\n}\n\n/** What a hint costs, said before the learner asks for one. */\nexport function HintCost({\n policy,\n strings: s,\n}: {\n policy: ResolvedItemScoringPolicy;\n strings: LkStrings;\n}): React.JSX.Element | null {\n if (policy.hintPenalty <= 0) {\n return null;\n }\n return <p className=\"lk-hint-cost\">{s.hintCost(percentOf(policy.hintPenalty))}</p>;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { f as SequenceSlotContext, n as useAiExplanation, r as useAiHints } from "./useAiHelp-B-ccDL5Q.js";
2
+ import { f as SequenceSlotContext, n as useAiExplanation, r as useAiHints } from "./useAiHelp-BJy05hqV.js";
3
3
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { resolveItemScoringPolicy, scoreTries } from "@intellectif/lk-core";
@@ -307,4 +307,4 @@ function HintCost({ policy, strings: s }) {
307
307
  //#endregion
308
308
  export { useItemScoringPolicy as a, AiHints as c, triesSummary as i, useComponentAiHints as l, TryActions as n, useTries as o, hintsOf as r, AiExplanation as s, HintCost as t };
309
309
 
310
- //# sourceMappingURL=tries-DTHSrLCm.js.map
310
+ //# sourceMappingURL=tries-CTT1URXx.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tries-DTHSrLCm.js","names":[],"sources":["../src/components/shared/AiHelp.tsx","../src/components/shared/tries.tsx"],"sourcesContent":["'use client';\n\nimport type {\n InteractionEvent,\n ItemOutcome,\n LearnerResponse,\n ResolvedDeliveryPolicy,\n} from '@intellectif/lk-core';\nimport { useEffect, useRef } from 'react';\nimport type { LearnerAi } from '../../ai/LkAiProvider.js';\nimport { type AiHintsHelp, useAiExplanation, useAiHints } from '../../ai/useAiHelp.js';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport type { RenderableActivity, RenderMode } from '../types.js';\n\ninterface Situation {\n ai: LearnerAi | undefined;\n data: RenderableActivity;\n renderMode: RenderMode;\n submitted: boolean;\n /** The learner's answer as it stands: submitted, restored, or being written. */\n response: LearnerResponse;\n /** The component's `locale`, the default language for what is written. */\n locale: string | undefined;\n onInteraction: ((event: InteractionEvent) => void) | undefined;\n strings: LkStrings;\n /** The policy the component resolved, the paper's included. */\n delivery: ResolvedDeliveryPolicy;\n}\n\n/** What the hooks take, from what a component holds: the optional half, without `undefined` values. */\nfunction situation(input: Situation): {\n data: RenderableActivity;\n response: LearnerResponse;\n submitted: boolean;\n renderMode: RenderMode;\n ai?: LearnerAi;\n locale?: string;\n onInteraction?: (event: InteractionEvent) => void;\n delivery: ResolvedDeliveryPolicy;\n} {\n const { data, response, submitted, renderMode, ai, locale, onInteraction, delivery } = input;\n return {\n data,\n response,\n submitted,\n renderMode,\n delivery,\n ...(ai !== undefined ? { ai } : {}),\n ...(locale !== undefined ? { locale } : {}),\n ...(onInteraction !== undefined ? { onInteraction } : {}),\n };\n}\n\n/**\n * \"Explain my answer\": the SDK's own surface over {@link useAiExplanation} — a\n * button, the explanation with who wrote it, and \"not available\" when a call\n * fails or its answer is refused.\n *\n * Nothing renders without a port, or where the rules say no explanation.\n */\nexport function AiExplanation({\n outcome,\n ...rest\n}: Situation & { outcome: ItemOutcome | undefined }) {\n const s = rest.strings;\n const help = useAiExplanation({\n ...situation(rest),\n ...(outcome !== undefined ? { outcome } : {}),\n });\n const buttonRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLElement>(null);\n const refocus = useRef(false);\n\n // The button that asked goes away when the explanation arrives; focus moves\n // to what it asked for rather than falling to the page.\n useEffect(() => {\n if (help.status === 'shown' && refocus.current) {\n refocus.current = false;\n panelRef.current?.focus();\n }\n if (help.status === 'unavailable') {\n refocus.current = false;\n }\n }, [help.status]);\n\n if (!help.offered) {\n return null;\n }\n\n const ask = (): void => {\n refocus.current =\n typeof document !== 'undefined' && document.activeElement === buttonRef.current;\n help.ask();\n };\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai\" data-state={help.status}>\n {help.explanation === null ? (\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={ask}\n >\n {loading ? s.aiExplaining : s.aiExplain}\n </button>\n ) : null}\n <div aria-live=\"polite\">\n {help.explanation !== null ? (\n <section\n ref={panelRef}\n className=\"lk-ai-panel\"\n tabIndex={-1}\n aria-label={s.aiExplanation}\n >\n <p className=\"lk-ai-heading\">{s.aiExplanation}</p>\n <p className=\"lk-ai-text\">{help.explanation.text}</p>\n <p className=\"lk-ai-notice\">{s.aiNotice}</p>\n </section>\n ) : help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiExplanationUnavailable}</p>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The AI hints of one of the SDK's own questions: {@link useAiHints}, called\n * by the component itself so it can count the hints its learner was shown.\n */\nexport function useComponentAiHints({\n disabled,\n ...rest\n}: Situation & { disabled: boolean }): AiHintsHelp {\n return useAiHints({ ...situation(rest), disabled });\n}\n\n/**\n * \"Get a hint\": the SDK's own surface over {@link useAiHints} — a button, the\n * hints given so far as a numbered list, and who wrote them.\n *\n * The hints stay listed after submit, so the learner can see what they were\n * given, but no more can be asked for — until a \"Try again\" reopens the\n * answer, when the same list carries on. Another question starts with none.\n */\nexport function AiHints({\n help,\n ai,\n strings: s,\n}: {\n help: AiHintsHelp;\n ai: LearnerAi | undefined;\n strings: LkStrings;\n}) {\n const used = help.used;\n\n if (ai?.hint === undefined || (!help.offered && used === 0)) {\n return null;\n }\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai lk-ai-hint-area\" data-state={help.status}>\n <div aria-live=\"polite\">\n {used > 0 ? (\n <ol className=\"lk-ai-hints\" aria-label={s.aiHints}>\n {help.hints.map((hint, index) => (\n // Hints are only ever appended, so a position is a stable key.\n // biome-ignore lint/suspicious/noArrayIndexKey: an append-only list\n <li key={index} className=\"lk-ai-hint\">\n <span className=\"lk-ai-hint-label\">{s.aiHintNumber(index + 1)}</span>{' '}\n <span className=\"lk-ai-text\">{hint.text}</span>\n </li>\n ))}\n </ol>\n ) : null}\n {help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiHintUnavailable}</p>\n ) : null}\n </div>\n {help.offered ? (\n used < help.limit ? (\n <button\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={help.ask}\n >\n {loading ? s.aiHintLoading : s.aiHint}\n </button>\n ) : (\n <p className=\"lk-ai-note\">{s.aiNoMoreHints}</p>\n )\n ) : null}\n {used > 0 ? <p className=\"lk-ai-notice\">{s.aiNotice}</p> : null}\n </div>\n );\n}\n","'use client';\n\nimport {\n type ItemScoringPolicy,\n type ItemTriesScore,\n type ItemTry,\n type ResolvedItemScoringPolicy,\n resolveItemScoringPolicy,\n scoreTries,\n} from '@intellectif/lk-core';\nimport { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport { SequenceSlotContext, type TriesState } from './sequence-slot.js';\n\n/**\n * The scoring policy a question is under: the paper's, when the paper around\n * it was given one, else its own.\n *\n * Not combined, as delivery policies are: a cost and a count of tries have no\n * \"stricter\" to pick. The paper decides, as it decides the mode — so a\n * question drawn inside a set is scored by the set's rules whatever it was\n * handed.\n *\n * A policy `validateItemScoringPolicy` refuses throws here, at render. Every\n * setting in it moves a grade, and there is no safe way to read one wrongly:\n * the question fails loudly rather than grade by a guess.\n */\nexport function useItemScoringPolicy(\n given: ItemScoringPolicy | null | undefined,\n): ResolvedItemScoringPolicy {\n const around = useContext(SequenceSlotContext)?.scoring;\n const key = policyKey(given);\n // biome-ignore lint/correctness/useExhaustiveDependencies: the policy's content is the trigger, not its identity\n const own = useMemo(\n () => (around === undefined ? resolveItemScoringPolicy(given) : null),\n [around, key],\n );\n return around ?? (own as ResolvedItemScoringPolicy);\n}\n\nfunction policyKey(given: unknown): string {\n try {\n return JSON.stringify(given) ?? '';\n } catch {\n return String(given);\n }\n}\n\n/** A hint count as a response carries it: anything but a whole number above 0 is none. */\nexport function hintsOf(response: unknown): number {\n const count = (response as { hintsRevealed?: unknown } | null | undefined)?.hintsRevealed;\n return typeof count === 'number' && Number.isSafeInteger(count) && count > 0 ? count : 0;\n}\n\n/** Whether a try earned every mark there was: float noise above the maximum included. */\nconst fullMarks = (one: ItemTry): boolean => one.score / one.maxScore >= 1 - 1e-9;\n\n/** Whether, after the tries `made`, a question offers another: the rule `open` reads. */\nfunction offersAfter(\n now: {\n policy: ResolvedItemScoringPolicy;\n graded: boolean;\n disabled: boolean;\n channel: { triesClosed: boolean } | null;\n },\n made: readonly ItemTry[],\n shut: boolean,\n): boolean {\n const last = made[made.length - 1];\n return (\n now.graded &&\n !now.disabled &&\n !shut &&\n now.channel?.triesClosed !== true &&\n last !== undefined &&\n made.length < 1 + now.policy.retries &&\n !fullMarks(last)\n );\n}\n\nexport interface TriesInput {\n policy: ResolvedItemScoringPolicy;\n /**\n * Whether the component grades this answer itself and the learner may see\n * how it went: `practice`, with `feedback` on. \"Try again\" anywhere else\n * would be meaningless, or would say an answer was wrong on a paper that\n * does not say so.\n */\n graded: boolean;\n submitted: boolean;\n disabled: boolean;\n}\n\nexport interface Tries {\n /** Every graded try so far, in order. */\n tries: readonly ItemTry[];\n /** The question's score under the policy, once a try has been graded. */\n counted: ItemTriesScore | null;\n /**\n * Whether the learner is being offered another try at a graded answer: the\n * last try earned less than full marks, the policy has tries left, and\n * nothing has closed the question. While it is, the question shows no right\n * answer and no explanation — either would hand over the answer the next try\n * is for.\n */\n open: boolean;\n /** Tries still to come after the last graded one. */\n left: number;\n /** Records a graded try and returns the question's score under the policy. */\n record: (graded: ItemTry) => ItemTriesScore;\n /** Ends the question's tries: \"Show answer\". */\n close: () => void;\n /** Forgets every try: a different question. */\n reset: () => void;\n}\n\n/**\n * The tries a question has had and whether it offers another: the SDK's own\n * components' rules for \"Try again\", in one place.\n *\n * Reports whether a try is on offer to the paper around the question, which\n * waits for it before reporting the set while the question is on screen — and\n * closes every question's tries once it has reported.\n */\nexport function useTries({ policy, graded, submitted, disabled }: TriesInput): Tries {\n const channel = useContext(SequenceSlotContext);\n const [tries, setTries] = useState<readonly ItemTry[]>([]);\n const [closed, setClosed] = useState(false);\n // Written synchronously, so a submit that records a try and reports it reads\n // every try before it, whatever React has rendered.\n const triesRef = useRef<readonly ItemTry[]>([]);\n const closedRef = useRef(false);\n const latest = useRef({ policy, graded, disabled, channel });\n latest.current = { policy, graded, disabled, channel };\n\n const record = useCallback((one: ItemTry): ItemTriesScore => {\n const next = [...triesRef.current, one];\n triesRef.current = next;\n setTries(next);\n // An offer of another try is reported before the grade is handed on, not\n // after the next render: the paper reports its set when a grade fills its\n // last slot, and must already know to wait. The end of the tries is not:\n // said now, it would let the paper report the grade this try is replacing.\n // The effect below says it once this grade has landed.\n if (offersAfter(latest.current, next, closedRef.current)) {\n latest.current.channel?.triesState('offered');\n }\n return scoreTries(next, latest.current.policy);\n }, []);\n const close = useCallback(() => {\n closedRef.current = true;\n setClosed(true);\n }, []);\n const reset = useCallback(() => {\n triesRef.current = [];\n closedRef.current = false;\n setTries([]);\n setClosed(false);\n }, []);\n\n const allowed = 1 + policy.retries;\n const left = Math.max(0, allowed - tries.length);\n const open = submitted && offersAfter(latest.current, tries, closed);\n // A try the learner is making now — after \"Try again\", before the answer is\n // in again. Reporting the set then would hand on the grade the learner is in\n // the middle of replacing.\n const retrying =\n !submitted &&\n tries.length > 0 &&\n !closed &&\n graded &&\n !disabled &&\n channel?.triesClosed !== true;\n const state: TriesState = open ? 'offered' : retrying ? 'retrying' : 'none';\n\n const report = channel?.triesState;\n useEffect(() => {\n report?.(state);\n }, [report, state]);\n // A question that goes away offers nothing: the paper must not wait for it.\n useEffect(() => () => report?.('none'), [report]);\n\n const counted = useMemo(\n () => (tries.length === 0 ? null : scoreTries(tries, policy)),\n [tries, policy],\n );\n return { tries, counted, open, left, record, close, reset };\n}\n\n/** A fraction of the marks as the whole percentage a learner reads. */\nexport const percentOf = (fraction: number): number => Math.round(fraction * 100);\n\n/**\n * The sentences after the score, on a question whose policy changed it: which\n * try counts when it is not the one just made, what this answer scored before\n * its costs, and how many tries are left.\n */\nexport function triesSummary(\n s: LkStrings,\n policy: ResolvedItemScoringPolicy,\n counted: ItemTriesScore,\n offered: boolean,\n): string {\n const parts: string[] = [];\n const latest = counted.tries.length - 1;\n const mine = counted.tries[latest];\n if (mine !== undefined) {\n if (counted.counted !== latest) {\n parts.push(\n s.countedTry(\n policy.counts === 'best' ? 'best' : 'first',\n percentOf(mine.scored / mine.maxScore),\n ),\n );\n } else if (mine.scored !== mine.score) {\n parts.push(s.scoreBeforeCosts(percentOf(mine.score / mine.maxScore)));\n }\n }\n if (offered) {\n parts.push(\n s.triesLeft(policy.retries + 1 - counted.tries.length, percentOf(policy.retryPenalty)),\n );\n }\n return parts.join(' ');\n}\n\n/**\n * \"Try again\" and \"Show answer\" — or \"Keep this answer\", on a paper that shows\n * no right answers — after a graded try that did not earn full marks.\n */\nexport function TryActions({\n tries,\n solutions,\n strings: s,\n onRetry,\n onClose,\n}: {\n tries: Tries;\n /** Whether closing the question will show the right answer. */\n solutions: boolean;\n strings: LkStrings;\n onRetry: () => void;\n onClose: () => void;\n}): React.JSX.Element | null {\n if (!tries.open) {\n return null;\n }\n return (\n <div className=\"lk-tries\">\n <button type=\"button\" className=\"lk-tries-again\" onClick={onRetry}>\n {s.tryAgain}\n </button>\n <button type=\"button\" className=\"lk-tries-close\" onClick={onClose}>\n {solutions ? s.showAnswer : s.keepAnswer}\n </button>\n </div>\n );\n}\n\n/** What a hint costs, said before the learner asks for one. */\nexport function HintCost({\n policy,\n strings: s,\n}: {\n policy: ResolvedItemScoringPolicy;\n strings: LkStrings;\n}): React.JSX.Element | null {\n if (policy.hintPenalty <= 0) {\n return null;\n }\n return <p className=\"lk-hint-cost\">{s.hintCost(percentOf(policy.hintPenalty))}</p>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tries-CTT1URXx.js","names":[],"sources":["../src/components/shared/AiHelp.tsx","../src/components/shared/tries.tsx"],"sourcesContent":["'use client';\n\nimport type {\n InteractionEvent,\n ItemOutcome,\n LearnerResponse,\n ResolvedDeliveryPolicy,\n} from '@intellectif/lk-core';\nimport { useEffect, useRef } from 'react';\nimport type { LearnerAi } from '../../ai/LkAiProvider.js';\nimport { type AiHintsHelp, useAiExplanation, useAiHints } from '../../ai/useAiHelp.js';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport type { RenderableActivity, RenderMode } from '../types.js';\n\ninterface Situation {\n ai: LearnerAi | undefined;\n data: RenderableActivity;\n renderMode: RenderMode;\n submitted: boolean;\n /** The learner's answer as it stands: submitted, restored, or being written. */\n response: LearnerResponse;\n /** The component's `locale`, the default language for what is written. */\n locale: string | undefined;\n onInteraction: ((event: InteractionEvent) => void) | undefined;\n strings: LkStrings;\n /** The policy the component resolved, the paper's included. */\n delivery: ResolvedDeliveryPolicy;\n}\n\n/** What the hooks take, from what a component holds: the optional half, without `undefined` values. */\nfunction situation(input: Situation): {\n data: RenderableActivity;\n response: LearnerResponse;\n submitted: boolean;\n renderMode: RenderMode;\n ai?: LearnerAi;\n locale?: string;\n onInteraction?: (event: InteractionEvent) => void;\n delivery: ResolvedDeliveryPolicy;\n} {\n const { data, response, submitted, renderMode, ai, locale, onInteraction, delivery } = input;\n return {\n data,\n response,\n submitted,\n renderMode,\n delivery,\n ...(ai !== undefined ? { ai } : {}),\n ...(locale !== undefined ? { locale } : {}),\n ...(onInteraction !== undefined ? { onInteraction } : {}),\n };\n}\n\n/**\n * \"Explain my answer\": the SDK's own surface over {@link useAiExplanation} — a\n * button, the explanation with who wrote it, and \"not available\" when a call\n * fails or its answer is refused.\n *\n * Nothing renders without a port, or where the rules say no explanation.\n */\nexport function AiExplanation({\n outcome,\n ...rest\n}: Situation & { outcome: ItemOutcome | undefined }) {\n const s = rest.strings;\n const help = useAiExplanation({\n ...situation(rest),\n ...(outcome !== undefined ? { outcome } : {}),\n });\n const buttonRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLElement>(null);\n const refocus = useRef(false);\n\n // The button that asked goes away when the explanation arrives; focus moves\n // to what it asked for rather than falling to the page.\n useEffect(() => {\n if (help.status === 'shown' && refocus.current) {\n refocus.current = false;\n panelRef.current?.focus();\n }\n if (help.status === 'unavailable') {\n refocus.current = false;\n }\n }, [help.status]);\n\n if (!help.offered) {\n return null;\n }\n\n const ask = (): void => {\n refocus.current =\n typeof document !== 'undefined' && document.activeElement === buttonRef.current;\n help.ask();\n };\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai\" data-state={help.status}>\n {help.explanation === null ? (\n <button\n ref={buttonRef}\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={ask}\n >\n {loading ? s.aiExplaining : s.aiExplain}\n </button>\n ) : null}\n <div aria-live=\"polite\">\n {help.explanation !== null ? (\n <section\n ref={panelRef}\n className=\"lk-ai-panel\"\n tabIndex={-1}\n aria-label={s.aiExplanation}\n >\n <p className=\"lk-ai-heading\">{s.aiExplanation}</p>\n <p className=\"lk-ai-text\">{help.explanation.text}</p>\n <p className=\"lk-ai-notice\">{s.aiNotice}</p>\n </section>\n ) : help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiExplanationUnavailable}</p>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The AI hints of one of the SDK's own questions: {@link useAiHints}, called\n * by the component itself so it can count the hints its learner was shown.\n */\nexport function useComponentAiHints({\n disabled,\n ...rest\n}: Situation & { disabled: boolean }): AiHintsHelp {\n return useAiHints({ ...situation(rest), disabled });\n}\n\n/**\n * \"Get a hint\": the SDK's own surface over {@link useAiHints} — a button, the\n * hints given so far as a numbered list, and who wrote them.\n *\n * The hints stay listed after submit, so the learner can see what they were\n * given, but no more can be asked for — until a \"Try again\" reopens the\n * answer, when the same list carries on. Another question starts with none.\n */\nexport function AiHints({\n help,\n ai,\n strings: s,\n}: {\n help: AiHintsHelp;\n ai: LearnerAi | undefined;\n strings: LkStrings;\n}) {\n const used = help.used;\n\n if (ai?.hint === undefined || (!help.offered && used === 0)) {\n return null;\n }\n\n const loading = help.status === 'loading';\n return (\n <div className=\"lk-ai lk-ai-hint-area\" data-state={help.status}>\n <div aria-live=\"polite\">\n {used > 0 ? (\n <ol className=\"lk-ai-hints\" aria-label={s.aiHints}>\n {help.hints.map((hint, index) => (\n // Hints are only ever appended, so a position is a stable key.\n // biome-ignore lint/suspicious/noArrayIndexKey: an append-only list\n <li key={index} className=\"lk-ai-hint\">\n <span className=\"lk-ai-hint-label\">{s.aiHintNumber(index + 1)}</span>{' '}\n <span className=\"lk-ai-text\">{hint.text}</span>\n </li>\n ))}\n </ol>\n ) : null}\n {help.status === 'unavailable' ? (\n <p className=\"lk-ai-unavailable\">{s.aiHintUnavailable}</p>\n ) : null}\n </div>\n {help.offered ? (\n used < help.limit ? (\n <button\n type=\"button\"\n className=\"lk-ai-button\"\n aria-busy={loading || undefined}\n aria-disabled={loading || undefined}\n onClick={help.ask}\n >\n {loading ? s.aiHintLoading : s.aiHint}\n </button>\n ) : (\n <p className=\"lk-ai-note\">{s.aiNoMoreHints}</p>\n )\n ) : null}\n {used > 0 ? <p className=\"lk-ai-notice\">{s.aiNotice}</p> : null}\n </div>\n );\n}\n","'use client';\n\nimport {\n type ItemScoringPolicy,\n type ItemTriesScore,\n type ItemTry,\n type ResolvedItemScoringPolicy,\n resolveItemScoringPolicy,\n scoreTries,\n} from '@intellectif/lk-core';\nimport { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport type { LkStrings } from '../../i18n/strings.js';\nimport { SequenceSlotContext, type TriesState } from './sequence-slot.js';\n\n/**\n * The scoring policy a question is under: the paper's, when the paper around\n * it was given one, else its own.\n *\n * Not combined, as delivery policies are: a cost and a count of tries have no\n * \"stricter\" to pick. The paper decides, as it decides the mode — so a\n * question drawn inside a set is scored by the set's rules whatever it was\n * handed.\n *\n * A policy `validateItemScoringPolicy` refuses throws here, at render. Every\n * setting in it moves a grade, and there is no safe way to read one wrongly:\n * the question fails loudly rather than grade by a guess.\n */\nexport function useItemScoringPolicy(\n given: ItemScoringPolicy | null | undefined,\n): ResolvedItemScoringPolicy {\n const around = useContext(SequenceSlotContext)?.scoring;\n const key = policyKey(given);\n // biome-ignore lint/correctness/useExhaustiveDependencies: the policy's content is the trigger, not its identity\n const own = useMemo(\n () => (around === undefined ? resolveItemScoringPolicy(given) : null),\n [around, key],\n );\n return around ?? (own as ResolvedItemScoringPolicy);\n}\n\nfunction policyKey(given: unknown): string {\n try {\n return JSON.stringify(given) ?? '';\n } catch {\n return String(given);\n }\n}\n\n/** A hint count as a response carries it: anything but a whole number above 0 is none. */\nexport function hintsOf(response: unknown): number {\n const count = (response as { hintsRevealed?: unknown } | null | undefined)?.hintsRevealed;\n return typeof count === 'number' && Number.isSafeInteger(count) && count > 0 ? count : 0;\n}\n\n/** Whether a try earned every mark there was: float noise above the maximum included. */\nconst fullMarks = (one: ItemTry): boolean => one.score / one.maxScore >= 1 - 1e-9;\n\n/** Whether, after the tries `made`, a question offers another: the rule `open` reads. */\nfunction offersAfter(\n now: {\n policy: ResolvedItemScoringPolicy;\n graded: boolean;\n disabled: boolean;\n channel: { triesClosed: boolean } | null;\n },\n made: readonly ItemTry[],\n shut: boolean,\n): boolean {\n const last = made[made.length - 1];\n return (\n now.graded &&\n !now.disabled &&\n !shut &&\n now.channel?.triesClosed !== true &&\n last !== undefined &&\n made.length < 1 + now.policy.retries &&\n !fullMarks(last)\n );\n}\n\nexport interface TriesInput {\n policy: ResolvedItemScoringPolicy;\n /**\n * Whether the component grades this answer itself and the learner may see\n * how it went: `practice`, with `feedback` on. \"Try again\" anywhere else\n * would be meaningless, or would say an answer was wrong on a paper that\n * does not say so.\n */\n graded: boolean;\n submitted: boolean;\n disabled: boolean;\n}\n\nexport interface Tries {\n /** Every graded try so far, in order. */\n tries: readonly ItemTry[];\n /** The question's score under the policy, once a try has been graded. */\n counted: ItemTriesScore | null;\n /**\n * Whether the learner is being offered another try at a graded answer: the\n * last try earned less than full marks, the policy has tries left, and\n * nothing has closed the question. While it is, the question shows no right\n * answer and no explanation — either would hand over the answer the next try\n * is for.\n */\n open: boolean;\n /** Tries still to come after the last graded one. */\n left: number;\n /** Records a graded try and returns the question's score under the policy. */\n record: (graded: ItemTry) => ItemTriesScore;\n /** Ends the question's tries: \"Show answer\". */\n close: () => void;\n /** Forgets every try: a different question. */\n reset: () => void;\n}\n\n/**\n * The tries a question has had and whether it offers another: the SDK's own\n * components' rules for \"Try again\", in one place.\n *\n * Reports whether a try is on offer to the paper around the question, which\n * waits for it before reporting the set while the question is on screen — and\n * closes every question's tries once it has reported.\n */\nexport function useTries({ policy, graded, submitted, disabled }: TriesInput): Tries {\n const channel = useContext(SequenceSlotContext);\n const [tries, setTries] = useState<readonly ItemTry[]>([]);\n const [closed, setClosed] = useState(false);\n // Written synchronously, so a submit that records a try and reports it reads\n // every try before it, whatever React has rendered.\n const triesRef = useRef<readonly ItemTry[]>([]);\n const closedRef = useRef(false);\n const latest = useRef({ policy, graded, disabled, channel });\n latest.current = { policy, graded, disabled, channel };\n\n const record = useCallback((one: ItemTry): ItemTriesScore => {\n const next = [...triesRef.current, one];\n triesRef.current = next;\n setTries(next);\n // An offer of another try is reported before the grade is handed on, not\n // after the next render: the paper reports its set when a grade fills its\n // last slot, and must already know to wait. The end of the tries is not:\n // said now, it would let the paper report the grade this try is replacing.\n // The effect below says it once this grade has landed.\n if (offersAfter(latest.current, next, closedRef.current)) {\n latest.current.channel?.triesState('offered');\n }\n return scoreTries(next, latest.current.policy);\n }, []);\n const close = useCallback(() => {\n closedRef.current = true;\n setClosed(true);\n }, []);\n const reset = useCallback(() => {\n triesRef.current = [];\n closedRef.current = false;\n setTries([]);\n setClosed(false);\n }, []);\n\n const allowed = 1 + policy.retries;\n const left = Math.max(0, allowed - tries.length);\n const open = submitted && offersAfter(latest.current, tries, closed);\n // A try the learner is making now — after \"Try again\", before the answer is\n // in again. Reporting the set then would hand on the grade the learner is in\n // the middle of replacing.\n const retrying =\n !submitted &&\n tries.length > 0 &&\n !closed &&\n graded &&\n !disabled &&\n channel?.triesClosed !== true;\n const state: TriesState = open ? 'offered' : retrying ? 'retrying' : 'none';\n\n const report = channel?.triesState;\n useEffect(() => {\n report?.(state);\n }, [report, state]);\n // A question that goes away offers nothing: the paper must not wait for it.\n useEffect(() => () => report?.('none'), [report]);\n\n const counted = useMemo(\n () => (tries.length === 0 ? null : scoreTries(tries, policy)),\n [tries, policy],\n );\n return { tries, counted, open, left, record, close, reset };\n}\n\n/** A fraction of the marks as the whole percentage a learner reads. */\nexport const percentOf = (fraction: number): number => Math.round(fraction * 100);\n\n/**\n * The sentences after the score, on a question whose policy changed it: which\n * try counts when it is not the one just made, what this answer scored before\n * its costs, and how many tries are left.\n */\nexport function triesSummary(\n s: LkStrings,\n policy: ResolvedItemScoringPolicy,\n counted: ItemTriesScore,\n offered: boolean,\n): string {\n const parts: string[] = [];\n const latest = counted.tries.length - 1;\n const mine = counted.tries[latest];\n if (mine !== undefined) {\n if (counted.counted !== latest) {\n parts.push(\n s.countedTry(\n policy.counts === 'best' ? 'best' : 'first',\n percentOf(mine.scored / mine.maxScore),\n ),\n );\n } else if (mine.scored !== mine.score) {\n parts.push(s.scoreBeforeCosts(percentOf(mine.score / mine.maxScore)));\n }\n }\n if (offered) {\n parts.push(\n s.triesLeft(policy.retries + 1 - counted.tries.length, percentOf(policy.retryPenalty)),\n );\n }\n return parts.join(' ');\n}\n\n/**\n * \"Try again\" and \"Show answer\" — or \"Keep this answer\", on a paper that shows\n * no right answers — after a graded try that did not earn full marks.\n */\nexport function TryActions({\n tries,\n solutions,\n strings: s,\n onRetry,\n onClose,\n}: {\n tries: Tries;\n /** Whether closing the question will show the right answer. */\n solutions: boolean;\n strings: LkStrings;\n onRetry: () => void;\n onClose: () => void;\n}): React.JSX.Element | null {\n if (!tries.open) {\n return null;\n }\n return (\n <div className=\"lk-tries\">\n <button type=\"button\" className=\"lk-tries-again\" onClick={onRetry}>\n {s.tryAgain}\n </button>\n <button type=\"button\" className=\"lk-tries-close\" onClick={onClose}>\n {solutions ? s.showAnswer : s.keepAnswer}\n </button>\n </div>\n );\n}\n\n/** What a hint costs, said before the learner asks for one. */\nexport function HintCost({\n policy,\n strings: s,\n}: {\n policy: ResolvedItemScoringPolicy;\n strings: LkStrings;\n}): React.JSX.Element | null {\n if (policy.hintPenalty <= 0) {\n return null;\n }\n return <p className=\"lk-hint-cost\">{s.hintCost(percentOf(policy.hintPenalty))}</p>;\n}\n"],"mappings":"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"}
@@ -203,6 +203,14 @@ function warnRefused(feature, activityId, reason) {
203
203
  if (isDevelopment()) console.warn(`learning-kit: the AI ${feature} for "${activityId}" was not shown (${reason}). The learner saw "not available" instead.`);
204
204
  }
205
205
  /** The language to write in: the host's choice for the learner, else the interface language. */
206
+ /**
207
+ * The group a request is about: the paper's own inside a pager, as the mode
208
+ * is, and otherwise the one the caller passed.
209
+ */
210
+ const groupOf = (paper, given) => {
211
+ const group = paper !== null ? paper.group : given;
212
+ return group === void 0 ? {} : { group };
213
+ };
206
214
  const localeOf = (ai, locale) => {
207
215
  const chosen = ai?.learnerLocale ?? locale;
208
216
  return chosen === void 0 ? {} : { learnerLocale: chosen };
@@ -302,6 +310,7 @@ function useAiExplanation(input) {
302
310
  });
303
311
  const latest = useRef({
304
312
  input,
313
+ around,
305
314
  renderMode,
306
315
  ai,
307
316
  resetKey,
@@ -310,6 +319,7 @@ function useAiExplanation(input) {
310
319
  });
311
320
  latest.current = {
312
321
  input,
322
+ around,
313
323
  renderMode,
314
324
  ai,
315
325
  resetKey,
@@ -332,6 +342,7 @@ function useAiExplanation(input) {
332
342
  data: now.input.data,
333
343
  response: now.input.response,
334
344
  ...now.renderMode === "review" && now.input.outcome !== void 0 ? { outcome: now.input.outcome } : {},
345
+ ...groupOf(now.around, now.input.group),
335
346
  ...localeOf(now.ai, now.input.locale)
336
347
  });
337
348
  if (request === null) {
@@ -447,6 +458,7 @@ function useAiHints(input) {
447
458
  const limit = hintLimit(ai);
448
459
  const latest = useRef({
449
460
  input,
461
+ around,
450
462
  renderMode,
451
463
  ai,
452
464
  offered,
@@ -455,6 +467,7 @@ function useAiHints(input) {
455
467
  });
456
468
  latest.current = {
457
469
  input,
470
+ around,
458
471
  renderMode,
459
472
  ai,
460
473
  offered,
@@ -489,6 +502,7 @@ function useAiHints(input) {
489
502
  data: now.input.data,
490
503
  response: now.input.response,
491
504
  previousHints: held.hints.map((hint) => hint.text),
505
+ ...groupOf(now.around, now.input.group),
492
506
  ...localeOf(now.ai, now.input.locale)
493
507
  });
494
508
  if (request === null) {
@@ -610,6 +624,7 @@ function useAiWritingFeedback(input) {
610
624
  const limit = writingFeedbackLimit(ai);
611
625
  const latest = useRef({
612
626
  input,
627
+ around,
613
628
  ai,
614
629
  offered,
615
630
  limit,
@@ -617,6 +632,7 @@ function useAiWritingFeedback(input) {
617
632
  });
618
633
  latest.current = {
619
634
  input,
635
+ around,
620
636
  ai,
621
637
  offered,
622
638
  limit,
@@ -651,6 +667,7 @@ function useAiWritingFeedback(input) {
651
667
  data: now.input.data,
652
668
  response: now.input.response,
653
669
  previousFeedback: held.shown.map((one) => one.feedback.text),
670
+ ...groupOf(now.around, now.input.group),
654
671
  ...localeOf(now.ai, now.input.locale)
655
672
  });
656
673
  if (request === null) return;
@@ -903,4 +920,4 @@ function useAiCoaching(input) {
903
920
  //#endregion
904
921
  export { ANONYMOUS_ACTOR as a, isDevelopment as c, randomSessionId as d, SequenceSlotContext as f, takeOf as h, useAiWritingFeedback as i, legacyCorrect as l, stampTake as m, useAiExplanation as n, detailIsRight as o, mintTake as p, useAiHints as r, everyBuiltInTypeHandled as s, useAiCoaching as t, objectIdFor as u };
905
922
 
906
- //# sourceMappingURL=useAiHelp-B-ccDL5Q.js.map
923
+ //# sourceMappingURL=useAiHelp-BJy05hqV.js.map