@tinyweb_dev/oe-exam-sdk 0.1.3 → 0.1.4
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/README.md +58 -4
- package/dist/api/contest-rooms.d.ts +13 -0
- package/dist/api/contest-rooms.js +1 -0
- package/dist/api/contest-rooms.types.d.ts +47 -0
- package/dist/api/contest-rooms.types.js +1 -0
- package/dist/api/exam-entry.d.ts +14 -0
- package/dist/api/exam-entry.js +1 -0
- package/dist/api/exam-entry.types.d.ts +93 -0
- package/dist/api/exam-entry.types.js +1 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +1 -1
- package/dist/components/exams/entry/EntryRestartDialog.d.ts +12 -0
- package/dist/components/exams/entry/EntryRestartDialog.js +1 -0
- package/dist/components/exams/entry/StudentExamEntryPageContainer.d.ts +31 -0
- package/dist/components/exams/entry/StudentExamEntryPageContainer.js +1 -0
- package/dist/components/exams/entry/exam-entry.states.d.ts +22 -0
- package/dist/components/exams/entry/exam-entry.states.js +1 -0
- package/dist/components/exams/entry/exam-entry.utils.d.ts +16 -0
- package/dist/components/exams/entry/exam-entry.utils.js +1 -0
- package/dist/components/exams/entry/fullscreen-loading.d.ts +5 -0
- package/dist/components/exams/entry/fullscreen-loading.js +1 -0
- package/dist/components/exams/entry/index.d.ts +7 -0
- package/dist/components/exams/entry/index.js +1 -0
- package/dist/components/exams/index.d.ts +8 -0
- package/dist/components/exams/index.js +1 -1
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.d.ts +18 -0
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +1 -0
- package/dist/components/exams/rooms/contest-rooms.utils.d.ts +32 -0
- package/dist/components/exams/rooms/contest-rooms.utils.js +1 -0
- package/dist/components/exams/rooms/index.d.ts +3 -0
- package/dist/components/exams/rooms/index.js +1 -0
- package/dist/components/exams/take/ExamTakingApiContext.d.ts +10 -0
- package/dist/components/exams/take/ExamTakingApiContext.js +1 -0
- package/dist/components/exams/take/ExamTakingPageContainer.d.ts +28 -4
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
- package/dist/components/exams/take/components/QuestionRenderer.d.ts +25 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/index.d.ts +10 -0
- package/dist/components/exams/take/components/index.js +1 -0
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.d.ts +7 -0
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.d.ts +3 -0
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +21 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +20 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.d.ts +32 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/components/speaking/FindObjectInPicture.module.css +133 -0
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.d.ts +9 -0
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingCompareImages.module.css +153 -0
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.d.ts +7 -0
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingDragObjects.module.css +238 -0
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.d.ts +27 -0
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingHeader.d.ts +6 -0
- package/dist/components/exams/take/components/speaking/SpeakingHeader.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingInfoExchange.module.css +193 -0
- package/dist/components/exams/take/components/speaking/SpeakingInfoExchangeSummerSky.module.css +238 -0
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.d.ts +5 -0
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.d.ts +14 -0
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.d.ts +7 -0
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.d.ts +6 -0
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +115 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.d.ts +11 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingStoryImages.module.css +231 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.d.ts +13 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +1 -0
- package/dist/components/exams/take/components/speaking/index.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/index.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.d.ts +26 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.d.ts +32 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +34 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/index.d.ts +13 -0
- package/dist/components/exams/take/components/speaking/renderers/index.js +1 -0
- package/dist/components/exams/take/hooks/index.d.ts +2 -0
- package/dist/components/exams/take/hooks/index.js +1 -0
- package/dist/components/exams/take/hooks/useAnswerAutosave.d.ts +27 -0
- package/dist/components/exams/take/hooks/useAnswerAutosave.js +1 -0
- package/dist/components/exams/take/hooks/useAttemptSubmit.d.ts +29 -0
- package/dist/components/exams/take/hooks/useAttemptSubmit.js +1 -0
- package/dist/components/exams/take/hooks/useAutoSave.d.ts +20 -0
- package/dist/components/exams/take/hooks/useAutoSave.js +1 -0
- package/dist/components/exams/take/hooks/useExamCountdown.d.ts +22 -0
- package/dist/components/exams/take/hooks/useExamCountdown.js +1 -0
- package/dist/components/exams/take/hooks/useExamTimer.d.ts +21 -0
- package/dist/components/exams/take/hooks/useExamTimer.js +1 -0
- package/dist/components/exams/take/hooks/useProctoringCamera.d.ts +19 -0
- package/dist/components/exams/take/hooks/useProctoringCamera.js +1 -0
- package/dist/components/exams/take/hooks/useProctoringSnapshot.d.ts +25 -0
- package/dist/components/exams/take/hooks/useProctoringSnapshot.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.d.ts +19 -0
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingNavigation.d.ts +37 -0
- package/dist/components/exams/take/hooks/useSpeakingNavigation.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingRecorder.d.ts +45 -5
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -1
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.d.ts +39 -0
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +1 -0
- package/dist/components/exams/take/hooks/useToeicAudio.d.ts +17 -0
- package/dist/components/exams/take/hooks/useToeicAudio.js +1 -0
- package/dist/components/exams/take/index.d.ts +9 -1
- package/dist/components/exams/take/index.js +1 -1
- package/dist/components/exams/take/types/speaking-cue-card.types.d.ts +10 -0
- package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -0
- package/dist/components/exams/take/types/speaking.types.d.ts +153 -0
- package/dist/components/exams/take/types/speaking.types.js +1 -0
- package/dist/components/exams/take/types.d.ts +286 -1
- package/dist/components/exams/take/utils/answer-transformers.d.ts +26 -0
- package/dist/components/exams/take/utils/answer-transformers.js +1 -0
- package/dist/components/exams/take/utils/coloring-assignments.d.ts +5 -0
- package/dist/components/exams/take/utils/coloring-assignments.js +1 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +362 -0
- package/dist/components/exams/take/utils/question-transformers.js +1 -0
- package/dist/components/features/exam-entry/entry-state-cards.d.ts +19 -0
- package/dist/components/features/exam-entry/entry-state-cards.js +1 -0
- package/dist/components/features/exam-entry/index.d.ts +2 -0
- package/dist/components/features/exam-entry/index.js +1 -0
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.d.ts +12 -0
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.d.ts +14 -0
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.d.ts +16 -0
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/components/CloseButton.d.ts +9 -0
- package/dist/components/features/exam-entry/themed/components/CloseButton.js +1 -0
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.d.ts +13 -0
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +1 -0
- package/dist/components/features/exam-entry/themed/components/ModeSelector.d.ts +7 -0
- package/dist/components/features/exam-entry/themed/components/ModeSelector.js +1 -0
- package/dist/components/features/exam-entry/themed/components/StartButton.d.ts +11 -0
- package/dist/components/features/exam-entry/themed/components/StartButton.js +1 -0
- package/dist/components/features/exam-entry/themed/components/index.d.ts +4 -0
- package/dist/components/features/exam-entry/themed/components/index.js +1 -0
- package/dist/components/features/exam-entry/themed/index.d.ts +1 -0
- package/dist/components/features/exam-entry/themed/index.js +1 -0
- package/dist/components/results/ResultReviewPageContainer.d.ts +3 -1
- package/dist/components/results/ResultReviewPageContainer.js +1 -1
- package/dist/components/results/hooks/useResultReviewPolling.d.ts +27 -0
- package/dist/components/results/hooks/useResultReviewPolling.js +1 -0
- package/dist/components/results/index.d.ts +3 -0
- package/dist/components/results/index.js +1 -1
- package/dist/components/results/review-status.utils.d.ts +12 -0
- package/dist/components/results/review-status.utils.js +1 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.d.ts +12 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.js +1 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.module.css +115 -0
- package/dist/components/shared/audio/assets/fr-image-part1-border.png +0 -0
- package/dist/components/shared/audio/assets/ic-speaker.webp +0 -0
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.d.ts +11 -3
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.d.ts +5 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -1
- package/dist/components/themes/index.d.ts +11 -0
- package/dist/components/themes/index.js +1 -1
- package/dist/components/themes/spaceship/SpaceshipAudioPlayer.d.ts +8 -0
- package/dist/components/themes/spaceship/SpaceshipAudioPlayer.js +1 -0
- package/dist/components/themes/spaceship/SpaceshipLayout.d.ts +2 -0
- package/dist/components/themes/spaceship/SpaceshipLayout.js +1 -0
- package/dist/components/themes/spaceship/assets/images/X-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/X.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/audio-bar.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/background.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/bg_sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/bingo.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-wrapper-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-wrapper.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/board_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/button_feedback_vi.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-right-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-right.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/focus-vector.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/head-phone_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-mute-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-mute.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play-violet.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/micro.png +0 -0
- package/dist/components/themes/spaceship/assets/images/next-small.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/next-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/next.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/purple-circle.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/question-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/question-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/red-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/red-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/red-wrapper-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/red-wrapper.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/robot-avatar.png +0 -0
- package/dist/components/themes/spaceship/assets/images/sound_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/student-avatar.png +0 -0
- package/dist/components/themes/spaceship/assets/images/teacher-play-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/teacher-play.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/timer-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/styles/SpaceshipLayout.module.css +347 -0
- package/dist/components/themes/spaceship/assets/styles/SpaceshipWaitingRoom.module.css +395 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/shared/lib/stores/examTakingStore.d.ts +54 -0
- package/dist/shared/lib/stores/examTakingStore.js +1 -0
- package/dist/shared/lib/stores/gradingTransientStore.d.ts +23 -0
- package/dist/shared/lib/stores/gradingTransientStore.js +1 -0
- package/dist/shared/lib/themes/config/starters.config.d.ts +3 -0
- package/dist/shared/lib/themes/config/starters.config.js +1 -0
- package/dist/shared/lib/themes/index.d.ts +18 -0
- package/dist/shared/lib/themes/index.js +1 -0
- package/dist/shared/lib/themes/types.d.ts +39 -0
- package/dist/shared/lib/themes/types.js +1 -0
- package/dist/shared/lib/toast.d.ts +19 -0
- package/dist/shared/lib/toast.js +1 -0
- package/package.json +10 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type WarningLevel = 'none' | 'warning' | 'critical';
|
|
2
|
+
export interface WarningThreshold {
|
|
3
|
+
seconds: number;
|
|
4
|
+
level: WarningLevel;
|
|
5
|
+
callback?: () => void;
|
|
6
|
+
}
|
|
7
|
+
interface UseExamTimerOptions {
|
|
8
|
+
onTimeExpired: () => void;
|
|
9
|
+
/** @deprecated Use warningThresholds[] instead */
|
|
10
|
+
onWarningThreshold?: () => void;
|
|
11
|
+
/** @deprecated Use warningThresholds[] instead */
|
|
12
|
+
warningThresholdSeconds?: number;
|
|
13
|
+
/** Multiple warning thresholds for TOEIC (15min + 5min). Highest severity wins. */
|
|
14
|
+
warningThresholds?: WarningThreshold[];
|
|
15
|
+
}
|
|
16
|
+
interface UseExamTimerReturn {
|
|
17
|
+
remainingSeconds: number;
|
|
18
|
+
warningLevel: WarningLevel;
|
|
19
|
+
}
|
|
20
|
+
export declare function useExamTimer(options: UseExamTimerOptions): UseExamTimerReturn;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useEffect as n,useRef as e}from"react";import{useExamTakingStore as r}from"../../../../shared/lib/stores/examTakingStore";export function useExamTimer(t){const i=r(),c=e(null),o=e(!1),s=(()=>{const n=t.warningThresholds??[];if(0===n.length&&void 0!==t.warningThresholdSeconds){const n=[];return t.onWarningThreshold&&n.push({seconds:t.warningThresholdSeconds,level:"critical",callback:t.onWarningThreshold}),n}return n})(),l=e(new Set),a=e(t.onTimeExpired);a.current=t.onTimeExpired,n(()=>{if(!i.isInitialized)return;c.current&&clearInterval(c.current),o.current=!1,l.current=new Set;const n=()=>{const n=i.syncRemainingSeconds();for(const e of s)n<=e.seconds&&!l.current.has(e.seconds)&&(l.current.add(e.seconds),e.callback?.());n<=0&&(c.current&&(clearInterval(c.current),c.current=null),o.current||(o.current=!0,a.current()))};c.current=setInterval(n,1e3);const e=()=>{"visible"===document.visibilityState&&n()};return document.addEventListener("visibilitychange",e),window.addEventListener("focus",e),()=>{c.current&&(clearInterval(c.current),c.current=null),document.removeEventListener("visibilitychange",e),window.removeEventListener("focus",e)}},[i.isInitialized]);const u=function(n,e){let r="none";for(const t of e)if(n<=t.seconds&&"none"!==t.level){if("critical"===t.level)return"critical";"warning"===t.level&&(r="warning")}return r}(i.remainingSeconds,s);return{remainingSeconds:i.remainingSeconds,warningLevel:u}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ExamTakingSdkApi } from '../../../../api/exam-taking';
|
|
2
|
+
interface UseProctoringCameraProps {
|
|
3
|
+
/** Injected API adapter (replaces the host app's `attemptsApi`). */
|
|
4
|
+
api: ExamTakingSdkApi;
|
|
5
|
+
attemptId: string | null;
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Gate camera capture. The host app previously gated on
|
|
9
|
+
* `NEXT_PUBLIC_ENABLE_PROCTORING_CAMERA`; expose it as a prop so the SDK does
|
|
10
|
+
* not read host env vars. Defaults to `false` (disabled).
|
|
11
|
+
*/
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function useProctoringCamera({ api, attemptId, intervalMs, enabled, }: UseProctoringCameraProps): {
|
|
15
|
+
isCameraActive: boolean;
|
|
16
|
+
cameraError: string;
|
|
17
|
+
snapshotCount: number;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useEffect as e,useRef as r,useState as t}from"react";export function useProctoringCamera({api:n,attemptId:a,intervalMs:o=1e4,enabled:c=!1}){const[i,u]=t(!1),[l,s]=t(null),[d,m]=t(0),p=r(null),h=r(null),g=r(null);return e(()=>{if("undefined"==typeof window)return;if(!c)return;let e=!0;return(async()=>{try{const r=await navigator.mediaDevices.getUserMedia({video:{facingMode:"user"},audio:!1});if(!e)return void r.getTracks().forEach(e=>e.stop());h.current=r;const t=document.createElement("video");t.srcObject=r,t.muted=!0,t.playsInline=!0,t.onloadedmetadata=()=>{e&&(t.play().catch(console.error),u(!0),s(null))},p.current=t}catch(r){console.error("Camera permission denied or error:",r),e&&(u(!1),s(r.message||"Camera access denied"))}})(),()=>{e=!1,h.current&&(h.current.getTracks().forEach(e=>e.stop()),h.current=null),u(!1),p.current=null}},[c]),e(()=>{if(!a||!i||!p.current)return void(g.current&&(clearInterval(g.current),g.current=null));return g.current=setInterval(async()=>{const e=p.current;if(e&&e.videoWidth)try{const r=document.createElement("canvas");r.width=e.videoWidth,r.height=e.videoHeight;const t=r.getContext("2d");if(!t)return;t.drawImage(e,0,0,r.width,r.height),r.toBlob(async e=>{if(e)try{await(n.uploadProctoringSnapshot?.({attemptId:a,imageBlob:e})),m(e=>e+1)}catch(e){console.error("Failed to upload proctoring snapshot:",e)}},"image/jpeg",.6)}catch(e){console.error("Error in capture process:",e)}},o),()=>{g.current&&(clearInterval(g.current),g.current=null)}},[n,a,i,o]),{isCameraActive:i,cameraError:l,snapshotCount:d}}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExamTakingSdkApi } from '../../../../api/exam-taking';
|
|
2
|
+
export interface UseProctoringSnapshotOptions {
|
|
3
|
+
/** API adapter; snapshots are skipped when `uploadProctoringSnapshot` is undefined. */
|
|
4
|
+
api: ExamTakingSdkApi;
|
|
5
|
+
/** Active attempt id, or null while loading. */
|
|
6
|
+
attemptId: string | null;
|
|
7
|
+
/** Whether proctoring is active (attempt loaded, room requires proctoring). */
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
/** Interval between snapshot uploads in milliseconds. */
|
|
10
|
+
intervalMs?: number;
|
|
11
|
+
/** Notified when a snapshot capture/upload fails (best-effort logging). */
|
|
12
|
+
onError?: (error: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface UseProctoringSnapshotResult {
|
|
15
|
+
/** Attach to a `<video>` element rendering the proctoring webcam stream. */
|
|
16
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
17
|
+
/** `true` once the webcam stream is live and snapshots are being captured. */
|
|
18
|
+
isActiveRef: React.MutableRefObject<boolean>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Captures periodic webcam snapshots and uploads them for proctoring. Owns the
|
|
22
|
+
* camera stream lifecycle (request on enable, stop on cleanup) and encodes
|
|
23
|
+
* frames off a hidden canvas, mirroring the host app's proctoring loop.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useProctoringSnapshot({ api, attemptId, enabled, intervalMs, onError, }: UseProctoringSnapshotOptions): UseProctoringSnapshotResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{useEffect as t,useRef as e}from"react";export function useProctoringSnapshot({api:r,attemptId:n,enabled:a,intervalMs:i=6e4,onError:o}){const c=e(null),s=e(!1),u=e(o);return u.current=o,t(()=>{if(!a||!n||!r.uploadProctoringSnapshot)return;if("undefined"==typeof navigator||!navigator.mediaDevices?.getUserMedia)return;let t=null,e=null,o=!1;const d=async()=>{const t=c.current;if(!t||0===t.videoWidth||!r.uploadProctoringSnapshot)return;const e=document.createElement("canvas");e.width=t.videoWidth,e.height=t.videoHeight;const a=e.getContext("2d");if(!a)return;a.drawImage(t,0,0,e.width,e.height);const i=await new Promise(t=>e.toBlob(t,"image/jpeg",.7));if(i)try{await r.uploadProctoringSnapshot({attemptId:n,imageBlob:i})}catch(t){u.current?.(t)}};return(async()=>{try{if(t=await navigator.mediaDevices.getUserMedia({video:!0,audio:!1}),o)return void t.getTracks().forEach(t=>t.stop());c.current&&(c.current.srcObject=t,await c.current.play().catch(()=>{})),s.current=!0,d(),e=setInterval(()=>{d()},i)}catch(t){u.current?.(t)}})(),()=>{o=!0,s.current=!1,e&&clearInterval(e),t&&t.getTracks().forEach(t=>t.stop())}},[r,n,a,i]),{videoRef:c,isActiveRef:s}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ConversationPlaybackPhase = 'idle' | 'playing-interlocutor' | 'playing-partner' | 'ready';
|
|
2
|
+
export interface UseSpeakingConversationAudioOptions {
|
|
3
|
+
/** S3 key or direct URL for the interlocutor's audio */
|
|
4
|
+
interlocutorAudioUrl?: string;
|
|
5
|
+
/** S3 key or direct URL for the partner's audio */
|
|
6
|
+
partnerAudioUrl?: string;
|
|
7
|
+
/** Called once when the full sequence has finished */
|
|
8
|
+
onSequenceComplete: () => void;
|
|
9
|
+
}
|
|
10
|
+
export interface UseSpeakingConversationAudioReturn {
|
|
11
|
+
phase: ConversationPlaybackPhase;
|
|
12
|
+
isPlaying: boolean;
|
|
13
|
+
replayAll: () => void;
|
|
14
|
+
togglePlayPause: () => void;
|
|
15
|
+
currentAudioUrl: string;
|
|
16
|
+
currentTime: number;
|
|
17
|
+
duration: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function useSpeakingConversationAudio({ interlocutorAudioUrl, partnerAudioUrl, onSequenceComplete, }: UseSpeakingConversationAudioOptions): UseSpeakingConversationAudioReturn;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useCallback as e,useEffect as r,useRef as t,useState as n}from"react";import{usePresignedFileUrl as u}from"../../../../shared/lib/hooks/usePresignedFileUrl";export function useSpeakingConversationAudio({interlocutorAudioUrl:a,partnerAudioUrl:i,onSequenceComplete:o}){const[c,l]=n("idle"),[d,s]=n(0),[p,y]=n(0),{previewUrl:m}=u(a??""),{previewUrl:g}=u(i??""),v=t(null),A=t(o);r(()=>{A.current=o},[o]);const E=t(m),L=t(g);r(()=>{E.current=m},[m]),r(()=>{L.current=g},[g]);const U=e(()=>{const e=v.current;e&&(e.pause(),e.src="")},[]),h=e(e=>{let r=v.current;r||(r=new Audio,v.current=r),r.src=e,r.play().catch(()=>{})},[]);r(()=>{let e=v.current;e||(e=new Audio,v.current=e);const r=()=>{l(e=>{if("playing-interlocutor"===e){const e=L.current;e?setTimeout(()=>{l("playing-partner"),h(e)},0):setTimeout(()=>{l("ready"),A.current()},0)}else"playing-partner"===e&&setTimeout(()=>{l("ready"),A.current()},0);return e})},t=()=>{e&&s(e.currentTime)},n=()=>{e&&(y(e.duration),s(0))};return e.addEventListener("ended",r),e.addEventListener("timeupdate",t),e.addEventListener("loadedmetadata",n),()=>{e.removeEventListener("ended",r),e.removeEventListener("timeupdate",t),e.removeEventListener("loadedmetadata",n)}},[h]),r(()=>(U(),l("idle"),m||g?(m?(l("playing-interlocutor"),h(m)):g&&(l("playing-partner"),h(g)),()=>{U()}):(l("ready"),void A.current())),[m,g]),r(()=>()=>{U()},[]);const T=e(()=>{U(),E.current?(l("playing-interlocutor"),h(E.current)):L.current?(l("playing-partner"),h(L.current)):(l("ready"),A.current())},[U,h]),f=e(()=>{const e=v.current;e&&(e.paused?e.play().catch(()=>{}):e.pause())},[]);return{phase:c,isPlaying:Boolean(v.current&&!v.current.paused),replayAll:T,togglePlayPause:f,currentAudioUrl:"playing-interlocutor"===c?m??"":"playing-partner"===c?g??"":"",currentTime:d,duration:p}}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ApiQuestion } from '../utils/question-transformers';
|
|
2
|
+
/**
|
|
3
|
+
* A speaking question enriched with part metadata.
|
|
4
|
+
* Extends ApiQuestion with part display info and a stable exam-wide index.
|
|
5
|
+
*/
|
|
6
|
+
export interface SpeakingQuestion extends ApiQuestion {
|
|
7
|
+
partName: string;
|
|
8
|
+
/** 0-based index within the full flattened exam question list */
|
|
9
|
+
questionIndex: number;
|
|
10
|
+
}
|
|
11
|
+
export interface UseSpeakingNavigationOptions {
|
|
12
|
+
questions: SpeakingQuestion[];
|
|
13
|
+
initialIndex?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface UseSpeakingNavigationReturn {
|
|
16
|
+
/** 0-based index of the currently displayed question */
|
|
17
|
+
currentIndex: number;
|
|
18
|
+
/** Currently displayed question, or null if questions list is empty */
|
|
19
|
+
currentQuestion: SpeakingQuestion | null;
|
|
20
|
+
/** Total number of questions across all parts */
|
|
21
|
+
totalQuestions: number;
|
|
22
|
+
/** part_no of the current question */
|
|
23
|
+
currentPartNo: number;
|
|
24
|
+
/** Display name of the current part (e.g. "Part 1") */
|
|
25
|
+
currentPartName: string;
|
|
26
|
+
/**
|
|
27
|
+
* 1-based question number within the current part.
|
|
28
|
+
* Used for "CÂU HỎI SỐ [N]" display label.
|
|
29
|
+
*/
|
|
30
|
+
questionNumberInPart: number;
|
|
31
|
+
canGoPrev: boolean;
|
|
32
|
+
canGoNext: boolean;
|
|
33
|
+
goNext: () => void;
|
|
34
|
+
goPrev: () => void;
|
|
35
|
+
goTo: (index: number) => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function useSpeakingNavigation(options: UseSpeakingNavigationOptions): UseSpeakingNavigationReturn;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useState as t,useCallback as n,useMemo as r}from"react";export function useSpeakingNavigation(e){const{questions:o,initialIndex:a=0}=e,[i,u]=t(a),c=o.length,s=o[i]??null,l=s?.part_no??1,g=s?.partName??"",m=r(()=>{if(!s)return 1;return o.slice(0,i).filter(t=>t.part_no===s.part_no).length+1},[i,s,o]),p=i>0,N=i<c-1,x=n(()=>{u(t=>Math.min(t+1,c-1))},[c]),P=n(()=>{u(t=>Math.max(t-1,0))},[]),f=n(t=>{t>=0&&t<c&&u(t)},[c]);return{currentIndex:i,currentQuestion:s,totalQuestions:c,currentPartNo:l,currentPartName:g,questionNumberInPart:m,canGoPrev:p,canGoNext:N,goNext:x,goPrev:P,goTo:f}}
|
|
@@ -1,8 +1,48 @@
|
|
|
1
|
-
export type
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export type RecordingStatus = 'idle' | 'recording' | 'recorded' | 'uploading' | 'uploaded' | 'error';
|
|
2
|
+
export interface UseSpeakingRecorderOptions {
|
|
3
|
+
/** Attempt ID — needed for upload endpoint */
|
|
4
|
+
attemptId: string;
|
|
5
|
+
/** Question ID — needed for upload endpoint */
|
|
6
|
+
questionId: string;
|
|
7
|
+
/** Max recording duration in ms (default: 30000) */
|
|
8
|
+
maxDurationMs?: number;
|
|
9
|
+
/** Called with S3 key when upload completes */
|
|
10
|
+
onRecordingComplete: (audioKey: string, forQuestionId?: string) => void;
|
|
11
|
+
/** Called on any non-retryable error */
|
|
12
|
+
onError?: (error: string) => void;
|
|
13
|
+
/** Existing S3 key from a previous recording (used to restore playback on navigation) */
|
|
14
|
+
existingAnswerKey?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface UseSpeakingRecorderReturn {
|
|
17
|
+
/** Current recording status */
|
|
18
|
+
status: RecordingStatus;
|
|
19
|
+
/** Countdown while recording (counts down from maxDurationMs/1000) */
|
|
5
20
|
remainingSeconds: number;
|
|
21
|
+
/** True when remainingSeconds <= 5 */
|
|
22
|
+
isWarning: boolean;
|
|
23
|
+
/** Elapsed seconds while recording */
|
|
24
|
+
durationSeconds: number;
|
|
25
|
+
/** Local blob URL for playback preview (available after recording stops) */
|
|
26
|
+
previewUrl: string | null;
|
|
27
|
+
/** Uploaded S3 key (available after upload completes) */
|
|
28
|
+
uploadedKey: string | null;
|
|
29
|
+
/** Error message if status === 'error' */
|
|
30
|
+
error: string | null;
|
|
31
|
+
/** Whether the browser supports MediaRecorder API */
|
|
32
|
+
isSupported: boolean;
|
|
33
|
+
/** True when attempt has expired — caller should redirect */
|
|
34
|
+
isAttemptExpired: boolean;
|
|
35
|
+
/** Live transcript text (STREAMING mode only — null in RECORDING mode) */
|
|
36
|
+
liveTranscript: string | null;
|
|
37
|
+
/** Start recording. Requests microphone permission if not granted. */
|
|
6
38
|
startRecording: () => Promise<void>;
|
|
39
|
+
/** Stop recording. Automatically triggers upload. */
|
|
7
40
|
stopRecording: () => void;
|
|
8
|
-
|
|
41
|
+
/** Discard current recording and reset to idle */
|
|
42
|
+
resetRecording: () => void;
|
|
43
|
+
/** Re-record: discard current recording and start a new one */
|
|
44
|
+
reRecord: () => Promise<void>;
|
|
45
|
+
/** Retry upload with the existing blob (no re-record required) */
|
|
46
|
+
retryUpload: () => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare function useSpeakingRecorder(options: UseSpeakingRecorderOptions): UseSpeakingRecorderReturn;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{useState as r,useRef as e,useEffect as n,useCallback as t}from"react";import{usePresignedFileUrl as u}from"../../../../shared/lib/hooks";import{useExamTakingApi as o}from"../ExamTakingApiContext";export function useSpeakingRecorder(i){const c=i.maxDurationMs??3e4,a=Math.floor(c/1e3),d=o(),s=e(d);s.current=d;const l=e(i.onRecordingComplete),p=e(i.onError);l.current=i.onRecordingComplete,p.current=i.onError;const g=e(i.attemptId),m=e(i.questionId);g.current=i.attemptId,m.current=i.questionId;const[h,y]=r("idle"),[w,f]=r(a),[A,v]=r(0),[R,T]=r(null),[E,x]=r(null),[I,b]=r(null),[K,M]=r(!1),k=e(null),P=e(null),D=e([]),S=e(null),U=e(null),O=e(""),_=e(0),q=e(null),L=e(void 0),B=e(0),V=e(!0),C=e(""),G="undefined"!=typeof MediaRecorder,{previewUrl:j}=u(i.existingAnswerKey),N=t(()=>{S.current&&(clearInterval(S.current),S.current=null)},[]),F=t(()=>{U.current&&(U.current.startsWith("blob:")&&URL.revokeObjectURL(U.current),U.current=null)},[]),W=t(()=>{P.current&&(P.current.getTracks().forEach(r=>r.stop()),P.current=null)},[]),X=t(async(r,e,n,t)=>{if(!V.current)return;const u=t||C.current||m.current;m.current===u&&y("uploading");const o=g.current;if("mock-attempt-speaking"===o){if(await new Promise(r=>setTimeout(r,500)),!V.current)return;const r=`mock-audio-key-${u}`;return m.current===u&&(y("uploaded"),x(r)),l.current(r,u),q.current=null,void(L.current=void 0)}try{if(!s.current.uploadAudioAnswer)throw new Error("uploadAudioAnswer is not implemented by the provided API adapter.");const n=(i=await s.current.uploadAudioAnswer({attemptId:o,questionId:u,audioBlob:r,durationMs:e}))&&"object"==typeof i&&"data"in i?i.data:i;if(!V.current)return;const t=n.audioKey??"";m.current===u&&(y("uploaded"),x(t)),l.current(t,u),q.current=null,L.current=void 0,B.current=0,s.current.realtimeGradeSingle?.({attemptId:o,questionId:u,audioKey:t}).catch(r=>{console.debug("[Realtime Grading] Skipped or failed:",r?.response?.status??r?.message)})}catch(u){if(!V.current)return;const o=u,i=o?.response?.data?.code;if("ATTEMPT_EXPIRED"===i||"ATTEMPT_NOT_IN_PROGRESS"===i){const r="ATTEMPT_EXPIRED"===i?"Bài thi đã hết giờ":"Bài thi đã được nộp.";return y("error"),b(r),M(!0),void p.current?.(r)}if("AUDIO_FILE_TOO_LARGE"===i){const r="Bài ghi âm quá dài. Vui lòng ghi âm ngắn hơn.";return y("error"),b(r),void p.current?.(r)}if(n<3){const u=1e3*Math.pow(2,n);if(await new Promise(r=>setTimeout(r,u)),!V.current)return;B.current=n+1,await X(r,e,n+1,t)}else{const r="Tải lên bản ghi âm thất bại. Vui lòng thử lại.";y("error"),b(r),p.current?.(r)}}var i},[]),z=t(async(r,e,n)=>{q.current=r,L.current=e,B.current=0,await X(r,e,0,n)},[X]),$=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);if(!G){const r="Trình duyệt này không hỗ trợ ghi âm. Vui lòng dùng Chrome, Firefox hoặc Edge.";return y("error"),b(r),void p.current?.(r)}const r=function(){if("undefined"==typeof MediaRecorder)return null;const r=["audio/webm;codecs=opus","audio/webm","audio/mp4","audio/ogg;codecs=opus","audio/ogg"];for(const e of r)if(MediaRecorder.isTypeSupported(e))return e;return null}();if(!r){const r="Trình duyệt không hỗ trợ định dạng ghi âm.";return y("error"),b(r),void p.current?.(r)}try{const e=await navigator.mediaDevices.getUserMedia({audio:!0});P.current=e,O.current=r;const n=new MediaRecorder(e,{mimeType:r});k.current=n,D.current=[],n.ondataavailable=r=>{r.data.size>0&&D.current.push(r.data)},n.onstop=()=>{const r=Date.now()-_.current,e=new Blob(D.current,{type:O.current}),n=URL.createObjectURL(e),t=C.current;F(),U.current=n,T(n),y("recorded"),z(e,r,t)},n.start(),_.current=Date.now(),C.current=m.current,y("recording"),v(0),f(a),b(null),x(null),T(null),M(!1),S.current=setInterval(()=>{v(r=>r+1),f(r=>{const e=Math.max(0,r-1);return 0===e&&(N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()),e})},1e3)}catch(r){W();const e=r instanceof DOMException&&("NotAllowedError"===r.name||"PermissionDeniedError"===r.name)?"Vui lòng cấp quyền truy cập microphone trong trình duyệt.":"Không thể truy cập microphone. Vui lòng thử lại.";y("error"),b(e),p.current?.(e)}},[G,a,i.existingAnswerKey,N,W,z,F]),H=t(()=>{N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()},[N,W]),J=t(()=>{N(),W(),k.current&&"recording"===k.current.state&&k.current.stop(),k.current=null,D.current=[],q.current=null,L.current=void 0,B.current=0,F(),T(null),x(null),v(0),f(a),b(null),y("idle"),M(!1)},[N,W,F,a]),Q=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);J(),await $()},[i.existingAnswerKey,J,$]),Y=t(async()=>{const r=q.current;r&&(B.current=0,await X(r,L.current,0,C.current||void 0))},[X]);n(()=>{J()},[i.questionId]),n(()=>{"idle"===h&&j&&i.existingAnswerKey&&!R&&(T(j),U.current=j,x(i.existingAnswerKey),y("uploaded"))},[h,j,i.existingAnswerKey,R]),n(()=>(V.current=!0,()=>{V.current=!1,N(),k.current&&"recording"===k.current.state&&k.current.stop(),W(),F()}),[]);return{status:h,remainingSeconds:w,isWarning:w<=5&&"recording"===h,durationSeconds:A,previewUrl:R,uploadedKey:E,error:I,isSupported:G,isAttemptExpired:K,liveTranscript:null,startRecording:$,stopRecording:H,resetRecording:J,reRecord:Q,retryUpload:Y}}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type SpeakingQuestion } from './useSpeakingNavigation';
|
|
2
|
+
import type { QuestionPart } from '../utils/question-transformers';
|
|
3
|
+
export interface UseSpeakingRendererSetupOptions {
|
|
4
|
+
part: QuestionPart;
|
|
5
|
+
answers: Record<string, unknown>;
|
|
6
|
+
onAnswerChange: (questionId: string, value: unknown) => void;
|
|
7
|
+
onPartComplete?: () => void;
|
|
8
|
+
onPartBack?: () => void;
|
|
9
|
+
maxDurationMs?: number;
|
|
10
|
+
hasMic?: boolean;
|
|
11
|
+
initialQuestionIndex?: number;
|
|
12
|
+
currentQuestionIndex?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function useSpeakingRendererSetup({ part, answers, onAnswerChange, onPartComplete, onPartBack, maxDurationMs, hasMic, initialQuestionIndex, currentQuestionIndex, }: UseSpeakingRendererSetupOptions): {
|
|
15
|
+
nav: {
|
|
16
|
+
showPrev: boolean;
|
|
17
|
+
showNext: boolean;
|
|
18
|
+
canGoPrev: boolean;
|
|
19
|
+
canGoNext: boolean;
|
|
20
|
+
currentIndex: number;
|
|
21
|
+
currentQuestion: SpeakingQuestion | null;
|
|
22
|
+
totalQuestions: number;
|
|
23
|
+
currentPartNo: number;
|
|
24
|
+
currentPartName: string;
|
|
25
|
+
questionNumberInPart: number;
|
|
26
|
+
goNext: () => void;
|
|
27
|
+
goPrev: () => void;
|
|
28
|
+
goTo: (index: number) => void;
|
|
29
|
+
};
|
|
30
|
+
recorder: import("./useSpeakingRecorder").UseSpeakingRecorderReturn;
|
|
31
|
+
currentQuestion: SpeakingQuestion;
|
|
32
|
+
handleNext: () => void;
|
|
33
|
+
handlePrev: () => void;
|
|
34
|
+
showExistingBadge: boolean;
|
|
35
|
+
isRecordingInProgress: boolean;
|
|
36
|
+
isQuestionLocked: boolean;
|
|
37
|
+
isPostRecordingDelayActive: boolean;
|
|
38
|
+
onAudioPlayingChange: (playing: boolean) => void;
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useEffect as e,useMemo as r,useCallback as t,useRef as n,useState as o}from"react";import{toast as i}from"../../../../shared/lib/toast";import{useExamTakingStore as s}from"../../../../shared/lib/stores/examTakingStore";import{useSpeakingNavigation as a}from"./useSpeakingNavigation";import{useSpeakingRecorder as c}from"./useSpeakingRecorder";export function useSpeakingRendererSetup({part:u,answers:d,onAnswerChange:m,onPartComplete:l,onPartBack:p,maxDurationMs:x=3e4,hasMic:g=!0,initialQuestionIndex:v=0,currentQuestionIndex:h}){const P=r(()=>u.questions.map((e,r)=>({...e,partName:u.name||`Part ${e.part_no}`,questionIndex:r})),[u.questions,u.name]),I=a({questions:P,initialIndex:v});e(()=>{void 0!==h&&h>=0&&h!==I.currentIndex&&I.goTo(h)},[h,I.currentIndex,I]);const N=I.currentQuestion,{attemptId:G}=s(),f=N?d[N.id]:void 0,k=!!f,w=c({attemptId:G??"",questionId:N?.id??"",maxDurationMs:x,existingAnswerKey:g?f:void 0,onRecordingComplete:(e,r)=>{const t=r||N?.id;t&&m(t,e)},onError:e=>i.error(e)});e(()=>{g&&w.isAttemptExpired&&i.error("Bài thi đã hết giờ hoặc đã được nộp.")},[g,w.isAttemptExpired]);const A=g&&"recording"===w.status,T=n(A),q=n(null),[Q,R]=o(!1);e(()=>{g?(T.current&&!A&&(R(!0),q.current&&clearTimeout(q.current),q.current=setTimeout(()=>{R(!1),q.current=null},500)),A&&q.current&&(clearTimeout(q.current),q.current=null,R(!1)),T.current=A):R(!1)},[g,A]),e(()=>()=>{q.current&&clearTimeout(q.current)},[]);const[S,C]=o(!1),D=t(e=>{C(e)},[]);e(()=>{C(!1)},[N?.id]);const E=A||Q||S,{setIsNavDisabled:b}=s();e(()=>(b(E),()=>b(!1)),[E,b]);const y=t(()=>{E||(I.canGoNext?I.goNext():l?.())},[I,E,l]),B=t(()=>{E||(I.canGoPrev?I.goPrev():p?.())},[I,E,p]),M=r(()=>({...I,showPrev:I.canGoPrev||!!p,showNext:I.canGoNext||!!l,canGoPrev:!E&&(I.canGoPrev||!!p),canGoNext:!E&&(I.canGoNext||!!l)}),[I,E,p,l]),K=!(!f||g&&"idle"!==w.status);return{nav:M,recorder:g?w:null,currentQuestion:N,handleNext:y,handlePrev:B,showExistingBadge:K,isRecordingInProgress:A,isQuestionLocked:k,isPostRecordingDelayActive:Q,onAudioPlayingChange:D}}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to manage TOEIC audio playback state across questions
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Track current audio URL for the part
|
|
6
|
+
* - Pause audio when switching questions
|
|
7
|
+
* - Reset audio state when part changes
|
|
8
|
+
* - Provide audio loading state
|
|
9
|
+
*/
|
|
10
|
+
export declare function useToeicAudio(): {
|
|
11
|
+
currentAudioUrl: string;
|
|
12
|
+
isPlayingPartAudio: boolean;
|
|
13
|
+
isAudioLoading: boolean;
|
|
14
|
+
setPartAudio: (audioUrl: string | null) => void;
|
|
15
|
+
setAudioLoading: (loading: boolean) => void;
|
|
16
|
+
setAudioPlaying: (playing: boolean) => void;
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useState as i,useCallback as o}from"react";export function useToeicAudio(){const[t,u]=i(null),[r,n]=i(!1),[d,e]=i(!1);return{currentAudioUrl:t,isPlayingPartAudio:r,isAudioLoading:d,setPartAudio:o(i=>{u(i),n(!1),e(!1)},[]),setAudioLoading:o(i=>{e(i)},[]),setAudioPlaying:o(i=>{n(i)},[])}}
|
|
@@ -3,4 +3,12 @@ export type { ExamTakingPageContainerProps } from './ExamTakingPageContainer';
|
|
|
3
3
|
export { GenericQuestionRenderer } from './GenericQuestionRenderer';
|
|
4
4
|
export type { GenericQuestionRendererProps } from './GenericQuestionRenderer';
|
|
5
5
|
export type { ExamTakingPart } from './exam-taking.utils';
|
|
6
|
-
export type { ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingSdkApi, ExamTakingTemplatePart, SaveExamTakingAnswersInput } from '
|
|
6
|
+
export type { ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingSdkApi, ExamTakingTemplatePart, SaveExamTakingAnswersInput } from '../../../api/exam-taking';
|
|
7
|
+
export { useExamCountdown } from './hooks/useExamCountdown';
|
|
8
|
+
export type { UseExamCountdownOptions, UseExamCountdownResult } from './hooks/useExamCountdown';
|
|
9
|
+
export { useAnswerAutosave } from './hooks/useAnswerAutosave';
|
|
10
|
+
export type { UseAnswerAutosaveOptions, UseAnswerAutosaveResult } from './hooks/useAnswerAutosave';
|
|
11
|
+
export { useProctoringSnapshot } from './hooks/useProctoringSnapshot';
|
|
12
|
+
export type { UseProctoringSnapshotOptions, UseProctoringSnapshotResult } from './hooks/useProctoringSnapshot';
|
|
13
|
+
export { useAttemptSubmit } from './hooks/useAttemptSubmit';
|
|
14
|
+
export type { UseAttemptSubmitOptions, UseAttemptSubmitResult, SubmittedSummary } from './hooks/useAttemptSubmit';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{ExamTakingPageContainer}from"./ExamTakingPageContainer";export{GenericQuestionRenderer}from"./GenericQuestionRenderer";
|
|
1
|
+
export{ExamTakingPageContainer}from"./ExamTakingPageContainer";export{GenericQuestionRenderer}from"./GenericQuestionRenderer";export{useExamCountdown}from"./hooks/useExamCountdown";export{useAnswerAutosave}from"./hooks/useAnswerAutosave";export{useProctoringSnapshot}from"./hooks/useProctoringSnapshot";export{useAttemptSubmit}from"./hooks/useAttemptSubmit";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SpeakingCueCardContent {
|
|
2
|
+
/** Rich text HTML content of the cue card — generated by Tiptap Editor */
|
|
3
|
+
cueCardHtml: string;
|
|
4
|
+
/** Preparation time in seconds (default: 60) */
|
|
5
|
+
prepTimeSeconds?: number;
|
|
6
|
+
/** Max speaking time in seconds (default: 120) */
|
|
7
|
+
speakingTimeSeconds?: number;
|
|
8
|
+
/** Backup question flag */
|
|
9
|
+
isBackup?: boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speaking exam types — Cambridge Movers/Flyers
|
|
3
|
+
*
|
|
4
|
+
* Covers all 4 speaking question types rendered in the student exam-taking view.
|
|
5
|
+
* These match the `content` and `questionGroup.payload` shapes from the API.
|
|
6
|
+
*/
|
|
7
|
+
export interface CompareImagesContent {
|
|
8
|
+
audioUrl: string;
|
|
9
|
+
isBackup?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface CompareImagesGroupPayload {
|
|
12
|
+
candidateImageUrl: string;
|
|
13
|
+
examinerImageUrl: string;
|
|
14
|
+
}
|
|
15
|
+
export interface StoryImagesContent {
|
|
16
|
+
audioUrl: string;
|
|
17
|
+
images: string[];
|
|
18
|
+
isBackup?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface OddOneOutContent {
|
|
21
|
+
audioUrl: string;
|
|
22
|
+
activeImageId?: string;
|
|
23
|
+
images?: string[];
|
|
24
|
+
isBackup?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface OddOneOutGroupPayload {
|
|
27
|
+
images: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
url: string;
|
|
30
|
+
label: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export interface QuestionListContent {
|
|
34
|
+
audioUrl: string;
|
|
35
|
+
activeQuestionId: string;
|
|
36
|
+
isBackup?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface QuestionListGroupPayload {
|
|
39
|
+
content?: Array<{
|
|
40
|
+
id: string;
|
|
41
|
+
text: string;
|
|
42
|
+
}>;
|
|
43
|
+
questions?: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
export interface InfoExchangeContent {
|
|
49
|
+
audioUrl: string;
|
|
50
|
+
isBackup?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface InfoExchangeGroupPayload {
|
|
53
|
+
title: string;
|
|
54
|
+
imageUrl: string;
|
|
55
|
+
candidateFields: Array<{
|
|
56
|
+
key: string;
|
|
57
|
+
answer: string;
|
|
58
|
+
}>;
|
|
59
|
+
examinerFields: Array<{
|
|
60
|
+
key: string;
|
|
61
|
+
answer: string;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
export interface ReadDisplayedContentContent {
|
|
65
|
+
/** The reference text passage the student must read aloud */
|
|
66
|
+
referenceText: string;
|
|
67
|
+
/** Optional: audio URL of a model/native pronunciation for reference */
|
|
68
|
+
modelAudioUrl?: string;
|
|
69
|
+
/** Optional: difficulty tag for the passage */
|
|
70
|
+
difficultyTag?: 'BASIC_PHONICS' | 'DAILY_ROUTINE' | 'DESCRIPTIVE_EMOTIONAL';
|
|
71
|
+
/** Backup flag - consistent with other speaking types */
|
|
72
|
+
isBackup?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface SpontaneousQAContent {
|
|
75
|
+
/** How the admin created this question: uploaded audio or entered text */
|
|
76
|
+
inputType: 'AUDIO' | 'TEXT';
|
|
77
|
+
/** Audio URL — always present at client render time */
|
|
78
|
+
audioUrl?: string;
|
|
79
|
+
/** Question text — only visible in admin/review mode, not shown to student */
|
|
80
|
+
questionText?: string;
|
|
81
|
+
/** Backup flag — consistent with other speaking types */
|
|
82
|
+
isBackup?: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface GNSpeakingInterviewContent {
|
|
85
|
+
/** Examiner's question text */
|
|
86
|
+
questionText: string;
|
|
87
|
+
/** Audio URL for examiner's question (TTS or uploaded) */
|
|
88
|
+
audioUrl?: string;
|
|
89
|
+
/** How admin created the question */
|
|
90
|
+
inputType: 'AUDIO' | 'TEXT';
|
|
91
|
+
/** Which candidate this question is addressed to */
|
|
92
|
+
targetCandidate: 'VIRTUAL' | 'REAL';
|
|
93
|
+
/** Booklet image URL — collage for Part 2 Discussion with Pictures */
|
|
94
|
+
imageUrl?: string;
|
|
95
|
+
/** Virtual student's pre-recorded answer (filled by admin) */
|
|
96
|
+
virtualAnswer?: {
|
|
97
|
+
/** Text of the virtual student's answer */
|
|
98
|
+
answerText: string;
|
|
99
|
+
/** Audio URL — admin uploads or auto-generates via TTS */
|
|
100
|
+
audioUrl?: string;
|
|
101
|
+
};
|
|
102
|
+
/** Whether this is an Extended Response question */
|
|
103
|
+
isExtendedResponse?: boolean;
|
|
104
|
+
/** Backup flag */
|
|
105
|
+
isBackup?: boolean;
|
|
106
|
+
}
|
|
107
|
+
export interface GNSpeakingInterviewGroupPayload {
|
|
108
|
+
/** Which candidate role is virtual (pre-recorded) */
|
|
109
|
+
virtualCandidate: 'A' | 'B';
|
|
110
|
+
/** Virtual student display name */
|
|
111
|
+
virtualStudentName?: string;
|
|
112
|
+
/** Virtual student avatar image URL */
|
|
113
|
+
virtualStudentAvatar?: string;
|
|
114
|
+
}
|
|
115
|
+
/** Answer stored in examTakingStore for speaking questions */
|
|
116
|
+
export interface SpeakingAnswer {
|
|
117
|
+
audioUrl: string;
|
|
118
|
+
}
|
|
119
|
+
/** Recording state managed by useSpeakingRecorder hook */
|
|
120
|
+
export interface RecordingState {
|
|
121
|
+
status: 'idle' | 'recording' | 'recorded' | 'uploading' | 'uploaded' | 'error';
|
|
122
|
+
remainingSeconds: number;
|
|
123
|
+
isWarning: boolean;
|
|
124
|
+
durationSeconds: number;
|
|
125
|
+
audioBlob: Blob | null;
|
|
126
|
+
audioUrl: string | null;
|
|
127
|
+
uploadedKey: string | null;
|
|
128
|
+
error: string | null;
|
|
129
|
+
isAttemptExpired: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** Response from POST /api/v1/student/attempts/:id/audio-answer */
|
|
132
|
+
export interface UploadAudioAnswerResponse {
|
|
133
|
+
questionId: string;
|
|
134
|
+
audioKey: string;
|
|
135
|
+
answeredAt: string;
|
|
136
|
+
}
|
|
137
|
+
export type SpeakingConversationResponder = 'PARTNER' | 'STUDENT';
|
|
138
|
+
export interface SpeakingConversationContent {
|
|
139
|
+
interlocutorAudioUrl?: string;
|
|
140
|
+
partnerAudioUrl?: string;
|
|
141
|
+
responder: SpeakingConversationResponder;
|
|
142
|
+
questionText?: string;
|
|
143
|
+
isBackup?: boolean;
|
|
144
|
+
}
|
|
145
|
+
export interface SpeakingConversationGroupPayload {
|
|
146
|
+
images?: Array<{
|
|
147
|
+
id: string;
|
|
148
|
+
url: string;
|
|
149
|
+
label: string;
|
|
150
|
+
}>;
|
|
151
|
+
partnerName?: string;
|
|
152
|
+
partnerAvatarUrl?: string;
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|