@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,1092 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { a as ANONYMOUS_ACTOR, c as isDevelopment, f as SequenceSlotContext, m as stampTake, o as detailIsRight, p as mintTake, u as objectIdFor } from "./useAiHelp-B-ccDL5Q.js";
|
|
3
|
+
import { i as useLkStrings } from "./LkIntlProvider-D_FdYXjQ.js";
|
|
4
|
+
import { t as ActivityErrorBoundary } from "./ActivityErrorBoundary-Dw6SbWv3.js";
|
|
5
|
+
import { useActivityState } from "./hooks/useActivityState.js";
|
|
6
|
+
import { n as localeDirectionOf } from "./direction-Dq0_HtA5.js";
|
|
7
|
+
import { t as ActivityMedia } from "./ActivityMedia-vDiP-Psa.js";
|
|
8
|
+
import { i as useDeliveryPolicy, t as FeedbackRegion } from "./FeedbackRegion-CyPSp8bA.js";
|
|
9
|
+
import { t as useSpeechRecorder } from "./useSpeechRecorder-zIi9Dlw8.js";
|
|
10
|
+
import { a as percentOfGrade, c as readOutcome, i as fractionOfGrade, l as usePlaybackRefusal, n as VISUALLY_HIDDEN, o as readAssessResult, r as markSentence, s as readEvidence, t as PronunciationFeedback, u as ReadingCoaching } from "./PronunciationFeedback-Dxd6XPkO.js";
|
|
11
|
+
import { useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { ActivitySchemaError, READ_ALOUD_MAX_SECONDS, READ_ALOUD_MAX_TAKES, assertRedacted, validateActivity, xAPIBuilder, xapiDefinitionFor } from "@intellectif/lk-core";
|
|
14
|
+
//#region src/components/shared/capture-registry.ts
|
|
15
|
+
/**
|
|
16
|
+
* Microphone captures that must stop when the region holding them goes away,
|
|
17
|
+
* keyed by group name.
|
|
18
|
+
*
|
|
19
|
+
* Module-level for the reason `playback-group.ts` is: the thing that knows a
|
|
20
|
+
* pane was hidden — the sequence — is not the thing holding the microphone,
|
|
21
|
+
* and a context would have to be threaded through every activity component to
|
|
22
|
+
* reach one. The caller mints the group name per region; a sequence uses the
|
|
23
|
+
* slot's, so hiding one slot stops that slot's capture and no other's. The name
|
|
24
|
+
* reaches a recorder only through `SequenceSlotContext`, never as a prop, so no
|
|
25
|
+
* consumer string can join another region's group.
|
|
26
|
+
*
|
|
27
|
+
* This **supplements** a recorder's own teardown and never replaces it. The
|
|
28
|
+
* sequence's pane effect runs only on a `hidden` transition, not on unmount,
|
|
29
|
+
* and neither a preview nor a standalone render has panes at all — so a
|
|
30
|
+
* component that records still releases the microphone from its own cleanup,
|
|
31
|
+
* and this registry only covers the case where the component is left mounted
|
|
32
|
+
* and merely hidden.
|
|
33
|
+
*
|
|
34
|
+
* No `useCaptureGroup` hook, unlike playback: that one re-registers after every
|
|
35
|
+
* commit because the element behind a ref can be replaced while the ref object
|
|
36
|
+
* is not, whereas a recorder's `stop` keeps one identity for the life of the
|
|
37
|
+
* hook and a plain effect registers it exactly once.
|
|
38
|
+
*/
|
|
39
|
+
const groups = /* @__PURE__ */ new Map();
|
|
40
|
+
/** Adds a capture's stop to a group; returns the matching removal. */
|
|
41
|
+
function joinCaptureGroup(group, stop) {
|
|
42
|
+
let members = groups.get(group);
|
|
43
|
+
if (members === void 0) {
|
|
44
|
+
members = /* @__PURE__ */ new Set();
|
|
45
|
+
groups.set(group, members);
|
|
46
|
+
}
|
|
47
|
+
members.add(stop);
|
|
48
|
+
return () => {
|
|
49
|
+
const current = groups.get(group);
|
|
50
|
+
if (current === void 0) return;
|
|
51
|
+
current.delete(stop);
|
|
52
|
+
if (current.size === 0) groups.delete(group);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Stops every capture in the group.
|
|
57
|
+
*
|
|
58
|
+
* The members are copied before they are called. Each `stop` is expected to
|
|
59
|
+
* run its own removal, and a component that answers one by starting again
|
|
60
|
+
* would otherwise be stopped by the sweep that its own stop provoked.
|
|
61
|
+
*/
|
|
62
|
+
function stopCaptureGroup(group) {
|
|
63
|
+
const members = groups.get(group);
|
|
64
|
+
if (members === void 0) return;
|
|
65
|
+
for (const stop of [...members]) stop();
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/components/ReadAloud/recording-bounds.ts
|
|
69
|
+
const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
70
|
+
/**
|
|
71
|
+
* The one reading of an item's `recording`: every bound the component captures
|
|
72
|
+
* under, every sentence that names one, and the key that decides whether a new
|
|
73
|
+
* `data` is a new reading all come from here, so no two of them can read the
|
|
74
|
+
* same field differently.
|
|
75
|
+
*
|
|
76
|
+
* `practice` renders unvalidated content in production, so what comes in may
|
|
77
|
+
* be anything a server stored. What comes out is always a set of bounds the
|
|
78
|
+
* schema accepts, and bounds the schema accepts come out unchanged. Three
|
|
79
|
+
* rules decide the rest, and each errs toward a take the learner can finish:
|
|
80
|
+
*
|
|
81
|
+
* - **A present bound that cannot be read is the schema's own ceiling**, never
|
|
82
|
+
* no bound at all: a `maxSeconds` of `"20"` or `NaN` records up to
|
|
83
|
+
* `READ_ALOUD_MAX_SECONDS`, and a `maxTakes` of `"1"` allows
|
|
84
|
+
* `READ_ALOUD_MAX_TAKES`. Unreadable data can make the recorder stricter
|
|
85
|
+
* than the ceiling, never looser: `maxTakes: "1"` used to mean unlimited
|
|
86
|
+
* takes. `null` is an absent optional, as it is everywhere a host sends JSON.
|
|
87
|
+
* - **A count is a whole number in range**: `0` and `1.5` read as `1`, `999`
|
|
88
|
+
* as the ceiling.
|
|
89
|
+
* - **A minimum the maximum cannot reach is no minimum.** A `minSeconds` of 30
|
|
90
|
+
* under a `maxSeconds` of 20 stopped every take at 20 and then refused it as
|
|
91
|
+
* too short, so the item could never be answered. The schema refuses that
|
|
92
|
+
* pair; a reading that honoured it would leave the learner a dead end.
|
|
93
|
+
*/
|
|
94
|
+
function readRecordingBounds(recording) {
|
|
95
|
+
const bounds = typeof recording === "object" && recording !== null ? recording : {};
|
|
96
|
+
const maxSeconds = isNumber(bounds.maxSeconds) && bounds.maxSeconds > 0 ? Math.min(bounds.maxSeconds, READ_ALOUD_MAX_SECONDS) : READ_ALOUD_MAX_SECONDS;
|
|
97
|
+
const minSeconds = isNumber(bounds.minSeconds) && bounds.minSeconds >= 0 && bounds.minSeconds < maxSeconds ? bounds.minSeconds : void 0;
|
|
98
|
+
const takes = bounds.maxTakes;
|
|
99
|
+
return {
|
|
100
|
+
maxSeconds,
|
|
101
|
+
minSeconds,
|
|
102
|
+
maxTakes: takes === void 0 || takes === null ? void 0 : isNumber(takes) ? Math.min(Math.max(1, Math.floor(takes)), READ_ALOUD_MAX_TAKES) : READ_ALOUD_MAX_TAKES
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A length of audio as the whole seconds a sentence says beside `maxSeconds`,
|
|
107
|
+
* never more than it. A bound may be fractional — the schema allows 2.5 — and
|
|
108
|
+
* rounding a take that ran to it said "3 of 2.5 seconds". Rounded to the
|
|
109
|
+
* nearest second for a finished take, and down for the counter of one still
|
|
110
|
+
* running, which should not reach its bound before the take does.
|
|
111
|
+
*/
|
|
112
|
+
function secondsWithin(milliseconds, maxSeconds, rounding) {
|
|
113
|
+
const seconds = milliseconds / 1e3;
|
|
114
|
+
return Math.min(Math.max(0, rounding === "nearest" ? Math.round(seconds) : Math.floor(seconds)), maxSeconds);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/components/ReadAloud/ReadAloud.tsx
|
|
118
|
+
/**
|
|
119
|
+
* Names playback groups for the model recordings. Per mount and never derived
|
|
120
|
+
* from `useId`: two separately hydrated roots derive the same id for the same
|
|
121
|
+
* tree position, and their recordings would pause each other. The name never
|
|
122
|
+
* reaches the DOM.
|
|
123
|
+
*/
|
|
124
|
+
let modelsGroupCount = 0;
|
|
125
|
+
/** A title with something in it a screen reader can say. */
|
|
126
|
+
const VISIBLE_TEXT_RE = /[^\p{White_Space}\p{Cc}\p{Cf}\p{Default_Ignorable_Code_Point}]/u;
|
|
127
|
+
/** A stored mark's id, as `gradeReadAloud` numbers the reference words. */
|
|
128
|
+
const MARK_ID_RE = /^w\d+$/;
|
|
129
|
+
/** The states a mark rebuilt from stored details can carry: insertions are not stored. */
|
|
130
|
+
const STORED_STATES = [
|
|
131
|
+
"correct",
|
|
132
|
+
"mispronounced",
|
|
133
|
+
"omitted"
|
|
134
|
+
];
|
|
135
|
+
/** The level meter's resolution, in steps the skin can select on. */
|
|
136
|
+
const LEVEL_STEPS = 10;
|
|
137
|
+
/**
|
|
138
|
+
* Whether the screen offers "Try again" for the take this phase describes —
|
|
139
|
+
* the one answer to that question, read by the button that offers it and by
|
|
140
|
+
* the take state a pager is told, so the two cannot disagree. A retry re-sends
|
|
141
|
+
* the SAME take: a take that failed to upload is sent again, and a stored take
|
|
142
|
+
* whose assessment failed is judged again without being uploaded twice.
|
|
143
|
+
*/
|
|
144
|
+
function offersRetry(phase, uploaded) {
|
|
145
|
+
return phase.kind === "upload-failed" || phase.kind === "assess-failed" && phase.retryable && uploaded !== null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The state of the phase's take, as a pager deciding whether its set is
|
|
149
|
+
* finished reads it. Derived from the phase alone, so no path through a submit
|
|
150
|
+
* can leave a pager waiting on a take this component has finished with: every
|
|
151
|
+
* path ends in a phase.
|
|
152
|
+
*/
|
|
153
|
+
function takeStateOf(phase, uploaded) {
|
|
154
|
+
if (phase.kind === "uploading" || phase.kind === "assessing") return "in-flight";
|
|
155
|
+
return offersRetry(phase, uploaded) ? "retryable" : "settled";
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Whether the recorder is capturing, or about to: the one answer to "is a take
|
|
159
|
+
* being recorded", read by everything that must hold still for one. A
|
|
160
|
+
* permission prompt counts — a prompt answered after the pane went away would
|
|
161
|
+
* otherwise open a microphone nobody is in front of, and a model recording
|
|
162
|
+
* played while the prompt is up would be charged a play and then silenced.
|
|
163
|
+
*/
|
|
164
|
+
function capturing(status) {
|
|
165
|
+
return status === "recording" || status === "requesting-permission";
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* What a take is a take OF: the item, the words, their language and the bounds
|
|
169
|
+
* the take is captured under. A change to any of these is a different reading,
|
|
170
|
+
* and a take of the old one answers nothing.
|
|
171
|
+
*
|
|
172
|
+
* Deliberately NOT the identity of `data`. `redact()` returns a new object on
|
|
173
|
+
* every call, and `activities={raw.map(redact)}` is a documented way to pass a
|
|
174
|
+
* paper, so a host that rebuilds its items on every render is ordinary — and a
|
|
175
|
+
* reset keyed on identity silently threw away every grade still in flight and
|
|
176
|
+
* refunded the take budget on each of those renders, so a `maxTakes: 1` exam
|
|
177
|
+
* accepted a second take. The title, the instructions and the model recordings
|
|
178
|
+
* are presentation; editing them leaves the take a take of the same reading.
|
|
179
|
+
*
|
|
180
|
+
* The bounds are the CLAMPED ones, which are what the recorder is given. Read
|
|
181
|
+
* field by field into strings, so no value unvalidated data carries can make
|
|
182
|
+
* the key throw.
|
|
183
|
+
*/
|
|
184
|
+
function readingKeyOf(data, bounds) {
|
|
185
|
+
const text = (value) => typeof value === "string" ? value : null;
|
|
186
|
+
return JSON.stringify([
|
|
187
|
+
text(data.id),
|
|
188
|
+
text(data.referenceText),
|
|
189
|
+
text(data.locale),
|
|
190
|
+
String(bounds.maxSeconds),
|
|
191
|
+
String(bounds.minSeconds),
|
|
192
|
+
String(bounds.maxTakes)
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The slower model recording as an `ActivityMedia`: it follows the model
|
|
197
|
+
* recording's playback policy minus `maxPlays` (`controls`, `seek`, `rate`,
|
|
198
|
+
* the native hints), so a locked scrubber or a fixed speed binds both files —
|
|
199
|
+
* and it never carries a budget of its own, so `<ActivityMedia>` never demands
|
|
200
|
+
* a binding for it. `undefined` when there is no slow recording.
|
|
201
|
+
*/
|
|
202
|
+
function slowMediaFor(data, label) {
|
|
203
|
+
const slow = data.slowMedia;
|
|
204
|
+
if (slow === void 0) return;
|
|
205
|
+
const { maxPlays: _budget, ...inherited } = data.media?.playback ?? {};
|
|
206
|
+
const hasPolicy = Object.values(inherited).some((value) => value !== void 0);
|
|
207
|
+
return {
|
|
208
|
+
type: "audio",
|
|
209
|
+
url: slow.url,
|
|
210
|
+
alt: slow.alt ?? label,
|
|
211
|
+
...hasPolicy ? { playback: inherited } : {}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Word marks rebuilt from stored scoring details — a server-recorded mark shown
|
|
216
|
+
* after the fact, with no assessment on the client. Inserted words are not in
|
|
217
|
+
* the details (`gradeReadAloud` stores one mark per REFERENCE word, and a word
|
|
218
|
+
* the learner added is not one the item asked for), so none appear.
|
|
219
|
+
*
|
|
220
|
+
* The details are a read grade's, so every entry is well-formed; what is left
|
|
221
|
+
* to decide here is whether they are word marks at all, rather than a rubric's
|
|
222
|
+
* own breakdown.
|
|
223
|
+
*/
|
|
224
|
+
function marksFromDetails(details) {
|
|
225
|
+
if (details.length === 0 || !details.every((detail) => MARK_ID_RE.test(detail.itemId))) return null;
|
|
226
|
+
return details.map((detail) => {
|
|
227
|
+
const heard = Array.isArray(detail.learnerResponse) ? detail.learnerResponse.join(" ") : detail.learnerResponse;
|
|
228
|
+
const reference = Array.isArray(detail.correctResponse) ? detail.correctResponse.join(" ") : detail.correctResponse;
|
|
229
|
+
const state = detailIsRight(detail) ? "correct" : detail.outcome === "incorrect-omission" ? "omitted" : "mispronounced";
|
|
230
|
+
return {
|
|
231
|
+
itemId: detail.itemId,
|
|
232
|
+
reference,
|
|
233
|
+
heard,
|
|
234
|
+
state,
|
|
235
|
+
...typeof detail.score === "number" ? { accuracy: detail.score * 100 } : {}
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** The stored take a learner response points at, or `null` for a blank or another type. */
|
|
240
|
+
function recordingOf(response) {
|
|
241
|
+
return response !== void 0 && response.type === "read-aloud" ? response.recording : null;
|
|
242
|
+
}
|
|
243
|
+
/** The response this component emits; `takes` is written only when one was made. */
|
|
244
|
+
function responseOf(recording, takes) {
|
|
245
|
+
return {
|
|
246
|
+
type: "read-aloud",
|
|
247
|
+
recording,
|
|
248
|
+
...takes > 0 ? { takes } : {}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Which learner message an unscorable code earns. The two codes that mean
|
|
253
|
+
* "nothing was heard" send the learner back to a quieter room; every other one,
|
|
254
|
+
* the SDK's and an application's own alike — and no code at all — says only
|
|
255
|
+
* that this take could not be assessed. **The code itself is never shown** — it
|
|
256
|
+
* is a developer's label.
|
|
257
|
+
*/
|
|
258
|
+
function unscorableMessage(code, s) {
|
|
259
|
+
return code === "no_speech" || code === "insufficient_voiced_time" ? s.readAloudNotHeard : s.readAloudNotAssessed;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The announcement, with grader feedback in its own isolated span: its
|
|
263
|
+
* direction is its own, so an English "Well read." in an Arabic interface keeps
|
|
264
|
+
* its full stop at its end. Its language is not guessed — feedback may be
|
|
265
|
+
* written in the interface's language or the reading's.
|
|
266
|
+
*/
|
|
267
|
+
function AnnouncementText({ announcement }) {
|
|
268
|
+
if (announcement === null) return null;
|
|
269
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [announcement.text, announcement.feedback ? /* @__PURE__ */ jsxs(Fragment$1, { children: [" ", /* @__PURE__ */ jsx("span", {
|
|
270
|
+
dir: "auto",
|
|
271
|
+
children: announcement.feedback
|
|
272
|
+
})] }) : null] });
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The server's outcome in `review`, as `readOutcome` read it. Nothing on this
|
|
276
|
+
* path scores, infers or defaults a grade.
|
|
277
|
+
*
|
|
278
|
+
* `scoreShown` says whether the grade is already on screen — it is, whenever
|
|
279
|
+
* `<PronunciationFeedback>` was given the same grade — so the sentence is not
|
|
280
|
+
* rendered twice. The wrapper and its attributes are rendered either way,
|
|
281
|
+
* because they are what a stylesheet and a diagnostic read.
|
|
282
|
+
*/
|
|
283
|
+
function OutcomeSummary({ reading, s, scoreShown }) {
|
|
284
|
+
switch (reading.kind) {
|
|
285
|
+
case "graded": return /* @__PURE__ */ jsx("div", {
|
|
286
|
+
className: "lk-ra-outcome",
|
|
287
|
+
"data-status": reading.status,
|
|
288
|
+
"data-passed": String(reading.grade.passed),
|
|
289
|
+
children: scoreShown ? null : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
|
|
290
|
+
className: "lk-ra-grade",
|
|
291
|
+
children: s.scoreAnnouncement(percentOfGrade(reading.grade), reading.grade.passed)
|
|
292
|
+
}), reading.grade.feedback ? /* @__PURE__ */ jsx("p", {
|
|
293
|
+
className: "lk-ra-grade-feedback",
|
|
294
|
+
dir: "auto",
|
|
295
|
+
children: reading.grade.feedback
|
|
296
|
+
}) : null] })
|
|
297
|
+
});
|
|
298
|
+
case "deferred": return /* @__PURE__ */ jsx("div", {
|
|
299
|
+
className: "lk-ra-outcome",
|
|
300
|
+
"data-status": "deferred",
|
|
301
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
302
|
+
className: "lk-ra-grade",
|
|
303
|
+
children: s.awaitingGrade
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
case "unscorable": return /* @__PURE__ */ jsx("div", {
|
|
307
|
+
className: "lk-ra-outcome",
|
|
308
|
+
"data-status": "unscorable",
|
|
309
|
+
...reading.code !== void 0 ? { "data-code": reading.code } : {},
|
|
310
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
311
|
+
className: "lk-ra-grade",
|
|
312
|
+
children: s.couldNotBeGraded
|
|
313
|
+
})
|
|
314
|
+
});
|
|
315
|
+
default: return /* @__PURE__ */ jsx("div", {
|
|
316
|
+
className: "lk-ra-outcome",
|
|
317
|
+
...reading.status !== void 0 ? { "data-status": reading.status } : {},
|
|
318
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
319
|
+
className: "lk-ra-grade",
|
|
320
|
+
children: s.couldNotBeGraded
|
|
321
|
+
})
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Reads a text aloud and hands the recording to the application.
|
|
327
|
+
*
|
|
328
|
+
* The SDK records; it never judges. A take is captured as mono 16-bit PCM WAV
|
|
329
|
+
* at 16 kHz — the one format `inspectWav` measures directly — handed to
|
|
330
|
+
* `recordingBinding.upload`, and reported through `onSubmit`. Whether anything
|
|
331
|
+
* assesses it, and where, is the application's: in `practice` an optional
|
|
332
|
+
* `assess` may come back with a grade, and in `exam` nothing is ever assessed
|
|
333
|
+
* on the client.
|
|
334
|
+
*
|
|
335
|
+
* **`recordingBinding.upload` is required outside `review`** and its absence
|
|
336
|
+
* throws at render, in production too: a learner must never speak into a
|
|
337
|
+
* control that stores nothing. `assess` is not required — a binding without it
|
|
338
|
+
* records, uploads and submits, and shows a notice in place of the feedback.
|
|
339
|
+
*
|
|
340
|
+
* **A grade that comes back is read, not trusted** — whether `assess` returned
|
|
341
|
+
* it or `outcome` carries it. A score that is not a number between 0 and its
|
|
342
|
+
* maximum, or a `passed` that is not a boolean, is shown as could-not-be-graded
|
|
343
|
+
* and never reaches `onComplete` or a statement; `onComplete` fires exactly when
|
|
344
|
+
* a score is on screen, with that score.
|
|
345
|
+
*
|
|
346
|
+
* **AI coaching**, with a `pronunciationCoaching` port: under the marks, a
|
|
347
|
+
* button asks a model to explain them — in `practice` once a take is graded,
|
|
348
|
+
* and in `review` wherever marks are shown, from kept evidence or from the
|
|
349
|
+
* stored grade's details. Never in `exam`. The marks are the engine's, and
|
|
350
|
+
* coaching on a word it did not mark is refused whole.
|
|
351
|
+
*
|
|
352
|
+
* **Under a strict Content-Security-Policy** the take is played back from a
|
|
353
|
+
* `blob:` URL, so `media-src` must allow `blob:` — refused, the player and the
|
|
354
|
+
* per-word buttons give way to a note saying the take cannot be played here —
|
|
355
|
+
* and the capture module loads from one too, so either `script-src` allows
|
|
356
|
+
* `blob:` or `workletUrl` names a self-hosted copy of `CAPTURE_PROCESSOR_SOURCE`.
|
|
357
|
+
*
|
|
358
|
+
* ```tsx
|
|
359
|
+
* <ReadAloud
|
|
360
|
+
* data={item}
|
|
361
|
+
* recordingBinding={{ upload, assess }}
|
|
362
|
+
* onSubmit={persist}
|
|
363
|
+
* onComplete={record}
|
|
364
|
+
* />
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
function ReadAloud$1({ data, onComplete, onSubmit, onInteraction, value, defaultValue, defaultSubmitted, renderMode = "practice", outcome, mediaBudget, mediaStrings, strings, theme, locale, disabled, recordingBinding, assessment, breakThreshold, monotoneThreshold, workletUrl, delivery, ai }) {
|
|
368
|
+
const isExam = renderMode === "exam";
|
|
369
|
+
const policy = useDeliveryPolicy(delivery);
|
|
370
|
+
const isReview = renderMode === "review";
|
|
371
|
+
const s = useLkStrings(strings);
|
|
372
|
+
const slot = useContext(SequenceSlotContext);
|
|
373
|
+
const captureGroup = slot?.captureGroup;
|
|
374
|
+
const devError = useMemo(() => {
|
|
375
|
+
if (!isDevelopment()) return null;
|
|
376
|
+
if (data.redacted === true) try {
|
|
377
|
+
assertRedacted(data);
|
|
378
|
+
return null;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
381
|
+
}
|
|
382
|
+
const result = validateActivity("read-aloud", data);
|
|
383
|
+
return result.success ? null : new ActivitySchemaError("read-aloud", result.errors);
|
|
384
|
+
}, [data]);
|
|
385
|
+
const { maxSeconds, minSeconds, maxTakes } = readRecordingBounds(data.recording);
|
|
386
|
+
const readingKey = readingKeyOf(data, {
|
|
387
|
+
maxSeconds,
|
|
388
|
+
minSeconds,
|
|
389
|
+
maxTakes
|
|
390
|
+
});
|
|
391
|
+
const recorder = useSpeechRecorder(useMemo(() => ({
|
|
392
|
+
maxDurationMs: maxSeconds * 1e3,
|
|
393
|
+
...minSeconds !== void 0 ? { minDurationMs: minSeconds * 1e3 } : {},
|
|
394
|
+
...maxTakes !== void 0 ? { maxTakes } : {},
|
|
395
|
+
...workletUrl !== void 0 ? { workletUrl } : {}
|
|
396
|
+
}), [
|
|
397
|
+
maxSeconds,
|
|
398
|
+
minSeconds,
|
|
399
|
+
maxTakes,
|
|
400
|
+
workletUrl
|
|
401
|
+
]));
|
|
402
|
+
const { state, start, complete, getTimeSpent, reset } = useActivityState(defaultSubmitted === true ? "completed" : "idle");
|
|
403
|
+
const [phase, setPhase] = useState({ kind: "idle" });
|
|
404
|
+
const [uploaded, setUploaded] = useState(null);
|
|
405
|
+
const [takeUrl, setTakeUrl] = useState(null);
|
|
406
|
+
const [playbackUrl, setPlaybackUrl] = useState(null);
|
|
407
|
+
const [errorNonce, setErrorNonce] = useState(0);
|
|
408
|
+
const titleId = `${useId()}-title`;
|
|
409
|
+
const [modelsGroup] = useState(() => {
|
|
410
|
+
modelsGroupCount += 1;
|
|
411
|
+
return `lk-ra-models-${modelsGroupCount}`;
|
|
412
|
+
});
|
|
413
|
+
const interactionRef = useRef(onInteraction);
|
|
414
|
+
interactionRef.current = onInteraction;
|
|
415
|
+
const bindingRef = useRef(recordingBinding);
|
|
416
|
+
bindingRef.current = recordingBinding;
|
|
417
|
+
const submittingRef = useRef(false);
|
|
418
|
+
const runRef = useRef(0);
|
|
419
|
+
const slotRef = useRef(slot);
|
|
420
|
+
slotRef.current = slot;
|
|
421
|
+
const reportedTakeRef = useRef(null);
|
|
422
|
+
useEffect(() => () => {
|
|
423
|
+
runRef.current += 1;
|
|
424
|
+
const take = reportedTakeRef.current;
|
|
425
|
+
if (take !== null) slotRef.current?.takeState(take, "settled");
|
|
426
|
+
}, []);
|
|
427
|
+
const fire = useCallback((type, payload) => {
|
|
428
|
+
interactionRef.current?.({
|
|
429
|
+
type,
|
|
430
|
+
activityId: data.id,
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
payload
|
|
433
|
+
});
|
|
434
|
+
}, [data.id]);
|
|
435
|
+
const lastStatusRef = useRef(recorder.status);
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
if (lastStatusRef.current === recorder.status) return;
|
|
438
|
+
lastStatusRef.current = recorder.status;
|
|
439
|
+
if (recorder.status === "recording") fire("recording-started", {
|
|
440
|
+
takes: recorder.takesUsed,
|
|
441
|
+
durationMs: 0
|
|
442
|
+
});
|
|
443
|
+
else if (recorder.status === "recorded" && recorder.take !== null) fire("recording-stopped", {
|
|
444
|
+
takes: recorder.takesUsed,
|
|
445
|
+
durationMs: recorder.take.durationMs
|
|
446
|
+
});
|
|
447
|
+
}, [
|
|
448
|
+
recorder.status,
|
|
449
|
+
recorder.take,
|
|
450
|
+
recorder.takesUsed,
|
|
451
|
+
fire
|
|
452
|
+
]);
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (recorder.error !== null) setErrorNonce((nonce) => nonce + 1);
|
|
455
|
+
}, [recorder.error]);
|
|
456
|
+
const take = recorder.take;
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
if (take === null || typeof URL.createObjectURL !== "function") return;
|
|
459
|
+
const url = URL.createObjectURL(take.blob);
|
|
460
|
+
setTakeUrl(url);
|
|
461
|
+
return () => {
|
|
462
|
+
URL.revokeObjectURL(url);
|
|
463
|
+
setTakeUrl(null);
|
|
464
|
+
};
|
|
465
|
+
}, [take]);
|
|
466
|
+
const restored = recordingOf(value ?? defaultValue);
|
|
467
|
+
const restoredKey = restored?.key ?? null;
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
const binding = bindingRef.current;
|
|
470
|
+
if (!isReview || restored === null || binding?.playbackUrl === void 0) return;
|
|
471
|
+
let live = true;
|
|
472
|
+
Promise.resolve(binding.playbackUrl(restored)).then((url) => {
|
|
473
|
+
if (live) setPlaybackUrl(url);
|
|
474
|
+
}, () => {});
|
|
475
|
+
return () => {
|
|
476
|
+
live = false;
|
|
477
|
+
};
|
|
478
|
+
}, [
|
|
479
|
+
isReview,
|
|
480
|
+
restoredKey,
|
|
481
|
+
readingKey
|
|
482
|
+
]);
|
|
483
|
+
const statusRef = useRef(recorder.status);
|
|
484
|
+
statusRef.current = recorder.status;
|
|
485
|
+
const releaseMicrophone = useCallback(() => {
|
|
486
|
+
if (capturing(statusRef.current)) recorder.discard();
|
|
487
|
+
}, [recorder.discard]);
|
|
488
|
+
useEffect(() => {
|
|
489
|
+
if (captureGroup === void 0) return;
|
|
490
|
+
return joinCaptureGroup(captureGroup, releaseMicrophone);
|
|
491
|
+
}, [captureGroup, releaseMicrophone]);
|
|
492
|
+
const defaultSubmittedRef = useRef(defaultSubmitted);
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
defaultSubmittedRef.current = defaultSubmitted;
|
|
495
|
+
}, [defaultSubmitted]);
|
|
496
|
+
const lastReadingRef = useRef(readingKey);
|
|
497
|
+
useEffect(() => {
|
|
498
|
+
if (lastReadingRef.current === readingKey) return;
|
|
499
|
+
lastReadingRef.current = readingKey;
|
|
500
|
+
submittingRef.current = false;
|
|
501
|
+
runRef.current += 1;
|
|
502
|
+
setPhase({ kind: "idle" });
|
|
503
|
+
setUploaded(null);
|
|
504
|
+
setPlaybackUrl(null);
|
|
505
|
+
recorder.reset();
|
|
506
|
+
reset(defaultSubmittedRef.current === true ? "completed" : "idle");
|
|
507
|
+
}, [
|
|
508
|
+
readingKey,
|
|
509
|
+
reset,
|
|
510
|
+
recorder.reset
|
|
511
|
+
]);
|
|
512
|
+
const phaseTake = phase.kind === "idle" ? null : phase.take;
|
|
513
|
+
const phaseTakeState = takeStateOf(phase, uploaded);
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
const take = phaseTake ?? reportedTakeRef.current;
|
|
516
|
+
if (slot === null || take === null) return;
|
|
517
|
+
reportedTakeRef.current = take;
|
|
518
|
+
slot.takeState(take, phaseTakeState);
|
|
519
|
+
}, [
|
|
520
|
+
slot,
|
|
521
|
+
phaseTake,
|
|
522
|
+
phaseTakeState
|
|
523
|
+
]);
|
|
524
|
+
const modelsRef = useRef(null);
|
|
525
|
+
const isCapturing = capturing(recorder.status);
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (!isCapturing) return;
|
|
528
|
+
const node = modelsRef.current;
|
|
529
|
+
if (node === null) return;
|
|
530
|
+
const hush = () => {
|
|
531
|
+
for (const media of node.querySelectorAll("audio, video")) {
|
|
532
|
+
const element = media;
|
|
533
|
+
if (!element.paused) element.pause();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
hush();
|
|
537
|
+
node.addEventListener("play", hush, true);
|
|
538
|
+
return () => {
|
|
539
|
+
node.removeEventListener("play", hush, true);
|
|
540
|
+
};
|
|
541
|
+
}, [isCapturing]);
|
|
542
|
+
const slowMedia = useMemo(() => slowMediaFor(data, s.readAloudSlowRecording), [data, s.readAloudSlowRecording]);
|
|
543
|
+
const takeAudio = isReview ? playbackUrl : takeUrl;
|
|
544
|
+
const playback = usePlaybackRefusal(takeAudio);
|
|
545
|
+
const submitRef = useRef(null);
|
|
546
|
+
if (devError) throw devError;
|
|
547
|
+
if (!isReview && recordingBinding?.upload === void 0) throw new Error(`ReadAloud "${data.id}" was rendered in renderMode "${renderMode}" without \`recordingBinding.upload\`. The learner could record a take that is stored nowhere and submitted as nothing, which looks like a working activity the whole way through. Pass a \`recordingBinding\` whose \`upload\` puts the take in your storage and returns its key. \`assess\` is optional: without it the take is still recorded, uploaded and submitted, and a notice replaces the feedback.`);
|
|
548
|
+
const submitted = state === "completed";
|
|
549
|
+
const inactive = disabled === true || submitted || isReview;
|
|
550
|
+
const busy = phase.kind === "uploading" || phase.kind === "assessing";
|
|
551
|
+
const takesLeft = maxTakes === void 0 ? void 0 : Math.max(0, maxTakes - recorder.takesUsed);
|
|
552
|
+
/** Marks a failure so the alert announces even when the sentence repeats. */
|
|
553
|
+
const raise = (next) => {
|
|
554
|
+
setErrorNonce((nonce) => nonce + 1);
|
|
555
|
+
setPhase(next);
|
|
556
|
+
};
|
|
557
|
+
/**
|
|
558
|
+
* Tells the pager slot, if there is one, that `take` is on its way — before
|
|
559
|
+
* anything is awaited. The report a pager's set waits on must exist before
|
|
560
|
+
* the first result it waits for can arrive; the take's later states follow
|
|
561
|
+
* from the phase it reaches (see the effect above).
|
|
562
|
+
*/
|
|
563
|
+
const beginTakeReport = (take) => {
|
|
564
|
+
reportedTakeRef.current = take;
|
|
565
|
+
slot?.takeState(take, "in-flight");
|
|
566
|
+
};
|
|
567
|
+
const runAssess = async (ref, current, take) => {
|
|
568
|
+
const assess = bindingRef.current?.assess;
|
|
569
|
+
const durationMs = current?.durationMs ?? 0;
|
|
570
|
+
const takes = recorder.takesUsed;
|
|
571
|
+
const run = runRef.current;
|
|
572
|
+
if (assess === void 0) {
|
|
573
|
+
setPhase({
|
|
574
|
+
kind: "unassessed",
|
|
575
|
+
take
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
setPhase({
|
|
580
|
+
kind: "assessing",
|
|
581
|
+
take
|
|
582
|
+
});
|
|
583
|
+
fire("assessment-requested", {
|
|
584
|
+
takes,
|
|
585
|
+
durationMs
|
|
586
|
+
});
|
|
587
|
+
let answer;
|
|
588
|
+
try {
|
|
589
|
+
answer = await assess(ref);
|
|
590
|
+
} catch {
|
|
591
|
+
if (run !== runRef.current) return;
|
|
592
|
+
raise({
|
|
593
|
+
kind: "assess-failed",
|
|
594
|
+
take,
|
|
595
|
+
retryable: true
|
|
596
|
+
});
|
|
597
|
+
fire("assessment-failed", {
|
|
598
|
+
takes,
|
|
599
|
+
durationMs,
|
|
600
|
+
retryable: true
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (run !== runRef.current) return;
|
|
605
|
+
const result = readAssessResult(answer);
|
|
606
|
+
if (result.status === "failed") {
|
|
607
|
+
raise({
|
|
608
|
+
kind: "assess-failed",
|
|
609
|
+
take,
|
|
610
|
+
retryable: result.retryable
|
|
611
|
+
});
|
|
612
|
+
fire("assessment-failed", {
|
|
613
|
+
takes,
|
|
614
|
+
durationMs,
|
|
615
|
+
retryable: result.retryable
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (result.status === "unscorable") {
|
|
620
|
+
setPhase({
|
|
621
|
+
kind: "unscorable",
|
|
622
|
+
take,
|
|
623
|
+
code: result.code
|
|
624
|
+
});
|
|
625
|
+
fire("assessment-failed", {
|
|
626
|
+
takes,
|
|
627
|
+
durationMs,
|
|
628
|
+
retryable: true,
|
|
629
|
+
...result.code !== void 0 ? { code: result.code } : {}
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const timeSpent = getTimeSpent();
|
|
634
|
+
setPhase({
|
|
635
|
+
kind: "graded",
|
|
636
|
+
take,
|
|
637
|
+
assessment: result.assessment,
|
|
638
|
+
grade: result.grade
|
|
639
|
+
});
|
|
640
|
+
const grade = result.grade;
|
|
641
|
+
if (grade === null) {
|
|
642
|
+
fire("assessment-failed", {
|
|
643
|
+
takes,
|
|
644
|
+
durationMs,
|
|
645
|
+
retryable: true
|
|
646
|
+
});
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
onComplete?.(stampTake({
|
|
650
|
+
score: grade.score,
|
|
651
|
+
maxScore: grade.maxScore,
|
|
652
|
+
passed: grade.passed,
|
|
653
|
+
timeSpent,
|
|
654
|
+
xapiStatement: xAPIBuilder.buildAnsweredStatement({
|
|
655
|
+
actor: ANONYMOUS_ACTOR,
|
|
656
|
+
object: {
|
|
657
|
+
id: objectIdFor(data.id),
|
|
658
|
+
name: { [data.locale ?? "en-US"]: data.title },
|
|
659
|
+
...xapiDefinitionFor(data)
|
|
660
|
+
},
|
|
661
|
+
timeSpentMs: timeSpent,
|
|
662
|
+
response: result.recognizedText ?? "",
|
|
663
|
+
scoringResult: {
|
|
664
|
+
score: fractionOfGrade(grade),
|
|
665
|
+
maxScore: 1,
|
|
666
|
+
passed: grade.passed,
|
|
667
|
+
feedback: grade.feedback,
|
|
668
|
+
details: grade.details ?? []
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
}, take));
|
|
672
|
+
};
|
|
673
|
+
const submitTake = async (current) => {
|
|
674
|
+
const binding = bindingRef.current;
|
|
675
|
+
if (inactive || submittingRef.current || binding === void 0) return;
|
|
676
|
+
submittingRef.current = true;
|
|
677
|
+
const takes = recorder.takesUsed;
|
|
678
|
+
const run = runRef.current;
|
|
679
|
+
const take = mintTake();
|
|
680
|
+
beginTakeReport(take);
|
|
681
|
+
try {
|
|
682
|
+
let ref = null;
|
|
683
|
+
if (current !== null) {
|
|
684
|
+
setPhase({
|
|
685
|
+
kind: "uploading",
|
|
686
|
+
take
|
|
687
|
+
});
|
|
688
|
+
try {
|
|
689
|
+
ref = await binding.upload(current);
|
|
690
|
+
} catch {
|
|
691
|
+
if (run !== runRef.current) return;
|
|
692
|
+
raise({
|
|
693
|
+
kind: "upload-failed",
|
|
694
|
+
take
|
|
695
|
+
});
|
|
696
|
+
fire("recording-upload-failed", {
|
|
697
|
+
takes,
|
|
698
|
+
durationMs: current.durationMs,
|
|
699
|
+
retryable: true
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (run !== runRef.current) return;
|
|
704
|
+
setUploaded(ref);
|
|
705
|
+
fire("recording-uploaded", {
|
|
706
|
+
takes,
|
|
707
|
+
durationMs: current.durationMs
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
onSubmit?.(stampTake(responseOf(ref, takes), take));
|
|
711
|
+
fire("submitted", {
|
|
712
|
+
takes,
|
|
713
|
+
durationMs: current?.durationMs ?? 0
|
|
714
|
+
});
|
|
715
|
+
if (isExam || ref === null) {
|
|
716
|
+
complete();
|
|
717
|
+
setPhase({
|
|
718
|
+
kind: "submitted",
|
|
719
|
+
take
|
|
720
|
+
});
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
await runAssess(ref, current, take);
|
|
724
|
+
} finally {
|
|
725
|
+
submittingRef.current = false;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
/**
|
|
729
|
+
* Judges the take that is already stored, again — the SAME take, so the same
|
|
730
|
+
* number. Guarded like a submit: an assessment costs an application money,
|
|
731
|
+
* and two presses of one retry must not buy two of them.
|
|
732
|
+
*/
|
|
733
|
+
const retryAssess = async (ref, take) => {
|
|
734
|
+
if (submittingRef.current) return;
|
|
735
|
+
submittingRef.current = true;
|
|
736
|
+
beginTakeReport(take);
|
|
737
|
+
try {
|
|
738
|
+
await runAssess(ref, recorder.take, take);
|
|
739
|
+
} finally {
|
|
740
|
+
submittingRef.current = false;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
const handleSubmit = (event) => {
|
|
744
|
+
event.preventDefault();
|
|
745
|
+
if (inactive || busy || recorder.take === null || phase.kind !== "idle") return;
|
|
746
|
+
submitTake(recorder.take);
|
|
747
|
+
};
|
|
748
|
+
const beginTake = () => {
|
|
749
|
+
if (inactive || busy || !recorder.canRecord) return;
|
|
750
|
+
if (recorder.take !== null) fire("recording-discarded", {
|
|
751
|
+
takes: recorder.takesUsed,
|
|
752
|
+
durationMs: recorder.take.durationMs
|
|
753
|
+
});
|
|
754
|
+
setPhase({ kind: "idle" });
|
|
755
|
+
setUploaded(null);
|
|
756
|
+
start();
|
|
757
|
+
recorder.start();
|
|
758
|
+
};
|
|
759
|
+
const contentLang = typeof data.locale === "string" ? data.locale : void 0;
|
|
760
|
+
const contentDir = localeDirectionOf(data.locale);
|
|
761
|
+
const coachingItem = {
|
|
762
|
+
id: data.id,
|
|
763
|
+
title: data.title,
|
|
764
|
+
referenceText: data.referenceText,
|
|
765
|
+
locale: data.locale,
|
|
766
|
+
...data.instructions !== void 0 ? { instructions: data.instructions } : {},
|
|
767
|
+
...data.ai !== void 0 ? { ai: data.ai } : {}
|
|
768
|
+
};
|
|
769
|
+
const outcomeReading = useMemo(() => readOutcome(outcome), [outcome]);
|
|
770
|
+
const keptEvidence = useMemo(() => readEvidence(assessment), [assessment]);
|
|
771
|
+
const storedGrade = outcomeReading?.kind === "graded" && outcomeReading.status === "graded" ? outcomeReading.grade : null;
|
|
772
|
+
const storedMarks = useMemo(() => policy.feedback && isReview && keptEvidence === null && storedGrade?.details !== void 0 ? marksFromDetails(storedGrade.details) : null, [
|
|
773
|
+
policy.feedback,
|
|
774
|
+
isReview,
|
|
775
|
+
keptEvidence,
|
|
776
|
+
storedGrade
|
|
777
|
+
]);
|
|
778
|
+
const feedbackAssessment = !policy.feedback ? null : isReview ? keptEvidence : phase.kind === "graded" ? phase.assessment : null;
|
|
779
|
+
const feedbackGrade = isReview ? storedGrade : phase.kind === "graded" ? phase.grade : null;
|
|
780
|
+
const feedbackAudio = playback.refused ? null : takeAudio;
|
|
781
|
+
const rootRef = useRef(null);
|
|
782
|
+
const [panelLostTheGrade, setPanelLostTheGrade] = useState(false);
|
|
783
|
+
const panelHasGrade = feedbackAssessment !== null && feedbackGrade !== null;
|
|
784
|
+
const gradeShownInPanel = panelHasGrade && !panelLostTheGrade;
|
|
785
|
+
useEffect(() => {
|
|
786
|
+
const root = rootRef.current;
|
|
787
|
+
const lost = panelHasGrade && (root === null || root.querySelector(".lk-pf-grade") === null);
|
|
788
|
+
setPanelLostTheGrade((was) => was === lost ? was : lost);
|
|
789
|
+
});
|
|
790
|
+
const announcement = useMemo(() => {
|
|
791
|
+
if (isReview) return null;
|
|
792
|
+
if (recorder.status === "recording") return {
|
|
793
|
+
text: s.readAloudRecordingStarted,
|
|
794
|
+
feedback: null
|
|
795
|
+
};
|
|
796
|
+
const stopped = recorder.status === "recorded" && take !== null ? {
|
|
797
|
+
text: s.readAloudRecordingStopped(secondsWithin(take.durationMs, maxSeconds, "nearest"), maxSeconds),
|
|
798
|
+
feedback: null
|
|
799
|
+
} : null;
|
|
800
|
+
if (isExam) {
|
|
801
|
+
if (phase.kind === "submitted" || submitted) return {
|
|
802
|
+
text: s.answerSubmitted,
|
|
803
|
+
feedback: null
|
|
804
|
+
};
|
|
805
|
+
return phase.kind === "idle" ? stopped : null;
|
|
806
|
+
}
|
|
807
|
+
switch (phase.kind) {
|
|
808
|
+
case "graded": {
|
|
809
|
+
const grade = phase.grade;
|
|
810
|
+
if (grade === null) return {
|
|
811
|
+
text: `${s.answerSubmitted} ${s.couldNotBeGraded}`,
|
|
812
|
+
feedback: null
|
|
813
|
+
};
|
|
814
|
+
if (!policy.feedback) return {
|
|
815
|
+
text: s.answerSubmitted,
|
|
816
|
+
feedback: null
|
|
817
|
+
};
|
|
818
|
+
return {
|
|
819
|
+
text: `${s.answerSubmitted} ${s.scoreAnnouncement(percentOfGrade(grade), grade.passed)}`,
|
|
820
|
+
feedback: grade.feedback
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
case "unscorable": return {
|
|
824
|
+
text: unscorableMessage(phase.code, s),
|
|
825
|
+
feedback: null
|
|
826
|
+
};
|
|
827
|
+
case "unassessed": return {
|
|
828
|
+
text: s.readAloudAssessmentUnavailable,
|
|
829
|
+
feedback: null
|
|
830
|
+
};
|
|
831
|
+
case "idle": return stopped;
|
|
832
|
+
default: return null;
|
|
833
|
+
}
|
|
834
|
+
}, [
|
|
835
|
+
isReview,
|
|
836
|
+
isExam,
|
|
837
|
+
phase,
|
|
838
|
+
submitted,
|
|
839
|
+
s,
|
|
840
|
+
recorder.status,
|
|
841
|
+
take,
|
|
842
|
+
maxSeconds,
|
|
843
|
+
policy.feedback
|
|
844
|
+
]);
|
|
845
|
+
const alertText = recorder.error !== null ? s.readAloudRecorderError(recorder.error) : phase.kind === "upload-failed" ? s.readAloudUploadFailed : phase.kind === "assess-failed" ? s.readAloudAssessmentFailed : null;
|
|
846
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
847
|
+
className: "lk-ra",
|
|
848
|
+
ref: rootRef,
|
|
849
|
+
"aria-labelledby": VISIBLE_TEXT_RE.test(data.title) ? titleId : void 0,
|
|
850
|
+
lang: locale,
|
|
851
|
+
"data-render-mode": renderMode,
|
|
852
|
+
style: theme,
|
|
853
|
+
onSubmit: handleSubmit,
|
|
854
|
+
children: [
|
|
855
|
+
/* @__PURE__ */ jsx("p", {
|
|
856
|
+
id: titleId,
|
|
857
|
+
className: "lk-ra-title",
|
|
858
|
+
dir: contentDir ?? "auto",
|
|
859
|
+
lang: contentLang,
|
|
860
|
+
children: data.title
|
|
861
|
+
}),
|
|
862
|
+
typeof data.instructions === "string" && data.instructions !== "" ? /* @__PURE__ */ jsx("p", {
|
|
863
|
+
className: "lk-ra-instructions",
|
|
864
|
+
dir: contentDir ?? "auto",
|
|
865
|
+
lang: contentLang,
|
|
866
|
+
children: data.instructions
|
|
867
|
+
}) : null,
|
|
868
|
+
/* @__PURE__ */ jsx("p", {
|
|
869
|
+
className: "lk-ra-reference",
|
|
870
|
+
dir: contentDir ?? "auto",
|
|
871
|
+
lang: contentLang,
|
|
872
|
+
children: data.referenceText
|
|
873
|
+
}),
|
|
874
|
+
data.media !== void 0 || slowMedia !== void 0 ? /* @__PURE__ */ jsxs("div", {
|
|
875
|
+
className: "lk-ra-models",
|
|
876
|
+
ref: modelsRef,
|
|
877
|
+
children: [data.media !== void 0 ? /* @__PURE__ */ jsxs("fieldset", {
|
|
878
|
+
className: "lk-ra-model",
|
|
879
|
+
children: [/* @__PURE__ */ jsx("legend", {
|
|
880
|
+
className: "lk-ra-model-label",
|
|
881
|
+
children: s.readAloudModelRecording
|
|
882
|
+
}), /* @__PURE__ */ jsx(ActivityMedia, {
|
|
883
|
+
media: data.media,
|
|
884
|
+
renderMode,
|
|
885
|
+
playbackGroup: modelsGroup,
|
|
886
|
+
...mediaBudget !== void 0 ? { mediaBudget } : {},
|
|
887
|
+
...mediaStrings !== void 0 ? { mediaStrings } : {},
|
|
888
|
+
...strings !== void 0 ? { strings } : {},
|
|
889
|
+
...onInteraction !== void 0 ? { onInteraction } : {},
|
|
890
|
+
...locale !== void 0 ? { locale } : {},
|
|
891
|
+
...disabled === true || isCapturing ? { disabled: true } : {}
|
|
892
|
+
})]
|
|
893
|
+
}) : null, slowMedia !== void 0 ? /* @__PURE__ */ jsxs("fieldset", {
|
|
894
|
+
className: "lk-ra-model",
|
|
895
|
+
"data-slow": "true",
|
|
896
|
+
children: [/* @__PURE__ */ jsx("legend", {
|
|
897
|
+
className: "lk-ra-model-label",
|
|
898
|
+
children: s.readAloudSlowRecording
|
|
899
|
+
}), /* @__PURE__ */ jsx(ActivityMedia, {
|
|
900
|
+
media: slowMedia,
|
|
901
|
+
renderMode,
|
|
902
|
+
playbackGroup: modelsGroup,
|
|
903
|
+
...mediaStrings !== void 0 ? { mediaStrings } : {},
|
|
904
|
+
...strings !== void 0 ? { strings } : {},
|
|
905
|
+
...locale !== void 0 ? { locale } : {},
|
|
906
|
+
...disabled === true || isCapturing ? { disabled: true } : {}
|
|
907
|
+
})]
|
|
908
|
+
}) : null]
|
|
909
|
+
}) : null,
|
|
910
|
+
isReview || submitted ? null : /* @__PURE__ */ jsxs("div", {
|
|
911
|
+
className: "lk-ra-recorder",
|
|
912
|
+
"data-status": recorder.status,
|
|
913
|
+
"data-level": recorder.status === "recording" ? Math.min(LEVEL_STEPS, Math.round(recorder.level * LEVEL_STEPS)) : void 0,
|
|
914
|
+
children: [
|
|
915
|
+
recorder.status === "recording" ? /* @__PURE__ */ jsx("button", {
|
|
916
|
+
type: "button",
|
|
917
|
+
className: "lk-ra-stop",
|
|
918
|
+
disabled: inactive,
|
|
919
|
+
onClick: () => recorder.stop(),
|
|
920
|
+
children: s.readAloudStop
|
|
921
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
922
|
+
type: "button",
|
|
923
|
+
className: "lk-ra-record",
|
|
924
|
+
disabled: inactive,
|
|
925
|
+
"aria-disabled": !recorder.canRecord || busy || void 0,
|
|
926
|
+
onClick: beginTake,
|
|
927
|
+
children: recorder.take === null ? s.readAloudRecord : s.readAloudRerecord
|
|
928
|
+
}),
|
|
929
|
+
recorder.status === "recording" ? /* @__PURE__ */ jsx("p", {
|
|
930
|
+
className: "lk-ra-progress",
|
|
931
|
+
"aria-hidden": "true",
|
|
932
|
+
children: s.readAloudRecordingProgress(secondsWithin(recorder.elapsedMs, maxSeconds, "down"), maxSeconds)
|
|
933
|
+
}) : null,
|
|
934
|
+
takesLeft !== void 0 && maxTakes !== void 0 ? /* @__PURE__ */ jsx("p", {
|
|
935
|
+
className: "lk-ra-takes",
|
|
936
|
+
children: s.readAloudTakesRemaining(takesLeft, maxTakes)
|
|
937
|
+
}) : null
|
|
938
|
+
]
|
|
939
|
+
}),
|
|
940
|
+
takeAudio === null ? null : playback.refused ? /* @__PURE__ */ jsx("p", {
|
|
941
|
+
className: "lk-ra-take-unavailable",
|
|
942
|
+
role: "note",
|
|
943
|
+
children: s.readAloudPlaybackUnavailable
|
|
944
|
+
}) : /* @__PURE__ */ jsx("audio", {
|
|
945
|
+
className: "lk-ra-take",
|
|
946
|
+
controls: true,
|
|
947
|
+
src: takeAudio,
|
|
948
|
+
"aria-label": s.readAloudYourRecording,
|
|
949
|
+
onError: (event) => playback.refuse(event.currentTarget),
|
|
950
|
+
...isReview ? {} : { "data-take": recorder.takesUsed }
|
|
951
|
+
}),
|
|
952
|
+
isReview || submitted ? null : /* @__PURE__ */ jsxs("div", {
|
|
953
|
+
className: "lk-ra-actions",
|
|
954
|
+
children: [
|
|
955
|
+
/* @__PURE__ */ jsx("button", {
|
|
956
|
+
ref: submitRef,
|
|
957
|
+
type: "submit",
|
|
958
|
+
className: "lk-ra-submit",
|
|
959
|
+
disabled: disabled === true,
|
|
960
|
+
"aria-disabled": recorder.take === null || phase.kind !== "idle" || void 0,
|
|
961
|
+
"aria-busy": busy || void 0,
|
|
962
|
+
children: s.submit
|
|
963
|
+
}),
|
|
964
|
+
isExam ? /* @__PURE__ */ jsx("button", {
|
|
965
|
+
type: "button",
|
|
966
|
+
className: "lk-ra-submit-blank",
|
|
967
|
+
disabled: inactive,
|
|
968
|
+
"aria-disabled": busy || void 0,
|
|
969
|
+
onClick: () => {
|
|
970
|
+
if (!busy) submitTake(null);
|
|
971
|
+
},
|
|
972
|
+
children: s.readAloudSubmitWithoutRecording
|
|
973
|
+
}) : null,
|
|
974
|
+
offersRetry(phase, uploaded) ? /* @__PURE__ */ jsx("button", {
|
|
975
|
+
type: "button",
|
|
976
|
+
className: "lk-ra-retry",
|
|
977
|
+
onClick: () => {
|
|
978
|
+
submitRef.current?.focus();
|
|
979
|
+
if (phase.kind === "upload-failed") submitTake(recorder.take);
|
|
980
|
+
else if (phase.kind === "assess-failed" && uploaded !== null) retryAssess(uploaded, phase.take);
|
|
981
|
+
},
|
|
982
|
+
children: s.readAloudTryAgain
|
|
983
|
+
}) : null
|
|
984
|
+
]
|
|
985
|
+
}),
|
|
986
|
+
busy ? /* @__PURE__ */ jsx("p", {
|
|
987
|
+
className: "lk-ra-pending",
|
|
988
|
+
role: "status",
|
|
989
|
+
"aria-busy": "true",
|
|
990
|
+
children: phase.kind === "uploading" ? s.readAloudUploading : s.readAloudAssessing
|
|
991
|
+
}) : null,
|
|
992
|
+
alertText !== null ? /* @__PURE__ */ jsx("p", {
|
|
993
|
+
className: "lk-ra-alert",
|
|
994
|
+
role: "alert",
|
|
995
|
+
children: alertText
|
|
996
|
+
}, errorNonce) : null,
|
|
997
|
+
feedbackAssessment !== null ? /* @__PURE__ */ jsx(PronunciationFeedback, {
|
|
998
|
+
data: coachingItem,
|
|
999
|
+
assessment: feedbackAssessment,
|
|
1000
|
+
...feedbackGrade !== null ? { grade: feedbackGrade } : {},
|
|
1001
|
+
...feedbackAudio !== null ? { audioUrl: feedbackAudio } : {},
|
|
1002
|
+
...breakThreshold !== void 0 ? { breakThreshold } : {},
|
|
1003
|
+
...monotoneThreshold !== void 0 ? { monotoneThreshold } : {},
|
|
1004
|
+
...locale !== void 0 ? { locale } : {},
|
|
1005
|
+
...strings !== void 0 ? { strings } : {},
|
|
1006
|
+
...ai !== void 0 ? { ai } : {},
|
|
1007
|
+
...onInteraction !== void 0 ? { onInteraction } : {},
|
|
1008
|
+
renderMode,
|
|
1009
|
+
delivery: policy
|
|
1010
|
+
}) : null,
|
|
1011
|
+
storedMarks !== null ? /* @__PURE__ */ jsxs("div", {
|
|
1012
|
+
className: "lk-ra-marks",
|
|
1013
|
+
children: [
|
|
1014
|
+
/* @__PURE__ */ jsx("ol", {
|
|
1015
|
+
className: "lk-ra-words",
|
|
1016
|
+
"aria-label": s.pronunciationWordsLabel,
|
|
1017
|
+
dir: contentDir,
|
|
1018
|
+
children: storedMarks.map((entry, index) => {
|
|
1019
|
+
const sentence = markSentence(entry, s);
|
|
1020
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
1021
|
+
className: "lk-ra-word",
|
|
1022
|
+
"data-state": entry.state,
|
|
1023
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1024
|
+
className: "lk-visually-hidden",
|
|
1025
|
+
style: VISUALLY_HIDDEN,
|
|
1026
|
+
children: sentence
|
|
1027
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1028
|
+
className: "lk-ra-word-text",
|
|
1029
|
+
"aria-hidden": "true",
|
|
1030
|
+
dir: contentDir ?? "auto",
|
|
1031
|
+
lang: contentLang,
|
|
1032
|
+
children: entry.reference
|
|
1033
|
+
})]
|
|
1034
|
+
}, index);
|
|
1035
|
+
})
|
|
1036
|
+
}),
|
|
1037
|
+
/* @__PURE__ */ jsx("ul", {
|
|
1038
|
+
className: "lk-ra-legend",
|
|
1039
|
+
children: STORED_STATES.map((state) => /* @__PURE__ */ jsx("li", {
|
|
1040
|
+
"data-state": state,
|
|
1041
|
+
children: s.pronunciationLegend(state)
|
|
1042
|
+
}, state))
|
|
1043
|
+
}),
|
|
1044
|
+
/* @__PURE__ */ jsx(ReadingCoaching, {
|
|
1045
|
+
data: coachingItem,
|
|
1046
|
+
grade: storedGrade,
|
|
1047
|
+
renderMode,
|
|
1048
|
+
delivery: policy,
|
|
1049
|
+
...ai !== void 0 ? { ai } : {},
|
|
1050
|
+
...locale !== void 0 ? { locale } : {},
|
|
1051
|
+
...onInteraction !== void 0 ? { onInteraction } : {},
|
|
1052
|
+
strings: s,
|
|
1053
|
+
contentLang,
|
|
1054
|
+
contentDir
|
|
1055
|
+
})
|
|
1056
|
+
]
|
|
1057
|
+
}) : null,
|
|
1058
|
+
/* @__PURE__ */ jsx(FeedbackRegion, {
|
|
1059
|
+
id: `${data.id}-feedback`,
|
|
1060
|
+
children: isReview ? outcomeReading !== void 0 && (policy.feedback || outcomeReading.kind !== "graded") ? /* @__PURE__ */ jsx(OutcomeSummary, {
|
|
1061
|
+
reading: outcomeReading,
|
|
1062
|
+
s,
|
|
1063
|
+
scoreShown: gradeShownInPanel
|
|
1064
|
+
}) : null : gradeShownInPanel ? /* @__PURE__ */ jsx("span", {
|
|
1065
|
+
className: "lk-visually-hidden",
|
|
1066
|
+
style: VISUALLY_HIDDEN,
|
|
1067
|
+
children: /* @__PURE__ */ jsx(AnnouncementText, { announcement })
|
|
1068
|
+
}) : /* @__PURE__ */ jsx(AnnouncementText, { announcement })
|
|
1069
|
+
})
|
|
1070
|
+
]
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region src/components/ReadAloud/index.tsx
|
|
1075
|
+
/**
|
|
1076
|
+
* Public Read Aloud activity: the core wrapped in `ActivityErrorBoundary` so a
|
|
1077
|
+
* render failure (including the dev schema-validation throw and the
|
|
1078
|
+
* missing-`upload` guard) degrades to an accessible fallback instead of
|
|
1079
|
+
* crashing the host.
|
|
1080
|
+
*/
|
|
1081
|
+
function ReadAloud(props) {
|
|
1082
|
+
const strings = useLkStrings(props.strings);
|
|
1083
|
+
return /* @__PURE__ */ jsx(ActivityErrorBoundary, {
|
|
1084
|
+
activityTitle: props.data?.title,
|
|
1085
|
+
strings,
|
|
1086
|
+
children: /* @__PURE__ */ jsx(ReadAloud$1, { ...props })
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
//#endregion
|
|
1090
|
+
export { stopCaptureGroup as n, ReadAloud as t };
|
|
1091
|
+
|
|
1092
|
+
//# sourceMappingURL=ReadAloud-C8RhaUqV.js.map
|