@intellectif/lk-react 23.1.0 → 23.1.2
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.
- package/CHANGELOG.md +23 -0
- package/README.md +9 -5
- package/dist/ActivityErrorBoundary-B379hvtL.cjs +39 -0
- package/dist/ActivityErrorBoundary-B379hvtL.cjs.map +1 -0
- package/dist/ActivityErrorBoundary-Dw6SbWv3.js +34 -0
- package/dist/ActivityErrorBoundary-Dw6SbWv3.js.map +1 -0
- package/dist/ActivityMedia-CQQTRNl7.cjs +641 -0
- package/dist/ActivityMedia-CQQTRNl7.cjs.map +1 -0
- package/dist/ActivityMedia-vDiP-Psa.js +636 -0
- package/dist/ActivityMedia-vDiP-Psa.js.map +1 -0
- package/dist/ActivityPreview-1hhTkYuj.js +240 -0
- package/dist/ActivityPreview-1hhTkYuj.js.map +1 -0
- package/dist/ActivityPreview-BKr34exz.cjs +245 -0
- package/dist/ActivityPreview-BKr34exz.cjs.map +1 -0
- package/dist/ActivitySequence-CkX87pfB.js +839 -0
- package/dist/ActivitySequence-CkX87pfB.js.map +1 -0
- package/dist/ActivitySequence-CmoSyDnp.cjs +844 -0
- package/dist/ActivitySequence-CmoSyDnp.cjs.map +1 -0
- package/dist/Dictation-BlxRp-3k.js +926 -0
- package/dist/Dictation-BlxRp-3k.js.map +1 -0
- package/dist/Dictation-jECzL88E.cjs +931 -0
- package/dist/Dictation-jECzL88E.cjs.map +1 -0
- package/dist/FeedbackRegion-BNNMtTY-.cjs +108 -0
- package/dist/FeedbackRegion-BNNMtTY-.cjs.map +1 -0
- package/dist/FeedbackRegion-CyPSp8bA.js +85 -0
- package/dist/FeedbackRegion-CyPSp8bA.js.map +1 -0
- package/dist/FillInTheBlanks-B6xzqYXx.js +474 -0
- package/dist/FillInTheBlanks-B6xzqYXx.js.map +1 -0
- package/dist/FillInTheBlanks-BpKhdHg2.cjs +479 -0
- package/dist/FillInTheBlanks-BpKhdHg2.cjs.map +1 -0
- package/dist/GapSelect-DUSiaEQH.js +441 -0
- package/dist/GapSelect-DUSiaEQH.js.map +1 -0
- package/dist/GapSelect-payTq_Zn.cjs +446 -0
- package/dist/GapSelect-payTq_Zn.cjs.map +1 -0
- package/dist/InteractiveVideo-BguGv-HA.cjs +2955 -0
- package/dist/InteractiveVideo-BguGv-HA.cjs.map +1 -0
- package/dist/InteractiveVideo-Cg2jbU3j.js +2944 -0
- package/dist/InteractiveVideo-Cg2jbU3j.js.map +1 -0
- package/dist/LkAiProvider-DAPpA3PQ.d.cts +84 -0
- package/dist/LkAiProvider-DAPpA3PQ.d.cts.map +1 -0
- package/dist/LkAiProvider-QdEcIhoo.d.ts +84 -0
- package/dist/LkAiProvider-QdEcIhoo.d.ts.map +1 -0
- package/dist/LkIntlProvider-D_FdYXjQ.js +375 -0
- package/dist/LkIntlProvider-D_FdYXjQ.js.map +1 -0
- package/dist/LkIntlProvider-oi57lYB0.cjs +410 -0
- package/dist/LkIntlProvider-oi57lYB0.cjs.map +1 -0
- package/dist/MultipleChoice-COn5MtKY.cjs +433 -0
- package/dist/MultipleChoice-COn5MtKY.cjs.map +1 -0
- package/dist/MultipleChoice-Dra0_KYD.js +428 -0
- package/dist/MultipleChoice-Dra0_KYD.js.map +1 -0
- package/dist/PronunciationFeedback-DlMPGjkR.cjs +831 -0
- package/dist/PronunciationFeedback-DlMPGjkR.cjs.map +1 -0
- package/dist/PronunciationFeedback-Dxd6XPkO.js +772 -0
- package/dist/PronunciationFeedback-Dxd6XPkO.js.map +1 -0
- package/dist/ReadAloud-C8RhaUqV.js +1092 -0
- package/dist/{chunk-DC4CQCK5.js.map → ReadAloud-C8RhaUqV.js.map} +1 -1
- package/dist/ReadAloud-akDehitT.cjs +1103 -0
- package/dist/ReadAloud-akDehitT.cjs.map +1 -0
- package/dist/StimulusPanel-BgzByfkb.js +74 -0
- package/dist/StimulusPanel-BgzByfkb.js.map +1 -0
- package/dist/StimulusPanel-b66gaN3i.cjs +79 -0
- package/dist/StimulusPanel-b66gaN3i.cjs.map +1 -0
- package/dist/ThemeProvider-B6DyGqzh.d.cts +62 -0
- package/dist/ThemeProvider-B6DyGqzh.d.cts.map +1 -0
- package/dist/ThemeProvider-BCrxBaY-.d.ts +62 -0
- package/dist/ThemeProvider-BCrxBaY-.d.ts.map +1 -0
- package/dist/ThemeProvider-BRN1tj4n.cjs +187 -0
- package/dist/ThemeProvider-BRN1tj4n.cjs.map +1 -0
- package/dist/ThemeProvider-C7K9iMLX.js +158 -0
- package/dist/ThemeProvider-C7K9iMLX.js.map +1 -0
- package/dist/WrittenResponse-Bj3eFmGl.cjs +491 -0
- package/dist/WrittenResponse-Bj3eFmGl.cjs.map +1 -0
- package/dist/WrittenResponse-Cl8G6nO_.js +486 -0
- package/dist/WrittenResponse-Cl8G6nO_.js.map +1 -0
- package/dist/ai/LkAiProvider.cjs +34 -14
- package/dist/ai/LkAiProvider.cjs.map +1 -1
- package/dist/ai/LkAiProvider.d.cts +2 -83
- package/dist/ai/LkAiProvider.d.ts +2 -83
- package/dist/ai/LkAiProvider.js +33 -2
- package/dist/ai/LkAiProvider.js.map +1 -1
- package/dist/ai/useAiHelp.cjs +7 -25
- package/dist/ai/useAiHelp.d.cts +175 -177
- package/dist/ai/useAiHelp.d.cts.map +1 -0
- package/dist/ai/useAiHelp.d.ts +175 -177
- package/dist/ai/useAiHelp.d.ts.map +1 -0
- package/dist/ai/useAiHelp.js +3 -4
- package/dist/components/ActivityPreview.cjs +3 -29
- package/dist/components/ActivityPreview.d.cts +2 -81
- package/dist/components/ActivityPreview.d.ts +2 -81
- package/dist/components/ActivityPreview.js +2 -20
- package/dist/components/ActivitySequence.cjs +3 -30
- package/dist/components/ActivitySequence.d.cts +2 -432
- package/dist/components/ActivitySequence.d.ts +2 -432
- package/dist/components/ActivitySequence.js +2 -21
- package/dist/components/Dictation.cjs +4 -21
- package/dist/components/Dictation.d.cts +2 -19
- package/dist/components/Dictation.d.ts +2 -19
- package/dist/components/Dictation.js +3 -12
- package/dist/components/FillInTheBlanks.cjs +4 -20
- package/dist/components/FillInTheBlanks.d.cts +2 -30
- package/dist/components/FillInTheBlanks.d.ts +2 -30
- package/dist/components/FillInTheBlanks.js +3 -11
- package/dist/components/GapSelect.cjs +4 -20
- package/dist/components/GapSelect.d.cts +2 -27
- package/dist/components/GapSelect.d.ts +2 -27
- package/dist/components/GapSelect.js +3 -11
- package/dist/components/InteractiveVideo.cjs +5 -32
- package/dist/components/InteractiveVideo.d.cts +3 -291
- package/dist/components/InteractiveVideo.d.ts +3 -291
- package/dist/components/InteractiveVideo.js +3 -19
- package/dist/components/MultipleChoice.cjs +4 -20
- package/dist/components/MultipleChoice.d.cts +2 -26
- package/dist/components/MultipleChoice.d.ts +2 -26
- package/dist/components/MultipleChoice.js +3 -11
- package/dist/components/PronunciationFeedback.cjs +4 -17
- package/dist/components/PronunciationFeedback.d.cts +2 -90
- package/dist/components/PronunciationFeedback.d.ts +2 -90
- package/dist/components/PronunciationFeedback.js +3 -8
- package/dist/components/ReadAloud.cjs +4 -22
- package/dist/components/ReadAloud.d.cts +7 -11
- package/dist/components/ReadAloud.d.cts.map +1 -0
- package/dist/components/ReadAloud.d.ts +7 -11
- package/dist/components/ReadAloud.d.ts.map +1 -0
- package/dist/components/ReadAloud.js +3 -13
- package/dist/components/StimulusPanel.cjs +3 -14
- package/dist/components/StimulusPanel.d.cts +2 -51
- package/dist/components/StimulusPanel.d.ts +2 -51
- package/dist/components/StimulusPanel.js +2 -5
- package/dist/components/WrittenResponse.cjs +4 -19
- package/dist/components/WrittenResponse.d.cts +3 -18
- package/dist/components/WrittenResponse.d.ts +3 -18
- package/dist/components/WrittenResponse.js +3 -10
- package/dist/direction-Dq0_HtA5.js +183 -0
- package/dist/direction-Dq0_HtA5.js.map +1 -0
- package/dist/direction-K1pj0HCA.cjs +200 -0
- package/dist/direction-K1pj0HCA.cjs.map +1 -0
- package/dist/hooks/useActivityState.cjs +28 -10
- package/dist/hooks/useActivityState.cjs.map +1 -1
- package/dist/hooks/useActivityState.d.cts +25 -24
- package/dist/hooks/useActivityState.d.cts.map +1 -0
- package/dist/hooks/useActivityState.d.ts +25 -24
- package/dist/hooks/useActivityState.d.ts.map +1 -0
- package/dist/hooks/useActivityState.js +28 -2
- package/dist/hooks/useActivityState.js.map +1 -1
- package/dist/hooks/useSpeechRecorder.cjs +5 -16
- package/dist/hooks/useSpeechRecorder.d.cts +2 -127
- package/dist/hooks/useSpeechRecorder.d.ts +2 -127
- package/dist/hooks/useSpeechRecorder.js +3 -3
- package/dist/hooks/useXAPI.cjs +126 -10
- package/dist/hooks/useXAPI.cjs.map +1 -1
- package/dist/hooks/useXAPI.d.cts +14 -14
- package/dist/hooks/useXAPI.d.cts.map +1 -0
- package/dist/hooks/useXAPI.d.ts +14 -14
- package/dist/hooks/useXAPI.d.ts.map +1 -0
- package/dist/hooks/useXAPI.js +126 -2
- package/dist/hooks/useXAPI.js.map +1 -1
- package/dist/i18n/LkIntlProvider.cjs +9 -32
- package/dist/i18n/LkIntlProvider.d.cts +34 -38
- package/dist/i18n/LkIntlProvider.d.cts.map +1 -0
- package/dist/i18n/LkIntlProvider.d.ts +34 -38
- package/dist/i18n/LkIntlProvider.d.ts.map +1 -0
- package/dist/i18n/LkIntlProvider.js +3 -3
- package/dist/index-6P4rJHrJ.d.cts +432 -0
- package/dist/index-6P4rJHrJ.d.cts.map +1 -0
- package/dist/index-90CX504k.d.ts +90 -0
- package/dist/index-90CX504k.d.ts.map +1 -0
- package/dist/index-B_SgnjsC.d.cts +291 -0
- package/dist/index-B_SgnjsC.d.cts.map +1 -0
- package/dist/index-BavVW5z5.d.cts +25 -0
- package/dist/index-BavVW5z5.d.cts.map +1 -0
- package/dist/index-BhHv6Fiu.d.cts +28 -0
- package/dist/index-BhHv6Fiu.d.cts.map +1 -0
- package/dist/index-Bsu8VciA.d.ts +432 -0
- package/dist/index-Bsu8VciA.d.ts.map +1 -0
- package/dist/index-C1KGpD1k.d.ts +49 -0
- package/dist/index-C1KGpD1k.d.ts.map +1 -0
- package/dist/index-CKTpYTCC.d.cts +49 -0
- package/dist/index-CKTpYTCC.d.cts.map +1 -0
- package/dist/index-CO_SRFJj.d.ts +16 -0
- package/dist/index-CO_SRFJj.d.ts.map +1 -0
- package/dist/index-CSsuUsP7.d.cts +124 -0
- package/dist/index-CSsuUsP7.d.cts.map +1 -0
- package/dist/index-CunUyeuN.d.cts +78 -0
- package/dist/index-CunUyeuN.d.cts.map +1 -0
- package/dist/index-CzfRzks6.d.cts +90 -0
- package/dist/index-CzfRzks6.d.cts.map +1 -0
- package/dist/index-D14xtpBS.d.cts +24 -0
- package/dist/index-D14xtpBS.d.cts.map +1 -0
- package/dist/index-DHRPAPZf.d.ts +291 -0
- package/dist/index-DHRPAPZf.d.ts.map +1 -0
- package/dist/index-DLV6cK26.d.cts +16 -0
- package/dist/index-DLV6cK26.d.cts.map +1 -0
- package/dist/index-DbCCQ9cP.d.ts +124 -0
- package/dist/index-DbCCQ9cP.d.ts.map +1 -0
- package/dist/index-DeV5B2m5.d.ts +24 -0
- package/dist/index-DeV5B2m5.d.ts.map +1 -0
- package/dist/index-TZCJVSge.d.ts +28 -0
- package/dist/index-TZCJVSge.d.ts.map +1 -0
- package/dist/index-ZVvLHsd3.d.ts +25 -0
- package/dist/index-ZVvLHsd3.d.ts.map +1 -0
- package/dist/index-eQwBgpSw.d.ts +78 -0
- package/dist/index-eQwBgpSw.d.ts.map +1 -0
- package/dist/index.cjs +118 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -24
- package/dist/index.d.ts +20 -24
- package/dist/index.js +84 -29
- package/dist/index.js.map +1 -1
- package/dist/theme/ThemeProvider.cjs +8 -28
- package/dist/theme/ThemeProvider.d.cts +2 -59
- package/dist/theme/ThemeProvider.d.ts +2 -59
- package/dist/theme/ThemeProvider.js +3 -3
- package/dist/tries-CbbtZkdG.cjs +363 -0
- package/dist/tries-CbbtZkdG.cjs.map +1 -0
- package/dist/tries-DTHSrLCm.js +310 -0
- package/dist/tries-DTHSrLCm.js.map +1 -0
- package/dist/types-BOQd93x3.d.cts +994 -0
- package/dist/types-BOQd93x3.d.cts.map +1 -0
- package/dist/types-HIrgF_mS.d.ts +994 -0
- package/dist/types-HIrgF_mS.d.ts.map +1 -0
- package/dist/useAiHelp-B-ccDL5Q.js +906 -0
- package/dist/useAiHelp-B-ccDL5Q.js.map +1 -0
- package/dist/useAiHelp-h9yOLFqt.cjs +995 -0
- package/dist/useAiHelp-h9yOLFqt.cjs.map +1 -0
- package/dist/useSpeechRecorder--O_f-xvt.cjs +720 -0
- package/dist/useSpeechRecorder--O_f-xvt.cjs.map +1 -0
- package/dist/useSpeechRecorder-DtAYvl02.d.cts +154 -0
- package/dist/useSpeechRecorder-DtAYvl02.d.cts.map +1 -0
- package/dist/useSpeechRecorder-DtAYvl02.d.ts +154 -0
- package/dist/useSpeechRecorder-DtAYvl02.d.ts.map +1 -0
- package/dist/useSpeechRecorder-zIi9Dlw8.js +709 -0
- package/dist/useSpeechRecorder-zIi9Dlw8.js.map +1 -0
- package/package.json +10 -11
- package/dist/WrittenResponse-B7A5gHMj.d.ts +0 -115
- package/dist/WrittenResponse-BpL9p2Hn.d.cts +0 -115
- package/dist/ai/useAiHelp.cjs.map +0 -1
- package/dist/ai/useAiHelp.js.map +0 -1
- package/dist/chunk-2PNSVBLQ.cjs +0 -54
- package/dist/chunk-2PNSVBLQ.cjs.map +0 -1
- package/dist/chunk-2TW6WLBC.js +0 -80
- package/dist/chunk-2TW6WLBC.js.map +0 -1
- package/dist/chunk-4ZZWZSWI.js +0 -61
- package/dist/chunk-4ZZWZSWI.js.map +0 -1
- package/dist/chunk-6N43WDVG.js +0 -29
- package/dist/chunk-6N43WDVG.js.map +0 -1
- package/dist/chunk-7ALBSPYR.cjs +0 -436
- package/dist/chunk-7ALBSPYR.cjs.map +0 -1
- package/dist/chunk-7NFY3M3W.js +0 -234
- package/dist/chunk-7NFY3M3W.js.map +0 -1
- package/dist/chunk-7SB2MJA4.js +0 -29
- package/dist/chunk-7SB2MJA4.js.map +0 -1
- package/dist/chunk-ABJJ3LII.js +0 -434
- package/dist/chunk-ABJJ3LII.js.map +0 -1
- package/dist/chunk-BIGGXHAW.cjs +0 -342
- package/dist/chunk-BIGGXHAW.cjs.map +0 -1
- package/dist/chunk-C7QZE5BI.js +0 -430
- package/dist/chunk-C7QZE5BI.js.map +0 -1
- package/dist/chunk-CWTUWNV3.cjs +0 -2937
- package/dist/chunk-CWTUWNV3.cjs.map +0 -1
- package/dist/chunk-DC4CQCK5.js +0 -915
- package/dist/chunk-DS6HN3YR.cjs +0 -496
- package/dist/chunk-DS6HN3YR.cjs.map +0 -1
- package/dist/chunk-FDQR4GAZ.js +0 -240
- package/dist/chunk-FDQR4GAZ.js.map +0 -1
- package/dist/chunk-FKGMSOJF.js +0 -394
- package/dist/chunk-FKGMSOJF.js.map +0 -1
- package/dist/chunk-G6PXJ2SA.js +0 -823
- package/dist/chunk-G6PXJ2SA.js.map +0 -1
- package/dist/chunk-HNLRVRQR.js +0 -335
- package/dist/chunk-HNLRVRQR.js.map +0 -1
- package/dist/chunk-J3C72RPK.cjs +0 -31
- package/dist/chunk-J3C72RPK.cjs.map +0 -1
- package/dist/chunk-K63VPFOV.cjs +0 -134
- package/dist/chunk-K63VPFOV.cjs.map +0 -1
- package/dist/chunk-KFLIDTDU.cjs +0 -825
- package/dist/chunk-KFLIDTDU.cjs.map +0 -1
- package/dist/chunk-KHKH5LCT.cjs +0 -250
- package/dist/chunk-KHKH5LCT.cjs.map +0 -1
- package/dist/chunk-LBEKRN6Y.cjs +0 -664
- package/dist/chunk-LBEKRN6Y.cjs.map +0 -1
- package/dist/chunk-LPUWXIPR.cjs +0 -578
- package/dist/chunk-LPUWXIPR.cjs.map +0 -1
- package/dist/chunk-MCO52PV5.js +0 -520
- package/dist/chunk-MCO52PV5.js.map +0 -1
- package/dist/chunk-MKXEM2C5.cjs +0 -396
- package/dist/chunk-MKXEM2C5.cjs.map +0 -1
- package/dist/chunk-MXNC5UNO.js +0 -563
- package/dist/chunk-MXNC5UNO.js.map +0 -1
- package/dist/chunk-NAK55BYU.cjs +0 -859
- package/dist/chunk-NAK55BYU.cjs.map +0 -1
- package/dist/chunk-NDBMKGQ5.cjs +0 -599
- package/dist/chunk-NDBMKGQ5.cjs.map +0 -1
- package/dist/chunk-OD65OZIW.cjs +0 -159
- package/dist/chunk-OD65OZIW.cjs.map +0 -1
- package/dist/chunk-ORT3KLGI.js +0 -50
- package/dist/chunk-ORT3KLGI.js.map +0 -1
- package/dist/chunk-PNNUPUH4.cjs +0 -82
- package/dist/chunk-PNNUPUH4.cjs.map +0 -1
- package/dist/chunk-PWHECW6N.js +0 -2934
- package/dist/chunk-PWHECW6N.js.map +0 -1
- package/dist/chunk-RBS5A2DF.js +0 -857
- package/dist/chunk-RBS5A2DF.js.map +0 -1
- package/dist/chunk-RPWUDXG5.cjs +0 -63
- package/dist/chunk-RPWUDXG5.cjs.map +0 -1
- package/dist/chunk-TCDJY3H6.js +0 -494
- package/dist/chunk-TCDJY3H6.js.map +0 -1
- package/dist/chunk-TP5UMRAU.cjs +0 -918
- package/dist/chunk-TP5UMRAU.cjs.map +0 -1
- package/dist/chunk-TS7J6DTY.js +0 -597
- package/dist/chunk-TS7J6DTY.js.map +0 -1
- package/dist/chunk-TUVENMXB.cjs +0 -20
- package/dist/chunk-TUVENMXB.cjs.map +0 -1
- package/dist/chunk-UDCQTOVU.cjs +0 -523
- package/dist/chunk-UDCQTOVU.cjs.map +0 -1
- package/dist/chunk-UHKZXOXD.js +0 -128
- package/dist/chunk-UHKZXOXD.js.map +0 -1
- package/dist/chunk-V6BA44DS.cjs +0 -236
- package/dist/chunk-V6BA44DS.cjs.map +0 -1
- package/dist/chunk-XO5R73Y4.js +0 -653
- package/dist/chunk-XO5R73Y4.js.map +0 -1
- package/dist/chunk-XOR4MPEN.cjs +0 -31
- package/dist/chunk-XOR4MPEN.cjs.map +0 -1
- package/dist/chunk-YORQQXGO.js +0 -155
- package/dist/chunk-YORQQXGO.js.map +0 -1
- package/dist/chunk-YY4PFR3Z.cjs +0 -432
- package/dist/chunk-YY4PFR3Z.cjs.map +0 -1
- package/dist/chunk-ZPI6Q42R.js +0 -17
- package/dist/chunk-ZPI6Q42R.js.map +0 -1
- package/dist/components/ActivityPreview.cjs.map +0 -1
- package/dist/components/ActivityPreview.js.map +0 -1
- package/dist/components/ActivitySequence.cjs.map +0 -1
- package/dist/components/ActivitySequence.js.map +0 -1
- package/dist/components/Dictation.cjs.map +0 -1
- package/dist/components/Dictation.js.map +0 -1
- package/dist/components/FillInTheBlanks.cjs.map +0 -1
- package/dist/components/FillInTheBlanks.js.map +0 -1
- package/dist/components/GapSelect.cjs.map +0 -1
- package/dist/components/GapSelect.js.map +0 -1
- package/dist/components/InteractiveVideo.cjs.map +0 -1
- package/dist/components/InteractiveVideo.js.map +0 -1
- package/dist/components/MultipleChoice.cjs.map +0 -1
- package/dist/components/MultipleChoice.js.map +0 -1
- package/dist/components/PronunciationFeedback.cjs.map +0 -1
- package/dist/components/PronunciationFeedback.js.map +0 -1
- package/dist/components/ReadAloud.cjs.map +0 -1
- package/dist/components/ReadAloud.js.map +0 -1
- package/dist/components/StimulusPanel.cjs.map +0 -1
- package/dist/components/StimulusPanel.js.map +0 -1
- package/dist/components/WrittenResponse.cjs.map +0 -1
- package/dist/components/WrittenResponse.js.map +0 -1
- package/dist/hooks/useSpeechRecorder.cjs.map +0 -1
- package/dist/hooks/useSpeechRecorder.js.map +0 -1
- package/dist/i18n/LkIntlProvider.cjs.map +0 -1
- package/dist/i18n/LkIntlProvider.js.map +0 -1
- package/dist/machine-CuwKqWXC.d.cts +0 -26
- package/dist/machine-CuwKqWXC.d.ts +0 -26
- package/dist/theme/ThemeProvider.cjs.map +0 -1
- package/dist/theme/ThemeProvider.js.map +0 -1
- package/dist/types-C4IsUQsK.d.ts +0 -989
- package/dist/types-CJgZWtl4.d.cts +0 -989
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLearnerAi } from "./ai/LkAiProvider.js";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { OPEN_DELIVERY_POLICY, aiAllowedByContent, aiCoachingRequest, aiExplanationRequest, aiHintRequest, aiSupports, aiWritingFeedbackRequest, checkAiCoaching, checkAiExplanation, checkAiHint, checkAiWritingFeedback, combineDeliveryPolicies } from "@intellectif/lk-core";
|
|
5
|
+
//#region src/components/shared/sequence-slot.ts
|
|
6
|
+
/** `null` outside a pager: a standalone component belongs to no slot. */
|
|
7
|
+
const SequenceSlotContext = createContext(null);
|
|
8
|
+
/**
|
|
9
|
+
* The last take number handed out. Module-level and never reset, so a number
|
|
10
|
+
* is unique for the life of the page: across mounts, across papers, and across
|
|
11
|
+
* two pagers side by side. A result can therefore only ever match the take that
|
|
12
|
+
* produced it, whatever slot and activity ids a later paper reuses — ids a host
|
|
13
|
+
* chooses, and can repeat, never decide it.
|
|
14
|
+
*/
|
|
15
|
+
let lastTake = 0;
|
|
16
|
+
/** A take number no other take on this page has had, or will have. */
|
|
17
|
+
function mintTake() {
|
|
18
|
+
lastTake += 1;
|
|
19
|
+
return lastTake;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The take each reported value belongs to. A `WeakMap` rather than a field, so
|
|
23
|
+
* a response or a result a host receives carries no extra key it would persist
|
|
24
|
+
* or compare, and the number never outlives the object.
|
|
25
|
+
*/
|
|
26
|
+
const takes = /* @__PURE__ */ new WeakMap();
|
|
27
|
+
/** Marks `value` as produced by take `take`, and returns it. */
|
|
28
|
+
function stampTake(value, take) {
|
|
29
|
+
takes.set(value, take);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/** The take `value` was produced by, or `undefined` when nothing stamped it. */
|
|
33
|
+
function takeOf(value) {
|
|
34
|
+
return typeof value === "object" && value !== null ? takes.get(value) : void 0;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/components/_internal.ts
|
|
38
|
+
/**
|
|
39
|
+
* Whether this is anything other than a production build — the one answer
|
|
40
|
+
* every development check and the error boundary's stack trace read.
|
|
41
|
+
*
|
|
42
|
+
* **The literal `process.env.NODE_ENV`, spelled exactly so.** It is the one
|
|
43
|
+
* expression bundlers replace at build time — Vite, webpack, esbuild, Next.js
|
|
44
|
+
* and Rollup's replace plugin all substitute it — and a browser has no
|
|
45
|
+
* `process` to read at run time. The reading this replaces,
|
|
46
|
+
* `globalThis.process?.env?.NODE_ENV`, is not that expression, so no bundler
|
|
47
|
+
* touched it, and in a browser it found no `process` and answered
|
|
48
|
+
* "development" in every production app: learners were shown a stack trace
|
|
49
|
+
* where each component's production fallback belonged. Tests running in Node
|
|
50
|
+
* never saw it, because Node has a `process`.
|
|
51
|
+
*
|
|
52
|
+
* This package's own build must leave the literal in its output, for the
|
|
53
|
+
* consumer's bundler to replace. A library build that substituted it would
|
|
54
|
+
* freeze every consumer into whichever mode the library was built in.
|
|
55
|
+
*
|
|
56
|
+
* The `try` covers code that reaches a runtime unbundled, with no `process`
|
|
57
|
+
* at all: that is development, as it always was.
|
|
58
|
+
*/
|
|
59
|
+
function isDevelopment() {
|
|
60
|
+
try {
|
|
61
|
+
return process.env.NODE_ENV !== "production";
|
|
62
|
+
} catch {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Activity components cannot know the learner's identity: `ActivityProps`
|
|
68
|
+
* carries none. They emit a structurally-valid statement with this anonymous
|
|
69
|
+
* actor; real identity is applied by the useXAPI/LRS layer (XAPIConfig.actor).
|
|
70
|
+
*/
|
|
71
|
+
const ANONYMOUS_ACTOR = {
|
|
72
|
+
objectType: "Agent",
|
|
73
|
+
account: {
|
|
74
|
+
homePage: "https://github.com/intellectif/learning-kit",
|
|
75
|
+
name: "anonymous"
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/** `data.id` is not an IRI; xAPI object ids must be. Wrap it as a URN. */
|
|
79
|
+
function objectIdFor(id) {
|
|
80
|
+
return `urn:learning-kit:activity:${encodeURIComponent(id)}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Random per-mount session id for the shuffle seed. `crypto.randomUUID` is
|
|
84
|
+
* unavailable outside secure contexts (plain-http LAN/staging hosts), so a
|
|
85
|
+
* Math.random fallback keeps the component from crashing there — the seed
|
|
86
|
+
* only randomises presentation order, so cryptographic strength is not needed.
|
|
87
|
+
*/
|
|
88
|
+
function randomSessionId() {
|
|
89
|
+
const c = globalThis.crypto;
|
|
90
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
91
|
+
return `s-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The `correct` flag of a detail stored before lk-core 0.3, which carries it in
|
|
95
|
+
* place of an `outcome`; `undefined` on any other detail. The type stopped
|
|
96
|
+
* describing it in 1.0, but a grade of record stored then still has it. On a
|
|
97
|
+
* blank, a gap or a word it meant the part was right; on a multiple-choice
|
|
98
|
+
* option, that the learner acted rightly on it.
|
|
99
|
+
*/
|
|
100
|
+
function legacyCorrect(detail) {
|
|
101
|
+
const { correct } = detail;
|
|
102
|
+
return typeof correct === "boolean" ? correct : void 0;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether a detail marks its blank, gap or word right: its `outcome`, or on a
|
|
106
|
+
* detail stored before lk-core 0.3, its `correct` flag.
|
|
107
|
+
*/
|
|
108
|
+
function detailIsRight(detail) {
|
|
109
|
+
const outcome = detail.outcome;
|
|
110
|
+
return outcome === void 0 ? legacyCorrect(detail) === true : outcome === "correct";
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* For the fallback of a dispatch over the built-in activity types — a
|
|
114
|
+
* `switch` default, or the end of an `if` chain: `value` has type `never`
|
|
115
|
+
* there only when every built-in type was handled above it, so a type lk-core
|
|
116
|
+
* gains fails to compile here until it has a component. At runtime it does
|
|
117
|
+
* nothing; what follows it decides what an unknown or custom type gets.
|
|
118
|
+
*/
|
|
119
|
+
function everyBuiltInTypeHandled(_value) {}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/ai/rules.ts
|
|
122
|
+
/**
|
|
123
|
+
* When AI help is offered, and what the SDK does with what a port answers.
|
|
124
|
+
*
|
|
125
|
+
* Internal: these are the rules behind {@link useAiHints} and
|
|
126
|
+
* {@link useAiExplanation}, which are what a host calls. They live apart from
|
|
127
|
+
* the hooks so the hooks' module exports nothing but the hooks themselves.
|
|
128
|
+
*/
|
|
129
|
+
/** The most hints a question gives when the host names no number, and the most it may name. */
|
|
130
|
+
const DEFAULT_MAX_HINTS = 3;
|
|
131
|
+
const MAX_HINTS_CEILING = 10;
|
|
132
|
+
/** A host's limit as a whole number from 1 to 10; anything else is the default, 3. */
|
|
133
|
+
function limitOf(asked) {
|
|
134
|
+
return typeof asked === "number" && Number.isInteger(asked) && asked >= 1 ? Math.min(asked, MAX_HINTS_CEILING) : DEFAULT_MAX_HINTS;
|
|
135
|
+
}
|
|
136
|
+
/** `maxHints` as a whole number from 1 to 10; anything else is the default. */
|
|
137
|
+
function hintLimit(ai) {
|
|
138
|
+
return limitOf(ai?.maxHints);
|
|
139
|
+
}
|
|
140
|
+
/** `maxWritingFeedback` as a whole number from 1 to 10; anything else is the default. */
|
|
141
|
+
function writingFeedbackLimit(ai) {
|
|
142
|
+
return limitOf(ai?.maxWritingFeedback);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether the learner may ask for feedback on a draft: `practice`, before
|
|
146
|
+
* submit, on a written response that is not disabled or redacted, whose author
|
|
147
|
+
* left explanations on. The paper's policy must show feedback and solutions —
|
|
148
|
+
* a correction is the right form beside a wrong one — and allow AI
|
|
149
|
+
* explanations, which writing feedback is: a model's words about the
|
|
150
|
+
* learner's own answer.
|
|
151
|
+
*/
|
|
152
|
+
function writingFeedbackOffered(input) {
|
|
153
|
+
const { data, renderMode, submitted, disabled } = input;
|
|
154
|
+
const delivery = input.delivery ?? OPEN_DELIVERY_POLICY;
|
|
155
|
+
return delivery.feedback && delivery.solutions && delivery.ai.explanations && renderMode === "practice" && !submitted && !disabled && data.redacted !== true && aiSupports(data.type, "writing-feedback") && aiAllowedByContent(data, "writing-feedback");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Whether the learner may ask for a hint: `practice`, before submit, on a
|
|
159
|
+
* question that is not disabled, of a type that takes hints, whose author left
|
|
160
|
+
* hints on, and whole — a redacted item is an exam's, and gives none.
|
|
161
|
+
*/
|
|
162
|
+
function hintOffered(input) {
|
|
163
|
+
const { data, renderMode, submitted, disabled } = input;
|
|
164
|
+
const delivery = input.delivery ?? OPEN_DELIVERY_POLICY;
|
|
165
|
+
return delivery.hints && delivery.ai.hints && renderMode === "practice" && !submitted && !disabled && data.redacted !== true && aiSupports(data.type, "hint") && aiAllowedByContent(data, "hint");
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Whether the learner may ask for an explanation: of a graded answer — in
|
|
169
|
+
* `practice` once submitted, or in `review` when the grade of record is
|
|
170
|
+
* scored — of a type the SDK explains, whose author left explanations on.
|
|
171
|
+
* Never in `exam`.
|
|
172
|
+
*/
|
|
173
|
+
function explanationOffered(input) {
|
|
174
|
+
const { data, renderMode, submitted, outcome } = input;
|
|
175
|
+
const delivery = input.delivery ?? OPEN_DELIVERY_POLICY;
|
|
176
|
+
if (!delivery.feedback || !delivery.solutions || !delivery.ai.explanations) return false;
|
|
177
|
+
return (renderMode === "practice" && submitted || renderMode === "review" && outcome?.status === "scored") && aiSupports(data.type, "explanation") && aiAllowedByContent(data, "explanation");
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Whether the learner may ask for coaching on a reading: one whose marks are
|
|
181
|
+
* on screen — which a read-aloud has only once it is graded, in `practice` or
|
|
182
|
+
* in `review` — never in `exam`, on a read-aloud whose author left
|
|
183
|
+
* explanations on. The paper's policy must show feedback, since the marks are
|
|
184
|
+
* feedback, and allow AI explanations, which coaching is: a model's words
|
|
185
|
+
* about the learner's own answer. Not `solutions`: a read-aloud hides no
|
|
186
|
+
* answer, and its text is on screen throughout.
|
|
187
|
+
*/
|
|
188
|
+
function coachingOffered(input) {
|
|
189
|
+
const { data, renderMode, marked } = input;
|
|
190
|
+
const delivery = input.delivery ?? OPEN_DELIVERY_POLICY;
|
|
191
|
+
return delivery.feedback && delivery.ai.explanations && renderMode !== "exam" && marked && aiSupports(data.type, "pronunciation-coaching") && aiAllowedByContent(data, "pronunciation-coaching");
|
|
192
|
+
}
|
|
193
|
+
/** Calls a port, turning a synchronous throw into a rejection. */
|
|
194
|
+
function callPort(call) {
|
|
195
|
+
try {
|
|
196
|
+
return Promise.resolve(call());
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return Promise.reject(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Tells a developer why a port's answer was not shown. Silent in production. */
|
|
202
|
+
function warnRefused(feature, activityId, reason) {
|
|
203
|
+
if (isDevelopment()) console.warn(`learning-kit: the AI ${feature} for "${activityId}" was not shown (${reason}). The learner saw "not available" instead.`);
|
|
204
|
+
}
|
|
205
|
+
/** The language to write in: the host's choice for the learner, else the interface language. */
|
|
206
|
+
const localeOf = (ai, locale) => {
|
|
207
|
+
const chosen = ai?.learnerLocale ?? locale;
|
|
208
|
+
return chosen === void 0 ? {} : { learnerLocale: chosen };
|
|
209
|
+
};
|
|
210
|
+
/** What an explanation is about: when any of it changes, the explanation shown no longer is. */
|
|
211
|
+
function explanationKey(renderMode, submitted, response, outcome) {
|
|
212
|
+
let graded = "";
|
|
213
|
+
if (outcome !== void 0) graded = `${outcome.status}:${"score" in outcome ? outcome.score : ""}`;
|
|
214
|
+
return `${renderMode}|${submitted}|${graded}|${JSON.stringify(response)}`;
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/ai/useAiHelp.ts
|
|
218
|
+
/**
|
|
219
|
+
* The mode a question is answered in: what the caller says, and what the paper
|
|
220
|
+
* around it says.
|
|
221
|
+
*
|
|
222
|
+
* A host drawing its own question is given `renderMode` like every other prop
|
|
223
|
+
* and is expected to pass it on, but nothing makes it — and a hook that took
|
|
224
|
+
* `practice` on trust would offer hints on a paper of record, as soon as an
|
|
225
|
+
* `LkAiProvider` sat anywhere above the pager. So an `exam` around the question
|
|
226
|
+
* wins, and where the caller says nothing the paper answers for it. A question
|
|
227
|
+
* standing on its own belongs to no paper, and is `practice` as before.
|
|
228
|
+
*/
|
|
229
|
+
function modeInForce(asked, around) {
|
|
230
|
+
if (around === "exam") return "exam";
|
|
231
|
+
return asked ?? around ?? "practice";
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* What the record of help a learner was shown carries beside the fact of it:
|
|
235
|
+
* who wrote it, and what the call cost — both only where the port sent them.
|
|
236
|
+
*
|
|
237
|
+
* The text is deliberately absent. A host that wants to keep what a learner
|
|
238
|
+
* read has it in its own port; an interaction is a record of what happened, and
|
|
239
|
+
* is kept for every learner and every question.
|
|
240
|
+
*/
|
|
241
|
+
function shownPayload(result) {
|
|
242
|
+
return {
|
|
243
|
+
...result.provenance !== void 0 ? { provenance: result.provenance } : {},
|
|
244
|
+
...result.usage !== void 0 ? { usage: result.usage } : {}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* "Explain my answer", without the SDK's own button and panel: the rules, the
|
|
249
|
+
* call, the checks and the record, for a question you draw yourself.
|
|
250
|
+
*
|
|
251
|
+
* It is what `<MultipleChoice>` and the others use, so a host's own question
|
|
252
|
+
* offers help on exactly the same terms: never in `exam`, never where the
|
|
253
|
+
* item's author switched explanations off, and never an explanation whose
|
|
254
|
+
* verdict contradicts the grade the SDK computed.
|
|
255
|
+
*
|
|
256
|
+
* ```tsx
|
|
257
|
+
* const { offered, status, explanation, ask } = useAiExplanation({
|
|
258
|
+
* data, response, submitted, renderMode, outcome, ai: question.ai,
|
|
259
|
+
* });
|
|
260
|
+
* if (!offered) return null;
|
|
261
|
+
* return explanation === null ? (
|
|
262
|
+
* <button type="button" onClick={ask} aria-busy={status === 'loading'}>Explain my answer</button>
|
|
263
|
+
* ) : (
|
|
264
|
+
* <section>
|
|
265
|
+
* <p>{explanation.text}</p>
|
|
266
|
+
* <p>Written by AI. It can make mistakes.</p>
|
|
267
|
+
* </section>
|
|
268
|
+
* );
|
|
269
|
+
* ```
|
|
270
|
+
*
|
|
271
|
+
* **Say who wrote it.** The SDK's own panel carries "Written by AI. It can make
|
|
272
|
+
* mistakes."; a page that drops that line passes a model's words off as the
|
|
273
|
+
* course's.
|
|
274
|
+
*
|
|
275
|
+
* When the answer or its grade changes, the explanation is dropped and a call
|
|
276
|
+
* still on its way is abandoned — a late explanation never lands beside an
|
|
277
|
+
* answer it was not written for — as it is when the caller unmounts.
|
|
278
|
+
*/
|
|
279
|
+
function useAiExplanation(input) {
|
|
280
|
+
const { data, response, submitted, outcome } = input;
|
|
281
|
+
const around = useContext(SequenceSlotContext);
|
|
282
|
+
const renderMode = modeInForce(input.renderMode, around?.renderMode);
|
|
283
|
+
const delivery = combineDeliveryPolicies(input.delivery, around?.delivery);
|
|
284
|
+
const ai = useLearnerAi(input.ai);
|
|
285
|
+
const resetKey = explanationKey(renderMode, submitted, response, outcome);
|
|
286
|
+
const [state, setState] = useState({
|
|
287
|
+
key: resetKey,
|
|
288
|
+
status: "idle"
|
|
289
|
+
});
|
|
290
|
+
const run = useRef(0);
|
|
291
|
+
const controller = useRef(null);
|
|
292
|
+
const current = state.key === resetKey ? state : {
|
|
293
|
+
key: resetKey,
|
|
294
|
+
status: "idle"
|
|
295
|
+
};
|
|
296
|
+
const offered = ai?.explain !== void 0 && explanationOffered({
|
|
297
|
+
data,
|
|
298
|
+
renderMode,
|
|
299
|
+
submitted,
|
|
300
|
+
outcome,
|
|
301
|
+
delivery
|
|
302
|
+
});
|
|
303
|
+
const latest = useRef({
|
|
304
|
+
input,
|
|
305
|
+
renderMode,
|
|
306
|
+
ai,
|
|
307
|
+
resetKey,
|
|
308
|
+
offered,
|
|
309
|
+
current
|
|
310
|
+
});
|
|
311
|
+
latest.current = {
|
|
312
|
+
input,
|
|
313
|
+
renderMode,
|
|
314
|
+
ai,
|
|
315
|
+
resetKey,
|
|
316
|
+
offered,
|
|
317
|
+
current
|
|
318
|
+
};
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
return () => {
|
|
321
|
+
run.current += 1;
|
|
322
|
+
controller.current?.abort();
|
|
323
|
+
controller.current = null;
|
|
324
|
+
};
|
|
325
|
+
}, [resetKey]);
|
|
326
|
+
const ask = useCallback(() => {
|
|
327
|
+
const now = latest.current;
|
|
328
|
+
const port = now.ai?.explain;
|
|
329
|
+
if (port === void 0 || !now.offered || now.current.status === "loading") return;
|
|
330
|
+
const key = now.resetKey;
|
|
331
|
+
const request = aiExplanationRequest({
|
|
332
|
+
data: now.input.data,
|
|
333
|
+
response: now.input.response,
|
|
334
|
+
...now.renderMode === "review" && now.input.outcome !== void 0 ? { outcome: now.input.outcome } : {},
|
|
335
|
+
...localeOf(now.ai, now.input.locale)
|
|
336
|
+
});
|
|
337
|
+
if (request === null) {
|
|
338
|
+
setState({
|
|
339
|
+
key,
|
|
340
|
+
status: "unavailable"
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
run.current += 1;
|
|
345
|
+
const mine = run.current;
|
|
346
|
+
controller.current?.abort();
|
|
347
|
+
const abort = new AbortController();
|
|
348
|
+
controller.current = abort;
|
|
349
|
+
setState({
|
|
350
|
+
key,
|
|
351
|
+
status: "loading"
|
|
352
|
+
});
|
|
353
|
+
callPort(() => port(request, { signal: abort.signal })).then((raw) => {
|
|
354
|
+
if (run.current !== mine) return;
|
|
355
|
+
const checked = checkAiExplanation(raw, request);
|
|
356
|
+
if (!checked.ok) {
|
|
357
|
+
warnRefused("explanation", now.input.data.id, checked.refusal);
|
|
358
|
+
setState({
|
|
359
|
+
key,
|
|
360
|
+
status: "unavailable"
|
|
361
|
+
});
|
|
362
|
+
latest.current.input.onInteraction?.({
|
|
363
|
+
type: "ai-help-refused",
|
|
364
|
+
activityId: now.input.data.id,
|
|
365
|
+
timestamp: Date.now(),
|
|
366
|
+
payload: {
|
|
367
|
+
feature: "explanation",
|
|
368
|
+
reason: checked.refusal
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
setState({
|
|
374
|
+
key,
|
|
375
|
+
status: "shown",
|
|
376
|
+
result: checked.result
|
|
377
|
+
});
|
|
378
|
+
latest.current.input.onInteraction?.({
|
|
379
|
+
type: "ai-explanation-shown",
|
|
380
|
+
activityId: now.input.data.id,
|
|
381
|
+
timestamp: Date.now(),
|
|
382
|
+
payload: shownPayload(checked.result)
|
|
383
|
+
});
|
|
384
|
+
}, () => {
|
|
385
|
+
if (run.current === mine) setState({
|
|
386
|
+
key,
|
|
387
|
+
status: "unavailable"
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}, []);
|
|
391
|
+
return {
|
|
392
|
+
offered,
|
|
393
|
+
status: current.status,
|
|
394
|
+
explanation: current.status === "shown" ? current.result : null,
|
|
395
|
+
ask
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* "Get a hint", without the SDK's own button and list: the rules, the call, the
|
|
400
|
+
* checks and the record, for a question you draw yourself.
|
|
401
|
+
*
|
|
402
|
+
* It is what `<MultipleChoice>` and the others use, so a host's own question
|
|
403
|
+
* gives hints on exactly the same terms: only in `practice` before submit, up
|
|
404
|
+
* to the host's limit, never on a redacted item or a type the SDK does not hint
|
|
405
|
+
* for, and never a hint that gives the answer away — which is refused rather
|
|
406
|
+
* than shown, and not counted.
|
|
407
|
+
*
|
|
408
|
+
* ```tsx
|
|
409
|
+
* const { offered, status, hints, used, limit, ask } = useAiHints({
|
|
410
|
+
* data, response, submitted, renderMode, ai: question.ai,
|
|
411
|
+
* });
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* **Say who wrote them**, as the SDK's own list does: "Written by AI. It can
|
|
415
|
+
* make mistakes."
|
|
416
|
+
*
|
|
417
|
+
* Keep the hints on screen after submit — the learner can see what they were
|
|
418
|
+
* given — while `offered` turns false. Another question starts with none.
|
|
419
|
+
*/
|
|
420
|
+
function useAiHints(input) {
|
|
421
|
+
const { data, submitted } = input;
|
|
422
|
+
const around = useContext(SequenceSlotContext);
|
|
423
|
+
const renderMode = modeInForce(input.renderMode, around?.renderMode);
|
|
424
|
+
const delivery = combineDeliveryPolicies(input.delivery, around?.delivery);
|
|
425
|
+
const disabled = input.disabled === true;
|
|
426
|
+
const ai = useLearnerAi(input.ai);
|
|
427
|
+
const resetKey = data.id;
|
|
428
|
+
const [state, setState] = useState({
|
|
429
|
+
key: resetKey,
|
|
430
|
+
hints: [],
|
|
431
|
+
status: "idle"
|
|
432
|
+
});
|
|
433
|
+
const run = useRef(0);
|
|
434
|
+
const controller = useRef(null);
|
|
435
|
+
const current = state.key === resetKey ? state : {
|
|
436
|
+
key: resetKey,
|
|
437
|
+
hints: [],
|
|
438
|
+
status: "idle"
|
|
439
|
+
};
|
|
440
|
+
const offered = ai?.hint !== void 0 && hintOffered({
|
|
441
|
+
data,
|
|
442
|
+
renderMode,
|
|
443
|
+
submitted,
|
|
444
|
+
disabled,
|
|
445
|
+
delivery
|
|
446
|
+
});
|
|
447
|
+
const limit = hintLimit(ai);
|
|
448
|
+
const latest = useRef({
|
|
449
|
+
input,
|
|
450
|
+
renderMode,
|
|
451
|
+
ai,
|
|
452
|
+
offered,
|
|
453
|
+
limit,
|
|
454
|
+
current
|
|
455
|
+
});
|
|
456
|
+
latest.current = {
|
|
457
|
+
input,
|
|
458
|
+
renderMode,
|
|
459
|
+
ai,
|
|
460
|
+
offered,
|
|
461
|
+
limit,
|
|
462
|
+
current
|
|
463
|
+
};
|
|
464
|
+
useEffect(() => {
|
|
465
|
+
return () => {
|
|
466
|
+
run.current += 1;
|
|
467
|
+
controller.current?.abort();
|
|
468
|
+
controller.current = null;
|
|
469
|
+
};
|
|
470
|
+
}, [resetKey]);
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
if (!offered) {
|
|
473
|
+
run.current += 1;
|
|
474
|
+
controller.current?.abort();
|
|
475
|
+
controller.current = null;
|
|
476
|
+
setState((previous) => previous.status === "loading" ? {
|
|
477
|
+
...previous,
|
|
478
|
+
status: "idle"
|
|
479
|
+
} : previous);
|
|
480
|
+
}
|
|
481
|
+
}, [offered]);
|
|
482
|
+
const ask = useCallback(() => {
|
|
483
|
+
const now = latest.current;
|
|
484
|
+
const port = now.ai?.hint;
|
|
485
|
+
const held = now.current;
|
|
486
|
+
if (port === void 0 || !now.offered || held.status === "loading" || held.hints.length >= now.limit) return;
|
|
487
|
+
const key = held.key;
|
|
488
|
+
const request = aiHintRequest({
|
|
489
|
+
data: now.input.data,
|
|
490
|
+
response: now.input.response,
|
|
491
|
+
previousHints: held.hints.map((hint) => hint.text),
|
|
492
|
+
...localeOf(now.ai, now.input.locale)
|
|
493
|
+
});
|
|
494
|
+
if (request === null) {
|
|
495
|
+
setState({
|
|
496
|
+
...held,
|
|
497
|
+
status: "unavailable"
|
|
498
|
+
});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
run.current += 1;
|
|
502
|
+
const mine = run.current;
|
|
503
|
+
controller.current?.abort();
|
|
504
|
+
const abort = new AbortController();
|
|
505
|
+
controller.current = abort;
|
|
506
|
+
setState({
|
|
507
|
+
...held,
|
|
508
|
+
status: "loading"
|
|
509
|
+
});
|
|
510
|
+
callPort(() => port(request, { signal: abort.signal })).then((raw) => {
|
|
511
|
+
if (run.current !== mine) return;
|
|
512
|
+
const checked = checkAiHint(raw, request);
|
|
513
|
+
if (!checked.ok) {
|
|
514
|
+
warnRefused("hint", now.input.data.id, checked.refusal);
|
|
515
|
+
setState((shown) => ({
|
|
516
|
+
...shown,
|
|
517
|
+
status: "unavailable"
|
|
518
|
+
}));
|
|
519
|
+
latest.current.input.onInteraction?.({
|
|
520
|
+
type: "ai-help-refused",
|
|
521
|
+
activityId: now.input.data.id,
|
|
522
|
+
timestamp: Date.now(),
|
|
523
|
+
payload: {
|
|
524
|
+
feature: "hint",
|
|
525
|
+
reason: checked.refusal,
|
|
526
|
+
hintNumber: request.hintNumber
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
setState((shown) => ({
|
|
532
|
+
...shown,
|
|
533
|
+
hints: shown.key === key ? [...shown.hints, checked.result] : shown.hints,
|
|
534
|
+
status: "idle"
|
|
535
|
+
}));
|
|
536
|
+
latest.current.input.onInteraction?.({
|
|
537
|
+
type: "ai-hint-shown",
|
|
538
|
+
activityId: now.input.data.id,
|
|
539
|
+
timestamp: Date.now(),
|
|
540
|
+
payload: {
|
|
541
|
+
hintNumber: request.hintNumber,
|
|
542
|
+
...shownPayload(checked.result)
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}, () => {
|
|
546
|
+
if (run.current === mine) setState((shown) => ({
|
|
547
|
+
...shown,
|
|
548
|
+
status: "unavailable"
|
|
549
|
+
}));
|
|
550
|
+
});
|
|
551
|
+
}, []);
|
|
552
|
+
return {
|
|
553
|
+
offered,
|
|
554
|
+
status: current.status,
|
|
555
|
+
hints: current.hints,
|
|
556
|
+
limit,
|
|
557
|
+
used: current.hints.length,
|
|
558
|
+
ask
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/** The text of a written response; another type's response is no draft at all. */
|
|
562
|
+
const draftOf = (response) => response.type === "written-response" ? response.text : "";
|
|
563
|
+
/**
|
|
564
|
+
* "Get feedback on my draft", without the SDK's own button and panel: the
|
|
565
|
+
* rules, the call, the checks and the record, for a written response you draw
|
|
566
|
+
* yourself.
|
|
567
|
+
*
|
|
568
|
+
* It is what `<WrittenResponse>` uses, so a host's own essay box gets feedback
|
|
569
|
+
* on the same terms: only in `practice` before submit, up to the host's limit,
|
|
570
|
+
* and never feedback that corrects words the learner did not write — which is
|
|
571
|
+
* refused rather than shown, and not counted.
|
|
572
|
+
*
|
|
573
|
+
* ```tsx
|
|
574
|
+
* const { offered, status, latest, current, used, limit, ask } = useAiWritingFeedback({
|
|
575
|
+
* data, response, submitted, renderMode, ai: question.ai,
|
|
576
|
+
* });
|
|
577
|
+
* ```
|
|
578
|
+
*
|
|
579
|
+
* **Say who wrote it**, as the SDK's own panel does: "Written by AI. It can make
|
|
580
|
+
* mistakes." And show `indicativeScore` as what it is — an indication, not a
|
|
581
|
+
* grade.
|
|
582
|
+
*/
|
|
583
|
+
function useAiWritingFeedback(input) {
|
|
584
|
+
const { data, submitted, response } = input;
|
|
585
|
+
const around = useContext(SequenceSlotContext);
|
|
586
|
+
const renderMode = modeInForce(input.renderMode, around?.renderMode);
|
|
587
|
+
const delivery = combineDeliveryPolicies(input.delivery, around?.delivery);
|
|
588
|
+
const disabled = input.disabled === true;
|
|
589
|
+
const ai = useLearnerAi(input.ai);
|
|
590
|
+
const resetKey = data.id;
|
|
591
|
+
const [state, setState] = useState({
|
|
592
|
+
key: resetKey,
|
|
593
|
+
shown: [],
|
|
594
|
+
status: "idle"
|
|
595
|
+
});
|
|
596
|
+
const run = useRef(0);
|
|
597
|
+
const controller = useRef(null);
|
|
598
|
+
const current = state.key === resetKey ? state : {
|
|
599
|
+
key: resetKey,
|
|
600
|
+
shown: [],
|
|
601
|
+
status: "idle"
|
|
602
|
+
};
|
|
603
|
+
const offered = ai?.writingFeedback !== void 0 && writingFeedbackOffered({
|
|
604
|
+
data,
|
|
605
|
+
renderMode,
|
|
606
|
+
submitted,
|
|
607
|
+
disabled,
|
|
608
|
+
delivery
|
|
609
|
+
});
|
|
610
|
+
const limit = writingFeedbackLimit(ai);
|
|
611
|
+
const latest = useRef({
|
|
612
|
+
input,
|
|
613
|
+
ai,
|
|
614
|
+
offered,
|
|
615
|
+
limit,
|
|
616
|
+
current
|
|
617
|
+
});
|
|
618
|
+
latest.current = {
|
|
619
|
+
input,
|
|
620
|
+
ai,
|
|
621
|
+
offered,
|
|
622
|
+
limit,
|
|
623
|
+
current
|
|
624
|
+
};
|
|
625
|
+
useEffect(() => {
|
|
626
|
+
return () => {
|
|
627
|
+
run.current += 1;
|
|
628
|
+
controller.current?.abort();
|
|
629
|
+
controller.current = null;
|
|
630
|
+
};
|
|
631
|
+
}, [resetKey]);
|
|
632
|
+
useEffect(() => {
|
|
633
|
+
if (!offered) {
|
|
634
|
+
run.current += 1;
|
|
635
|
+
controller.current?.abort();
|
|
636
|
+
controller.current = null;
|
|
637
|
+
setState((previous) => previous.status === "loading" ? {
|
|
638
|
+
...previous,
|
|
639
|
+
status: "idle"
|
|
640
|
+
} : previous);
|
|
641
|
+
}
|
|
642
|
+
}, [offered]);
|
|
643
|
+
const ask = useCallback(() => {
|
|
644
|
+
const now = latest.current;
|
|
645
|
+
const port = now.ai?.writingFeedback;
|
|
646
|
+
const held = now.current;
|
|
647
|
+
if (port === void 0 || !now.offered || held.status === "loading" || held.shown.length >= now.limit) return;
|
|
648
|
+
const key = held.key;
|
|
649
|
+
const about = draftOf(now.input.response);
|
|
650
|
+
const request = aiWritingFeedbackRequest({
|
|
651
|
+
data: now.input.data,
|
|
652
|
+
response: now.input.response,
|
|
653
|
+
previousFeedback: held.shown.map((one) => one.feedback.text),
|
|
654
|
+
...localeOf(now.ai, now.input.locale)
|
|
655
|
+
});
|
|
656
|
+
if (request === null) return;
|
|
657
|
+
run.current += 1;
|
|
658
|
+
const mine = run.current;
|
|
659
|
+
controller.current?.abort();
|
|
660
|
+
const abort = new AbortController();
|
|
661
|
+
controller.current = abort;
|
|
662
|
+
setState({
|
|
663
|
+
...held,
|
|
664
|
+
status: "loading"
|
|
665
|
+
});
|
|
666
|
+
callPort(() => port(request, { signal: abort.signal })).then((raw) => {
|
|
667
|
+
if (run.current !== mine) return;
|
|
668
|
+
const checked = checkAiWritingFeedback(raw, request);
|
|
669
|
+
if (!checked.ok) {
|
|
670
|
+
warnRefused("writing feedback", now.input.data.id, checked.refusal);
|
|
671
|
+
setState((was) => ({
|
|
672
|
+
...was,
|
|
673
|
+
status: "unavailable"
|
|
674
|
+
}));
|
|
675
|
+
latest.current.input.onInteraction?.({
|
|
676
|
+
type: "ai-help-refused",
|
|
677
|
+
activityId: now.input.data.id,
|
|
678
|
+
timestamp: Date.now(),
|
|
679
|
+
payload: {
|
|
680
|
+
feature: "writing-feedback",
|
|
681
|
+
reason: checked.refusal,
|
|
682
|
+
draftNumber: request.draftNumber
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const { feedback } = checked;
|
|
688
|
+
setState((was) => ({
|
|
689
|
+
...was,
|
|
690
|
+
shown: was.key === key ? [...was.shown, {
|
|
691
|
+
feedback,
|
|
692
|
+
about
|
|
693
|
+
}] : was.shown,
|
|
694
|
+
status: "idle"
|
|
695
|
+
}));
|
|
696
|
+
latest.current.input.onInteraction?.({
|
|
697
|
+
type: "ai-writing-feedback-shown",
|
|
698
|
+
activityId: now.input.data.id,
|
|
699
|
+
timestamp: Date.now(),
|
|
700
|
+
payload: {
|
|
701
|
+
draftNumber: request.draftNumber,
|
|
702
|
+
corrections: feedback.corrections.length,
|
|
703
|
+
...feedback.indicativeScore !== null ? { indicativeScore: feedback.indicativeScore } : {},
|
|
704
|
+
...shownPayload(feedback)
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}, () => {
|
|
708
|
+
if (run.current === mine) setState((was) => ({
|
|
709
|
+
...was,
|
|
710
|
+
status: "unavailable"
|
|
711
|
+
}));
|
|
712
|
+
});
|
|
713
|
+
}, []);
|
|
714
|
+
const last = current.shown[current.shown.length - 1];
|
|
715
|
+
return {
|
|
716
|
+
offered,
|
|
717
|
+
status: current.status,
|
|
718
|
+
feedback: current.shown.map((one) => one.feedback),
|
|
719
|
+
latest: last?.feedback ?? null,
|
|
720
|
+
current: last !== void 0 && last.about === draftOf(response),
|
|
721
|
+
limit,
|
|
722
|
+
used: current.shown.length,
|
|
723
|
+
ask
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* "Coach me on this reading", without the SDK's own button and panel: the
|
|
728
|
+
* rules, the call, the checks and the record, for marks you draw yourself.
|
|
729
|
+
*
|
|
730
|
+
* It is what `<PronunciationFeedback>` and `<ReadAloud>` use, so a host's own
|
|
731
|
+
* marks get coaching on the same terms: never in `exam`, never where the
|
|
732
|
+
* item's author switched explanations off, and never coaching on a word the
|
|
733
|
+
* engine did not mark or a sound it did not report — which is refused whole.
|
|
734
|
+
*
|
|
735
|
+
* ```tsx
|
|
736
|
+
* const { offered, status, coaching, ask } = useAiCoaching({
|
|
737
|
+
* data: item, assessment, grade, renderMode, ai,
|
|
738
|
+
* });
|
|
739
|
+
* ```
|
|
740
|
+
*
|
|
741
|
+
* **Say who wrote it**, as the SDK's own panel does: "Written by AI. It can make
|
|
742
|
+
* mistakes." The marks beside it are the engine's; the coaching is a model's.
|
|
743
|
+
*
|
|
744
|
+
* Another reading — other marks, another text — drops the coaching and
|
|
745
|
+
* abandons a call still on its way, as the caller unmounting does.
|
|
746
|
+
*/
|
|
747
|
+
function useAiCoaching(input) {
|
|
748
|
+
const { data, assessment, grade } = input;
|
|
749
|
+
const around = useContext(SequenceSlotContext);
|
|
750
|
+
const renderMode = modeInForce(input.renderMode, around?.renderMode);
|
|
751
|
+
const delivery = combineDeliveryPolicies(input.delivery, around?.delivery);
|
|
752
|
+
const ai = useLearnerAi(input.ai);
|
|
753
|
+
const { learnerLocale } = localeOf(ai, input.locale);
|
|
754
|
+
const { id, title, referenceText, locale: itemLocale, instructions, ai: permissions } = data;
|
|
755
|
+
const request = useMemo(() => {
|
|
756
|
+
return aiCoachingRequest({
|
|
757
|
+
data: {
|
|
758
|
+
type: "read-aloud",
|
|
759
|
+
id,
|
|
760
|
+
title,
|
|
761
|
+
referenceText,
|
|
762
|
+
locale: itemLocale,
|
|
763
|
+
instructions,
|
|
764
|
+
ai: permissions
|
|
765
|
+
},
|
|
766
|
+
...assessment !== void 0 && assessment !== null ? { assessment } : {},
|
|
767
|
+
...grade !== void 0 && grade !== null ? { grade } : {},
|
|
768
|
+
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
769
|
+
});
|
|
770
|
+
}, [
|
|
771
|
+
id,
|
|
772
|
+
title,
|
|
773
|
+
referenceText,
|
|
774
|
+
itemLocale,
|
|
775
|
+
instructions,
|
|
776
|
+
permissions,
|
|
777
|
+
assessment,
|
|
778
|
+
grade,
|
|
779
|
+
learnerLocale
|
|
780
|
+
]);
|
|
781
|
+
const take = typeof assessment?.recordingKey === "string" ? assessment.recordingKey : null;
|
|
782
|
+
const resetKey = useMemo(() => request === null ? "" : JSON.stringify([take, request]), [take, request]);
|
|
783
|
+
const [state, setState] = useState({
|
|
784
|
+
key: resetKey,
|
|
785
|
+
status: "idle"
|
|
786
|
+
});
|
|
787
|
+
const run = useRef(0);
|
|
788
|
+
const controller = useRef(null);
|
|
789
|
+
const current = state.key === resetKey ? state : {
|
|
790
|
+
key: resetKey,
|
|
791
|
+
status: "idle"
|
|
792
|
+
};
|
|
793
|
+
const offered = ai?.pronunciationCoaching !== void 0 && coachingOffered({
|
|
794
|
+
data: {
|
|
795
|
+
type: "read-aloud",
|
|
796
|
+
id,
|
|
797
|
+
title,
|
|
798
|
+
ai: permissions
|
|
799
|
+
},
|
|
800
|
+
renderMode,
|
|
801
|
+
marked: request !== null,
|
|
802
|
+
delivery
|
|
803
|
+
});
|
|
804
|
+
const latest = useRef({
|
|
805
|
+
input,
|
|
806
|
+
ai,
|
|
807
|
+
request,
|
|
808
|
+
resetKey,
|
|
809
|
+
offered,
|
|
810
|
+
current
|
|
811
|
+
});
|
|
812
|
+
latest.current = {
|
|
813
|
+
input,
|
|
814
|
+
ai,
|
|
815
|
+
request,
|
|
816
|
+
resetKey,
|
|
817
|
+
offered,
|
|
818
|
+
current
|
|
819
|
+
};
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
return () => {
|
|
822
|
+
run.current += 1;
|
|
823
|
+
controller.current?.abort();
|
|
824
|
+
controller.current = null;
|
|
825
|
+
};
|
|
826
|
+
}, [resetKey]);
|
|
827
|
+
useEffect(() => {
|
|
828
|
+
if (!offered) {
|
|
829
|
+
run.current += 1;
|
|
830
|
+
controller.current?.abort();
|
|
831
|
+
controller.current = null;
|
|
832
|
+
setState((previous) => previous.status === "loading" ? {
|
|
833
|
+
key: previous.key,
|
|
834
|
+
status: "idle"
|
|
835
|
+
} : previous);
|
|
836
|
+
}
|
|
837
|
+
}, [offered]);
|
|
838
|
+
const ask = useCallback(() => {
|
|
839
|
+
const now = latest.current;
|
|
840
|
+
const port = now.ai?.pronunciationCoaching;
|
|
841
|
+
const request = now.request;
|
|
842
|
+
if (port === void 0 || !now.offered || request === null || now.current.status === "loading" || now.current.status === "shown") return;
|
|
843
|
+
const key = now.resetKey;
|
|
844
|
+
const activityId = request.facts.activityId;
|
|
845
|
+
run.current += 1;
|
|
846
|
+
const mine = run.current;
|
|
847
|
+
controller.current?.abort();
|
|
848
|
+
const abort = new AbortController();
|
|
849
|
+
controller.current = abort;
|
|
850
|
+
setState({
|
|
851
|
+
key,
|
|
852
|
+
status: "loading"
|
|
853
|
+
});
|
|
854
|
+
callPort(() => port(request, { signal: abort.signal })).then((raw) => {
|
|
855
|
+
if (run.current !== mine) return;
|
|
856
|
+
const checked = checkAiCoaching(raw, request);
|
|
857
|
+
if (!checked.ok) {
|
|
858
|
+
warnRefused("pronunciation coaching", activityId, checked.refusal);
|
|
859
|
+
setState({
|
|
860
|
+
key,
|
|
861
|
+
status: "unavailable"
|
|
862
|
+
});
|
|
863
|
+
latest.current.input.onInteraction?.({
|
|
864
|
+
type: "ai-help-refused",
|
|
865
|
+
activityId,
|
|
866
|
+
timestamp: Date.now(),
|
|
867
|
+
payload: {
|
|
868
|
+
feature: "pronunciation-coaching",
|
|
869
|
+
reason: checked.refusal
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const { coaching } = checked;
|
|
875
|
+
setState({
|
|
876
|
+
key,
|
|
877
|
+
status: "shown",
|
|
878
|
+
coaching
|
|
879
|
+
});
|
|
880
|
+
latest.current.input.onInteraction?.({
|
|
881
|
+
type: "ai-coaching-shown",
|
|
882
|
+
activityId,
|
|
883
|
+
timestamp: Date.now(),
|
|
884
|
+
payload: {
|
|
885
|
+
words: coaching.words.length,
|
|
886
|
+
...shownPayload(coaching)
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}, () => {
|
|
890
|
+
if (run.current === mine) setState({
|
|
891
|
+
key,
|
|
892
|
+
status: "unavailable"
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
}, []);
|
|
896
|
+
return {
|
|
897
|
+
offered,
|
|
898
|
+
status: offered ? current.status : "idle",
|
|
899
|
+
coaching: offered && current.status === "shown" ? current.coaching : null,
|
|
900
|
+
ask
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
//#endregion
|
|
904
|
+
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
|
+
|
|
906
|
+
//# sourceMappingURL=useAiHelp-B-ccDL5Q.js.map
|