@tinyweb_dev/oe-exam-sdk 0.1.3 → 0.1.5
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 +112 -13
- 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/api/result-review.d.ts +2 -1
- package/dist/api/result-review.js +1 -1
- package/dist/api/result-review.types.d.ts +69 -3
- package/dist/api/result-review.types.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 +17 -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 +16 -2
- package/dist/components/results/ResultReviewPageContainer.js +1 -1
- package/dist/components/results/ReviewQuestionRenderer.d.ts +11 -1
- package/dist/components/results/ReviewQuestionRenderer.js +1 -1
- package/dist/components/results/hooks/useResultReviewPolling.d.ts +37 -0
- package/dist/components/results/hooks/useResultReviewPolling.js +1 -0
- package/dist/components/results/index.d.ts +36 -1
- package/dist/components/results/index.js +1 -1
- package/dist/components/results/renderers/QuestionExplanations.d.ts +22 -0
- package/dist/components/results/renderers/QuestionExplanations.js +1 -0
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.d.ts +13 -0
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.d.ts +22 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewColoringRenderer.d.ts +16 -0
- package/dist/components/results/renderers/ReviewColoringRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +18 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListeningHeader.d.ts +44 -0
- package/dist/components/results/renderers/ReviewListeningHeader.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.d.ts +17 -0
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingMedia.d.ts +39 -0
- package/dist/components/results/renderers/ReviewSpeakingMedia.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.d.ts +5 -0
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.d.ts +3 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.d.ts +80 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +1 -0
- package/dist/components/results/renderers/review-question-body-dedicated.d.ts +15 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -0
- package/dist/components/results/renderers/review-question-body-movers.d.ts +17 -0
- package/dist/components/results/renderers/review-question-body-movers.js +1 -0
- package/dist/components/results/renderers/review-speaking-map.d.ts +7 -0
- package/dist/components/results/renderers/review-speaking-map.js +1 -0
- package/dist/components/results/report/BadgeTitle.d.ts +5 -0
- package/dist/components/results/report/BadgeTitle.js +1 -0
- package/dist/components/results/report/ExamSkillRow.d.ts +14 -0
- package/dist/components/results/report/ExamSkillRow.js +1 -0
- package/dist/components/results/report/OceanEduExamReport.d.ts +31 -0
- package/dist/components/results/report/OceanEduExamReport.js +1 -0
- package/dist/components/results/report/OceanEduSpeakingReport.d.ts +26 -0
- package/dist/components/results/report/OceanEduSpeakingReport.js +1 -0
- package/dist/components/results/report/OverallAssessmentSection.d.ts +24 -0
- package/dist/components/results/report/OverallAssessmentSection.js +1 -0
- package/dist/components/results/report/ResultErrorState.d.ts +14 -0
- package/dist/components/results/report/ResultErrorState.js +1 -0
- package/dist/components/results/report/ResultLoadingState.d.ts +11 -0
- package/dist/components/results/report/ResultLoadingState.js +1 -0
- package/dist/components/results/report/ResultNotPublicState.d.ts +16 -0
- package/dist/components/results/report/ResultNotPublicState.js +1 -0
- package/dist/components/results/report/SpeakingReportParts.d.ts +4 -0
- package/dist/components/results/report/SpeakingReportParts.js +1 -0
- package/dist/components/results/report/SpeakingReportShared.d.ts +36 -0
- package/dist/components/results/report/SpeakingReportShared.js +1 -0
- package/dist/components/results/report/SpeakingRubricSection.d.ts +21 -0
- package/dist/components/results/report/SpeakingRubricSection.js +1 -0
- package/dist/components/results/report/assets/dofu2.png +0 -0
- package/dist/components/results/report/assets/dofu4.png +0 -0
- package/dist/components/results/report/assets/logo-ocean.png +0 -0
- package/dist/components/results/report/assets/penguin-main.png +0 -0
- package/dist/components/results/report/assets/penguin-small.png +0 -0
- package/dist/components/results/report/speaking-report.utils.d.ts +37 -0
- package/dist/components/results/report/speaking-report.utils.js +1 -0
- package/dist/components/results/review-data.d.ts +1 -0
- package/dist/components/results/review-data.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/results/review-types.d.ts +14 -0
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.d.ts +17 -0
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +1 -0
- package/dist/components/results/speaking/SpeakingReviewResult.d.ts +30 -0
- package/dist/components/results/speaking/SpeakingReviewResult.js +1 -0
- package/dist/components/results/utils/review-score.utils.d.ts +21 -0
- package/dist/components/results/utils/review-score.utils.js +1 -0
- package/dist/components/results/utils/speaking-summary.d.ts +41 -0
- package/dist/components/results/utils/speaking-summary.js +1 -0
- package/dist/components/results/utils/transform-speaking-review.d.ts +62 -0
- package/dist/components/results/utils/transform-speaking-review.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 +12 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCheck as s,Clock as r,MessageSquare as l,MessageSquareQuote as a,Music2 as n,Volume2 as i,Zap as d}from"lucide-react";import{ReviewGradingStatus as c}from"../../../api/result-review";import{ResolvedImage as x}from"../../../components/common/ResolvedImage";import{cn as o}from"../../../shared/lib/utils";import{SpeakingResultAudioPlayer as m}from"../speaking/SpeakingResultAudioPlayer";import{deriveQuestionSkillMetrics as p,extractPrompt as u,extractQuestionFeedback as h,extractStudentAnswerText as f,formatScore as b,getPendingGradingLabel as g,getQuestionTypeLabel as N,isGradingComplete as w}from"./speaking-report.utils";export function InfoRow({icon:s,label:r,value:l}){return t("p",{className:"flex items-start text-[13px] font-medium text-[#333] xs:text-sm sm:text-base md:text-lg",children:[e(s,{className:"mr-2 mt-0.5 h-[14px] w-[14px] shrink-0 text-[#0054A6] xs:h-[16px] xs:w-[16px] sm:mr-3 sm:h-[18px] sm:w-[18px]"}),t("span",{className:"flex flex-wrap items-baseline gap-x-1.5",children:[t("span",{className:"whitespace-nowrap",children:[r," :"]}),e("strong",{suppressHydrationWarning:!0,className:"font-bold text-[#0054A6]",children:l})]})]})}function v({metric:t}){const r="h-[18px] w-[18px] text-white";switch(t.icon){case"speaker":return e(i,{className:r});case"bolt":return e(d,{className:r});case"message":return e(l,{className:r});case"check-double":return e(s,{className:r});case"music":return e(n,{className:r});default:return null}}export function SkillRow({metric:s,compact:r=!1}){const l=b(r?s.score/10:s.score,1),a=r?"/10":"/100";return t("div",{className:o("flex items-center rounded-full border border-[#eef4fb] bg-white shadow-[0_1px_0_rgba(255,255,255,0.9)] last:mb-0",r?"min-h-[70px] pl-[12px] pr-4":"mb-2 px-3 py-2 sm:mb-3 sm:px-4 sm:py-3"),children:[e("div",{className:"flex h-[48px] w-[48px] shrink-0 items-center justify-center rounded-full bg-white shadow-[0_4px_12px_rgba(15,23,42,0.08)] xs:h-[52px] xs:w-[52px] sm:h-[58px] sm:w-[58px]",children:e("div",{className:o("flex items-center justify-center rounded-full text-white",r?"h-[42px] w-[42px]":"h-[36px] w-[36px] xs:h-[38px] xs:w-[38px] sm:h-[42px] sm:w-[42px]",s.barClass),children:e(v,{metric:s})})}),t("div",{className:o("ml-2 mr-2 flex-1 border-r border-dashed border-[#d9e0ea] pr-2 xs:ml-3 xs:mr-3 xs:pr-3 sm:pr-4",r?"w-[148px]":"min-w-0"),children:[e("h4",{className:o("truncate font-extrabold leading-5 text-[#0054A6]",r?"text-[15px]":"text-[13px] xs:text-[14px] sm:text-[15px]"),children:s.label}),e("span",{className:o("mt-0.5 block truncate text-[#8b97a8] sm:mt-1",r?"text-[11px]":"text-[10px] xs:text-[10.5px] sm:text-[11px]"),children:s.sublabel})]}),t("div",{className:o("shrink-0 font-extrabold text-[#0054A6]",r?"w-[86px] text-[18px]":"w-[60px] text-[16px] xs:w-[70px] xs:text-[17px] sm:w-20 sm:text-xl"),children:[l," ",e("small",{className:o("font-semibold text-[#c5ced8]",r?"text-[12px]":"text-[11px] xs:text-[11.5px] sm:text-[13px]"),children:a})]}),e("div",{className:o("ml-2 flex-1 overflow-hidden rounded-full bg-[#edf0f4] xs:ml-2.5 sm:ml-3",r?"h-[12px]":"h-[10px] sm:h-[11px]"),children:e("div",{className:o("h-full rounded-full",s.barClass),style:{width:`${s.score}%`}})})]})}export function PendingGradingCard({gradingStatus:s}){const l=g(s),a=s===c.FAILED;return t("div",{className:o("flex flex-col items-center justify-center gap-3 rounded-2xl border px-6 py-8 text-center",a?"border-orange-200 bg-orange-50":"border-[#dbeafe] bg-[#F4F9FF]"),children:[e("div",{className:o("flex h-14 w-14 items-center justify-center rounded-full",a?"bg-orange-100":"bg-[#E8F6FF]"),children:e(r,{className:o("h-7 w-7 animate-pulse",a?"text-orange-500":"text-[#00AEEF]")})}),t("div",{children:[e("p",{className:o("text-base font-bold",a?"text-orange-600":"text-[#0054A6]"),children:l}),e("p",{className:"mt-1 text-sm text-slate-500",children:"Kết quả sẽ được cập nhật sớm nhất có thể."})]})]})}export function NotAnsweredCard(){return t("div",{className:"flex flex-col items-center justify-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-6 py-8 text-center",children:[e("div",{className:"flex h-14 w-14 items-center justify-center rounded-full bg-slate-100",children:e(i,{className:"h-7 w-7 text-slate-300"})}),t("div",{children:[e("p",{className:"text-base font-bold text-slate-500",children:"Học sinh chưa trả lời câu hỏi"}),e("p",{className:"mt-1 text-sm text-slate-400",children:"Câu hỏi này không có bài ghi âm."})]})]})}export function QuestionScorePill({question:s}){return s.gradingStatus===c.NOT_ANSWERED?t("div",{className:"inline-flex items-center rounded-full bg-slate-100 px-3.5 py-2 text-sm font-bold text-slate-500 shadow-sm ring-1 ring-slate-200",children:["Bỏ qua — 0 / ",b(s.maxScore,0)]}):w(s.gradingStatus)?e("div",{className:"inline-flex items-center rounded-full bg-[#E8F6FF] px-3.5 py-2 text-sm font-extrabold text-[#0054A6] shadow-sm ring-1 ring-[#b9dcff]",children:null==s.score?`Chưa chấm / ${b(s.maxScore,0)}`:`${b(s.score,2)} / ${b(s.maxScore,0)}`}):t("div",{className:"inline-flex items-center gap-1.5 rounded-full bg-[#FFF8E8] px-3.5 py-2 text-sm font-bold text-[#D97706] shadow-sm ring-1 ring-[#fcd34d]",children:[e(r,{className:"h-3.5 w-3.5"}),"Chờ chấm"]})}export function PromptBlock({question:s}){const r=s.questionContent??{},l=r.imageUrl||r.image||r.promptImageUrl,a=s.audioUrl||r.audioUrl||r.audio||r.questionAudio,n=u(s);return t("div",{className:"rounded-xl border border-[#dbeafe] bg-[#F8FBFF] px-4 py-3",children:[e("div",{className:"mb-2 text-xs font-bold uppercase tracking-wide text-[#00AEEF]",children:"Câu hỏi"}),n.text&&"Nội dung câu hỏi đang được cập nhật."!==n.text&&(n.isHtml?e("div",{className:"mb-3 text-sm leading-relaxed text-slate-700 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:my-0.5 [&_p]:my-1",dangerouslySetInnerHTML:{__html:n.text}}):e("p",{className:"mb-3 text-center text-sm leading-relaxed text-slate-700",children:n.text})),a?e("div",{className:"mb-3",children:e(m,{audioKey:a})}):null,l?e("div",{className:"overflow-hidden rounded-lg border border-slate-200",children:e(x,{src:l,alt:"Question image",className:"h-auto w-full"})}):null]})}export function AnswerBlock({question:s}){const r=f(s);return s.studentAnswerKey?t("div",{className:"rounded-xl border border-slate-200 bg-white px-4 py-3",children:[t("div",{className:"mb-2.5 flex items-center gap-2 text-xs font-bold text-[#0054A6]",children:[e(i,{className:"h-3.5 w-3.5 text-[#00AEEF]"}),"Bài nói của học viên"]}),e(m,{audioKey:s.studentAnswerKey})]}):t("div",{className:"rounded-xl border border-slate-200 bg-slate-50 px-4 py-3",children:[e("div",{className:"mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-400",children:"Câu trả lời của học viên"}),e("p",{className:"text-sm leading-relaxed text-slate-600",children:r})]})}export function QuestionComponentScores({question:s}){const r=p(s);return t("div",{className:"rounded-[28px] border border-[#cfe4ff] bg-[#F3F8FF] px-4 py-5 md:px-5 md:py-6",children:[e("div",{className:"mb-5 text-center text-[20px] font-black uppercase tracking-tight text-[#0054A6]",children:"ĐIỂM THÀNH PHẦN"}),e("div",{className:"space-y-3",children:r.map(t=>e(SkillRow,{metric:t,compact:!0},`${s.questionId}-${t.key}`))})]})}export function FeedbackBlock({question:s}){return t("div",{className:"rounded-xl border border-[#ffe6b3] bg-[#FFF8E8] px-4 py-3",children:[t("div",{className:"mb-1.5 flex items-center gap-1.5 text-xs font-bold text-[#D97706]",children:[e(a,{className:"h-3.5 w-3.5"}),"Nhận xét AI"]}),e("p",{className:"text-sm leading-relaxed text-slate-700",children:h(s)})]})}export function QuestionCard({question:s}){const r=w(s.gradingStatus),l=s.gradingStatus===c.NOT_ANSWERED;return t("article",{className:"rounded-[28px] border border-[#cfe4ff] bg-white p-4 shadow-[0_10px_28px_rgba(15,23,42,0.06)] md:p-6",children:[e("div",{className:"mb-5 rounded-[22px] border border-[#dbeafe] bg-[linear-gradient(135deg,#f8fbff_0%,#eef6ff_100%)] px-4 py-4 md:px-5",children:t("div",{className:"flex flex-col gap-4 md:flex-row md:items-start md:justify-between",children:[t("div",{className:"min-w-0",children:[t("div",{className:"text-xl font-black text-[#0054A6]",children:["Câu ",s.questionNumber]}),e("div",{className:"mt-1 text-sm font-semibold text-slate-500",children:N(s.questionType)})]}),e("div",{className:"shrink-0 self-start",children:e(QuestionScorePill,{question:s})})]})}),t("div",r?{className:"grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] xl:items-start",children:[t("div",{className:"space-y-4",children:[e(PromptBlock,{question:s}),e(AnswerBlock,{question:s}),e(FeedbackBlock,{question:s})]}),e("aside",{className:"min-w-0",children:e(QuestionComponentScores,{question:s})})]}:l?{className:"space-y-4",children:[e(PromptBlock,{question:s}),e(NotAnsweredCard,{})]}:{className:"space-y-4",children:[e(PromptBlock,{question:s}),e(PendingGradingCard,{gradingStatus:s.gradingStatus})]})]})}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SpeakingRubric } from '../../../api/result-review';
|
|
2
|
+
export declare enum SpeakingRubricScoringVersion {
|
|
3
|
+
TEENS_FOCUS_PEARSON_V2 = "TEENS_FOCUS_PEARSON_V2",
|
|
4
|
+
PRESCHOOL_PLACEMENT_V1 = "PRESCHOOL_PLACEMENT_V1",
|
|
5
|
+
KINDY_PLACEMENT_V1 = "KINDY_PLACEMENT_V1"
|
|
6
|
+
}
|
|
7
|
+
export declare enum KidsRubricSectionCode {
|
|
8
|
+
WARM_UP = "WARM_UP",
|
|
9
|
+
PART_1 = "PART_1",
|
|
10
|
+
PART_2 = "PART_2",
|
|
11
|
+
PART_3 = "PART_3"
|
|
12
|
+
}
|
|
13
|
+
export interface SpeakingRubricSectionProps {
|
|
14
|
+
rubric: SpeakingRubric;
|
|
15
|
+
badgeNumber?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Attempt-level speaking rubric breakdown for TEENS / PRESCHOOL / KINDY / KIDS.
|
|
19
|
+
* Source: oe-exam-fe/.../review/components/SpeakingRubricSection.tsx
|
|
20
|
+
*/
|
|
21
|
+
export declare function SpeakingRubricSection({ rubric, badgeNumber, }: SpeakingRubricSectionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as n}from"react/jsx-runtime";import{Star as t}from"lucide-react";import{BadgeTitle as i}from"./BadgeTitle";export var SpeakingRubricScoringVersion;!function(e){e.TEENS_FOCUS_PEARSON_V2="TEENS_FOCUS_PEARSON_V2",e.PRESCHOOL_PLACEMENT_V1="PRESCHOOL_PLACEMENT_V1",e.KINDY_PLACEMENT_V1="KINDY_PLACEMENT_V1"}(SpeakingRubricScoringVersion||(SpeakingRubricScoringVersion={}));export var KidsRubricSectionCode;!function(e){e.WARM_UP="WARM_UP",e.PART_1="PART_1",e.PART_2="PART_2",e.PART_3="PART_3"}(KidsRubricSectionCode||(KidsRubricSectionCode={}));const c={[SpeakingRubricScoringVersion.TEENS_FOCUS_PEARSON_V2]:{C1:{vi:"Ngữ pháp & Từ vựng",en:"Grammar & Vocabulary",hint:"Cấu trúc câu, thì động từ và độ đa dạng từ vựng"},C2:{vi:"Phát âm",en:"Pronunciation",hint:"Độ rõ ràng, trọng âm từ và ngữ điệu câu"},C3:{vi:"Giao tiếp tương tác",en:"Interactive Communication",hint:"Phản hồi đủ ý và tự mở rộng câu trả lời"},C4:{vi:"Quản lý diễn ngôn",en:"Discourse Management",hint:"Mạch lạc, tổ chức và phát triển ý rõ ràng"}},[SpeakingRubricScoringVersion.PRESCHOOL_PLACEMENT_V1]:{C1:{vi:"Nhận biết số đếm (1–10)",en:"Cardinal Numbers (1–10)",hint:"Đếm 1–10 và nhận ra từng số"},C2:{vi:"Nhận biết màu sắc",en:"Basic Colours",hint:"Gọi tên màu sắc và màu yêu thích"},C3:{vi:"Nhận biết hình học",en:"Basic Shapes",hint:"Gọi tên các hình cơ bản"},C4:{vi:"Nhận biết động vật",en:"Animals",hint:"Gọi tên động vật và con vật yêu thích"}},[SpeakingRubricScoringVersion.KINDY_PLACEMENT_V1]:{C1:{vi:"Chào hỏi & Giới thiệu bản thân",en:"Greeting & Self-Introduction",hint:"Giao tiếp xã hội cơ bản"},C2:{vi:"Nhận biết số đếm (1–20)",en:"Cardinal Numbers (1–20)",hint:"Từ vựng số học"},C3:{vi:"Đọc bảng chữ cái",en:"The Alphabet",hint:"Nhận biết chữ cái"},C4:{vi:"Nhận biết hình dạng",en:"Shapes",hint:"Từ vựng hình học"},C5:{vi:"Nhận biết màu sắc",en:"Colours",hint:"Từ vựng màu sắc"},C6:{vi:"Nhận biết động vật",en:"Animals",hint:"Từ vựng chủ đề quen thuộc"},C7:{vi:"Nhận biết thức ăn",en:"Food/Drinks",hint:"Từ vựng chủ đề quen thuộc"}}},r={5:{text:"#166534",bg:"#dcfce7",accent:"#16a34a"},4:{text:"#15803d",bg:"#dcfce7",accent:"#22c55e"},3:{text:"#b45309",bg:"#fef3c7",accent:"#f59e0b"},2:{text:"#c2410c",bg:"#ffedd5",accent:"#f97316"},1:{text:"#b91c1c",bg:"#fee2e2",accent:"#ef4444"}},a={5:"Excellent",4:"Good",3:"Adequate",2:"Weak",1:"Very weak"},s={5:"Vượt kỳ vọng",4:"Đáp ứng kỳ vọng (Cao)",3:"Đáp ứng kỳ vọng (Thấp)",2:"Dưới kỳ vọng",1:"Không thực hiện"},l={[SpeakingRubricScoringVersion.TEENS_FOCUS_PEARSON_V2]:a,[SpeakingRubricScoringVersion.PRESCHOOL_PLACEMENT_V1]:s,[SpeakingRubricScoringVersion.KINDY_PLACEMENT_V1]:s},o={[KidsRubricSectionCode.WARM_UP]:{vi:"Khởi động",en:"Warm-up Questions"},[KidsRubricSectionCode.PART_1]:{vi:"Part 1 — Nhận diện qua tranh",en:"Picture Identification"},[KidsRubricSectionCode.PART_2]:{vi:"Part 2 — Đọc và cung cấp thông tin",en:"Reading and Giving Information"},[KidsRubricSectionCode.PART_3]:{vi:"Part 3 — Mô tả tranh",en:"Picture Description"}};function d({score:n}){const i=Math.max(0,Math.min(5,Math.round(n)));return e("span",{className:"inline-flex items-center gap-0.5","aria-label":`${i}/5`,children:Array.from({length:5}).map((n,c)=>e(t,{"aria-hidden":!0,className:c<i?"h-4 w-4 fill-[#FFC107] text-[#FFC107]":"h-4 w-4 fill-[#e6edf5] text-[#e6edf5]"},c))})}function h({rubric:t}){const i=c[t.scoringVersion]??{},s=l[t.scoringVersion]??a;return e("div",{className:"overflow-hidden rounded-xl border border-[#00AEEF]/20 shadow-sm",children:e("div",{className:"overflow-x-auto",children:n("table",{className:"w-full min-w-[640px] text-left text-sm text-slate-700",children:[e("thead",{className:"border-b border-[#00AEEF]/20 bg-[#f0f8ff] text-[#0054A6]",children:n("tr",{className:"text-center",children:[e("th",{scope:"col",className:"w-[30%] border-r border-[#00AEEF]/10 px-4 py-3 font-bold",children:"Tiêu chí"}),e("th",{scope:"col",className:"border-r border-[#00AEEF]/10 px-4 py-3 font-bold",children:"Nhận xét chi tiết"}),e("th",{scope:"col",className:"w-[22%] px-4 py-3 font-bold",children:"Điểm"})]})}),e("tbody",{className:"divide-y divide-slate-100",children:t.criteria.map(t=>{const c=i[t.code]??{vi:t.code,en:"",hint:""},a=Math.round(t.score),l=Math.max(1,Math.min(5,a)),o=s[l],h=r[l];return n("tr",{className:"align-top transition-colors hover:bg-slate-50/70",children:[n("td",{className:"border-r border-[#00AEEF]/10 px-4 py-4",style:{boxShadow:`inset 4px 0 0 ${h.accent}`},children:[e("div",{className:"font-bold text-slate-800",children:c.vi}),c.en?e("div",{className:"text-xs italic text-slate-500",children:c.en}):null,c.hint?e("div",{className:"mt-1 text-[11px] leading-snug text-slate-400",children:c.hint}):null]}),e("td",{className:"border-r border-[#00AEEF]/10 px-4 py-4 leading-relaxed text-slate-700",children:t.evidence?.trim()?t.evidence:e("span",{className:"italic text-slate-400",children:"Không có nhận xét chi tiết."})}),n("td",{className:"px-4 py-4",children:[e(d,{score:t.score}),n("div",{className:"mt-1.5 flex items-baseline gap-1",children:[e("span",{className:"text-lg font-black leading-none text-[#0054A6]",children:a}),e("span",{className:"text-xs font-semibold text-slate-400",children:"/ 5"})]}),o?e("span",{className:"mt-1.5 inline-block whitespace-nowrap rounded-full px-2.5 py-0.5 text-xs font-bold",style:{backgroundColor:h.bg,color:h.text},children:o}):null]})]},t.code)})})]})})})}function m({section:t}){const i=o[t.code]??{vi:t.code,en:""},c=t.maxScore>0?Math.max(0,Math.min(100,t.score/t.maxScore*100)):0;return n("div",{className:"overflow-hidden rounded-xl border border-[#00AEEF]/20 shadow-sm",children:[n("div",{className:"flex flex-col gap-2 border-b border-[#00AEEF]/20 bg-[#f0f8ff] px-4 py-3 sm:flex-row sm:items-center sm:justify-between",children:[n("div",{children:[e("div",{className:"font-bold text-[#0054A6]",children:i.vi}),i.en?e("div",{className:"text-xs italic text-slate-500",children:i.en}):null]}),n("div",{className:"flex items-center gap-3",children:[e("div",{className:"h-[9px] w-24 overflow-hidden rounded-full bg-[#e6edf5]",children:e("div",{className:"h-full rounded-full bg-[#4CAF50]",style:{width:`${c}%`}})}),n("div",{className:"whitespace-nowrap text-sm font-black text-[#0054A6]",children:[t.score,n("span",{className:"text-xs font-semibold text-slate-400",children:[" / ",t.maxScore]})]})]})]}),e("table",{className:"w-full text-left text-sm text-slate-700",children:e("tbody",{className:"divide-y divide-[#00AEEF]/10",children:t.questions.map(t=>n("tr",{className:"align-top hover:bg-slate-50",children:[e("td",{className:"w-[72px] px-4 py-3",children:n("span",{className:"inline-flex h-7 min-w-7 items-center justify-center rounded-full bg-[#eaf4ff] px-2 text-xs font-black text-[#0054A6]",children:["Câu ",t.questionNo]})}),n("td",{className:"w-[88px] whitespace-nowrap px-2 py-3 font-black text-[#0054A6]",children:[t.score,n("span",{className:"text-xs font-semibold text-slate-400",children:[" / ",t.maxScore]})]}),e("td",{className:"px-4 py-3 leading-relaxed text-slate-700",children:t.evidence?.trim()?t.evidence:e("span",{className:"italic text-slate-400",children:"Không có nhận xét chi tiết."})})]},t.questionNo))})})]})}export function SpeakingRubricSection({rubric:t,badgeNumber:c="4"}){const r=t.criteria?.length>0,a=(t.sections?.length??0)>0;return r||a?n("section",{className:"relative mt-8 rounded-[20px] border-2 border-[#00AEEF] px-5 pb-6 pt-11 md:px-8",children:[e(i,{number:c,title:"ĐÁNH GIÁ KỸ NĂNG NÓI"}),a?e("div",{className:"space-y-4",children:(t.sections??[]).map(n=>e(m,{section:n},n.code))}):e(h,{rubric:t})]}):null}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SpeakingReviewPart, SpeakingReviewQuestion } from '../utils/transform-speaking-review';
|
|
2
|
+
import { type SkillMetric as SharedSkillMetric } from '../utils/speaking-summary';
|
|
3
|
+
export type LocalizedText = string | {
|
|
4
|
+
vi?: string;
|
|
5
|
+
en?: string;
|
|
6
|
+
} | null | undefined;
|
|
7
|
+
export type SkillUiIcon = 'speaker' | 'bolt' | 'message' | 'check-double' | 'music';
|
|
8
|
+
export interface SkillMetric extends SharedSkillMetric {
|
|
9
|
+
icon: SkillUiIcon;
|
|
10
|
+
colorVarClass: string;
|
|
11
|
+
barClass: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const SKILL_UI_BY_KEY: Record<SharedSkillMetric['key'], Pick<SkillMetric, 'icon' | 'colorVarClass' | 'barClass'>>;
|
|
14
|
+
export declare function withUi(metric: SharedSkillMetric): SkillMetric;
|
|
15
|
+
export declare function getLocalizedText(value: LocalizedText): string;
|
|
16
|
+
export declare function formatScore(score: number | null | undefined, digits?: number): string;
|
|
17
|
+
/** Format total score for "ĐIỂM TỔNG" display */
|
|
18
|
+
export declare function formatTotalScore(score: number | null | undefined): string;
|
|
19
|
+
export declare function getScoreStroke(totalScore: number | null, maxScore: number, isPendingGrading: boolean): string;
|
|
20
|
+
export declare function getQuestionTypeLabel(questionType: string): string;
|
|
21
|
+
export declare function isGradingComplete(gradingStatus: string | null | undefined): boolean;
|
|
22
|
+
export declare function getPendingGradingLabel(gradingStatus: string | null | undefined): string;
|
|
23
|
+
export declare function deriveQuestionSkillMetrics(question: SpeakingReviewQuestion): SkillMetric[];
|
|
24
|
+
export declare function extractPrompt(question: SpeakingReviewQuestion): {
|
|
25
|
+
text: string;
|
|
26
|
+
isHtml: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare function extractStudentAnswerText(question: SpeakingReviewQuestion): string;
|
|
29
|
+
export declare function extractQuestionFeedback(question: SpeakingReviewQuestion): string;
|
|
30
|
+
export declare function getPartScore(part: SpeakingReviewPart): {
|
|
31
|
+
score: number | null;
|
|
32
|
+
maxScore: number;
|
|
33
|
+
hasPending: boolean;
|
|
34
|
+
};
|
|
35
|
+
export declare function resolveAssetUrl(asset: {
|
|
36
|
+
src: string;
|
|
37
|
+
} | string, assetBaseUrl?: string, fileName?: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ReviewGradingStatus as e}from"../../../api/result-review";import{buildSkillMetrics as t}from"../utils/speaking-summary";export const SKILL_UI_BY_KEY={pronunciation:{icon:"speaker",colorVarClass:"text-[#4CAF50]",barClass:"bg-[#4CAF50]"},fluency:{icon:"bolt",colorVarClass:"text-[#2196F3]",barClass:"bg-[#2196F3]"},vocabulary:{icon:"message",colorVarClass:"text-[#FF9800]",barClass:"bg-[#FF9800]"},grammar:{icon:"check-double",colorVarClass:"text-[#9C27B0]",barClass:"bg-[#9C27B0]"},coherence:{icon:"music",colorVarClass:"text-[#00BCD4]",barClass:"bg-[#00BCD4]"}};const n={READ_DISPLAYED_CONTENT:"Read displayed content",SPEAKING_DESCRIBE_IMAGE:"Describe image",SPONTANEOUS_QA:"Spontaneous Q&A"},i={[e.PENDING]:"Đang chờ chấm điểm",[e.QUEUED]:"Đang xếp hàng chờ chấm điểm",[e.PROCESSING]:"Đang chấm điểm...",[e.MANUAL_REVIEW]:"Đang chờ giáo viên xem xét",[e.FAILED]:"Chấm điểm thất bại — đang xử lý lại"};export function withUi(e){return{...e,...SKILL_UI_BY_KEY[e.key]}}export function getLocalizedText(e){return"string"==typeof e?e:e&&"object"==typeof e?"string"==typeof e.vi&&e.vi.trim()?e.vi:"string"==typeof e.en&&e.en.trim()?e.en:"":""}export function formatScore(e,t=1){if(null==e||Number.isNaN(e))return"—";return e.toFixed(t).replace(/\.0+$/,"").replace(/(\.\d*[1-9])0+$/,"$1")}export function formatTotalScore(e){return null!=e&&Number.isFinite(e)?e<10?e.toFixed(1):String(Math.round(e)):"—"}export function getScoreStroke(e,t,n){if(n||null==e||t<=0)return"conic-gradient(#cbd5e1 0% 100%, #eef5ff 100% 100%)";const i=Math.max(0,Math.min(100,e/t*100));return`conic-gradient(#00AEEF 0% ${i}%, #eef5ff ${i}% 100%)`}export function getQuestionTypeLabel(e){return n[e]??e.replace(/_/g," ")}export function isGradingComplete(t){return t===e.GRADED||t===e.AUTO_GRADED}export function getPendingGradingLabel(t){return t?i[t]??i[e.PENDING]:i[e.PENDING]}export function deriveQuestionSkillMetrics(e){const n=e.aiAssessment??null,i=e.llmAssessment??null;return t({pronunciation:n?.pronunciation?.pronunciationScore??null,fluency:n?.pronunciation?.fluencyScore??null,vocabulary:i?.contentScore??null,grammar:n?.pronunciation?.accuracyScore??null,coherence:n?.pronunciation?.prosodyScore??null}).map(withUi)}export function extractPrompt(e){const t=e.questionContent??{};if("SPEAKING_CUE_CARD"===e.questionType&&"string"==typeof t.cueCardHtml&&t.cueCardHtml.trim())return{text:t.cueCardHtml,isHtml:!0};const n=[getLocalizedText(t.title),getLocalizedText(t.prompt),getLocalizedText(t.question),getLocalizedText(t.questionText),getLocalizedText(t.instruction),getLocalizedText(t.content),getLocalizedText(t.text),getLocalizedText(t.paragraph),getLocalizedText(t.referenceText),getLocalizedText(t.cueCard),getLocalizedText(t.topic)].filter(e=>e.trim().length>0);if(n.length>0)return{text:n[0],isHtml:!1};if("READ_DISPLAYED_CONTENT"===e.questionType){const e=[getLocalizedText(t.displayedContent),getLocalizedText(t.passage),getLocalizedText(t.sentence),getLocalizedText(t.referenceText)].filter(e=>e.trim().length>0);if(e.length>0)return{text:e.join(" "),isHtml:!1}}if("SPONTANEOUS_QA"===e.questionType){const e=(Array.isArray(t.questions)?t.questions:[]).map(e=>{const t=e;return getLocalizedText(t.text||t.question||t.content)}).filter(e=>e.trim().length>0);if(e.length>0)return{text:e.join(" / "),isHtml:!1}}return{text:"Nội dung câu hỏi đang được cập nhật.",isHtml:!1}}export function extractStudentAnswerText(e){const t=e.studentAnswer??{},n=[getLocalizedText(t.transcript),getLocalizedText(t.recognizedText),getLocalizedText(t.answerText),getLocalizedText(t.text),getLocalizedText(t.content),getLocalizedText(t.finalText),getLocalizedText(t.response)].filter(e=>e.trim().length>0);return n.length>0?n[0]:e.studentAnswerKey?"Đã gửi file ghi âm.":"Chưa có câu trả lời."}export function extractQuestionFeedback(e){const t=e.classification??null,n=[getLocalizedText(e.llmAssessment?.feedback),getLocalizedText(e.aiAssessment?.feedback),getLocalizedText(t?.feedback),getLocalizedText(t?.description),getLocalizedText(t?.title)].filter(e=>e.trim().length>0);if(n.length>0)return n[0];if(e.isPendingGrading)return"Câu hỏi này đang chờ chấm điểm chi tiết.";if(null==e.score)return"Chưa có nhận xét chi tiết cho câu hỏi này.";const i=e.maxScore>0?e.score/e.maxScore:0;return i>=.85?"Câu trả lời tốt, tiếp tục duy trì phong độ này.":i>=.6?"Câu trả lời khá ổn, có thể cải thiện thêm độ rõ ràng và tự tin.":"Nên luyện thêm để cải thiện độ chính xác và cách diễn đạt ở câu hỏi này."}export function getPartScore(e){const t=e.questions.reduce((e,t)=>e+(Number.isFinite(t.maxScore)?t.maxScore:0),0),n=e.questions.some(e=>!isGradingComplete(e.gradingStatus)),i=e.questions.filter(e=>isGradingComplete(e.gradingStatus)).map(e=>e.score).filter(e=>"number"==typeof e&&Number.isFinite(e));return{score:i.length>0?i.reduce((e,t)=>e+t,0):null,maxScore:t,hasPending:n}}export function resolveAssetUrl(e,t,n){if(t&&n){return`${t.replace(/\/$/,"")}/${n}`}return"string"==typeof e?e:e.src}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { ReviewAnswer, ReviewTransformResult, ResultReviewTemplatePart } from './review-types';
|
|
2
2
|
export declare function transformResultAnswersForReview(answers: ReviewAnswer[], templateParts?: ResultReviewTemplatePart[]): ReviewTransformResult;
|
|
3
|
+
export declare const transformDetailedResultForReview: typeof transformResultAnswersForReview;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function transformResultAnswersForReview(
|
|
1
|
+
const e=new Set(["READ_PASSAGE_AND_ANSWER_QUESTIONS","LOOK_PICTURE_FILL_WORD_HINT","LABEL_THE_PICTURE","READ_AND_COLOR_OBJECTS","IMAGE_OBJECT_MATCHING"]);export function transformResultAnswersForReview(e,s=[]){const o=function(e){const r=new Map;return e.forEach(e=>{const t=[e.partId??"unknown",e.questionGroup?.id??"none",e.questionType].join(":");r.set(t,[...r.get(t)??[],e])}),r}(e),a={parts:[],studentAnswers:{},correctAnswersMap:new Map,isCorrectMap:{},correctAnswerTextMap:{},correctOptionIdMap:{},cellAnswersMap:{},essayGradingMap:{},partScoresMap:{},rawCorrectAnswerMap:{},rawStudentAnswerMap:{},questionScoreMap:{}},c=[];for(const e of o.values()){e.sort((e,r)=>e.questionNumber-r.questionNumber);const o=e[0];if(!o)continue;const u=s.find(e=>String(e.id)===String(o.partId)),p=r(u,o.questionNumber),A=i(u?.name)??o.partNo??0;a.parts.push({partId:o.partId,partNo:A,questionType:o.questionType,name:u?.name,instructions:p?.name||p?.instructions||u?.instructions,audioUrl:u?.audioUrl,questions:e.map(t)});const _=e.filter(e=>!e.isExample);c.push({score:_.reduce((e,r)=>e+(r.score??0),0),maxScore:_.reduce((e,r)=>e+r.maxScore,0),correctCount:_.filter(e=>!0===e.isCorrect).length,totalCount:_.length}),e.forEach(e=>n(a,e))}const u=a.parts.map((e,r)=>r).sort((e,r)=>{const t=a.parts[e],n=a.parts[r];return t.partNo-n.partNo||(t.questions[0]?.question_number??0)-(n.questions[0]?.question_number??0)});return a.parts=u.map(e=>a.parts[e]),u.forEach((e,r)=>{a.partScoresMap[r]=c[e]}),a}export const transformDetailedResultForReview=transformResultAnswersForReview;function r(e,r){const t=r-1;return e?.sections?.find(e=>null!=e.startIndex&&null!=e.endIndex&&t>=e.startIndex&&t<=e.endIndex)}function t(e){const r=o(e.questionContent);return{id:e.questionId,content:{...r,_correctAnswer:e.correctAnswer},correct_answer:e.correctAnswer,total_points:String(e.maxScore),question_type:e.questionType,is_example:e.isExample??!1,explanation:e.explanation??"",difficult_level:"MEDIUM",question_skill_type:"",exam_id:"",part_id:e.partId??"",part_no:e.partNo??0,question_number:e.questionNumber,questionGroup:e.questionGroup??null,score:e.score}}function n(r,t){const n=function(e,r,t){if(null==t)return;const n=o(t);if("CHOOSE_THE_CORRECT_ANSWER"===r||"CHOOSE_CORRECT_ADJECTIVE"===r){const r=s(e,n.answer);return r.length>1?r:r[0]}if("READ_PASSAGE_AND_ANSWER_QUESTIONS"===r&&a(n.subAnswers))return Object.fromEntries(Object.entries(n.subAnswers).map(([e,r])=>[e,"number"==typeof r?`${e}-opt-${r}`:r]));if("LABEL_THE_PICTURE"===r)return n.answers??t;if("LOOK_PICTURE_FILL_WORD_HINT"===r&&"string"==typeof n.answer&&n.answer.includes(","))return Object.fromEntries(n.answer.split(",").map((r,t)=>[`${e}-blank-${t}`,r.trim()]));if("WORD_FILL_PARAGRAPH"===r){if(Array.isArray(n.answers))return n.answers;if(Array.isArray(n.answer))return n.answer;if(Array.isArray(t))return t;if("string"==typeof t)return[t];return Object.keys(n).filter(e=>/^\d+$/.test(e)).sort((e,r)=>Number(e)-Number(r)).map(e=>n[e])}return"READ_AND_COLOR_OBJECTS"===r||"IMAGE_OBJECT_MATCHING"===r?n.answers??t:"WRITE_A_SHORT_LETTER"===r?"string"==typeof t?t:n.answer??"":n.answer??t}(t.questionId,t.questionType,t.studentAnswer);void 0!==n&&(e.has(t.questionType)&&a(n)?Object.assign(r.studentAnswers,n):r.studentAnswers[t.questionId]=n),r.correctAnswersMap.set(t.questionId,t.correctAnswer),r.isCorrectMap[t.questionId]=t.isCorrect,r.rawCorrectAnswerMap[t.questionId]=t.correctAnswer,r.rawStudentAnswerMap[t.questionId]=t.studentAnswer,r.questionScoreMap[t.questionId]={score:t.score,maxScore:t.maxScore},function(e,r){const t=o(r.correctAnswer),n=s(r.questionId,t.answer);n.length&&(e.correctOptionIdMap[r.questionId]=n.join(","));"READ_PASSAGE_AND_ANSWER_QUESTIONS"===r.questionType&&a(t.answers)&&Object.entries(t.answers).forEach(([r,t])=>{"number"==typeof t&&(e.correctOptionIdMap[r]=`${r}-opt-${t}`)});const i=function(e){if(Array.isArray(e.answers))return e.answers.map(String);if(a(e.answers))return Object.values(e.answers).map(String);const r=e.answer??e.correctAnswer;return null==r?[]:[String(r)]}(t);i[0]&&(e.correctAnswerTextMap[r.questionId]=i[0]);"WORD_FILL_PARAGRAPH"===r.questionType&&i.forEach((t,n)=>{e.correctAnswerTextMap[`${r.questionId}-blank-${n}`]=t});"LABEL_THE_PICTURE"===r.questionType&&a(t.labels)&&Object.entries(t.labels).forEach(([r,t])=>{e.correctAnswerTextMap[r]=String(t)});"FILL_MISSING_WORDS_IN_GRID"===r.questionType&&a(t.cells)&&(e.cellAnswersMap[r.questionId]={cells:t.cells,caseSensitive:!0===t.caseSensitive})}(r,t),"WRITE_A_SHORT_LETTER"!==t.questionType&&"WRITE_SHORT_PARAGRAPH"!==t.questionType||(r.essayGradingMap[t.questionId]={score:t.teacherScore??t.score,maxScore:t.maxScore,teacherFeedback:t.teacherFeedback??null})}function s(e,r){return(Array.isArray(r)?r:[r]).filter(e=>"number"==typeof e||"string"==typeof e).map(r=>"number"==typeof r?`${e}-opt-${r}`:r)}function o(e){return a(e)?e:{}}function a(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function i(e){const r=e?.match(/\d+/);return r?Number(r[0]):void 0}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Status & access helpers for the result review screen. */
|
|
2
|
+
/**
|
|
3
|
+
* Determine whether a load error means "this result is not public". Ports the
|
|
4
|
+
* host app's logic: a 403 with an explicit `RESULT_NOT_PUBLIC`/`FORBIDDEN` code,
|
|
5
|
+
* or any 403 reached via a token link (the only anonymous-access path).
|
|
6
|
+
*/
|
|
7
|
+
export declare function isResultNotPublic(error: unknown, hasToken: boolean): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Toggle the `data-pdf-ready` body attribute the headless PDF renderer waits on.
|
|
10
|
+
* Call with `true` once the result has finished loading in print mode.
|
|
11
|
+
*/
|
|
12
|
+
export declare function setPdfReady(ready: boolean): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t=new Set(["RESULT_NOT_PUBLIC","FORBIDDEN"]);export function isResultNotPublic(e,n){const o=function(t){return"object"!=typeof t||null===t?null:t}(e);if(403!==o?.response?.status)return!1;const r=o.response.data?.errorCode;return r?t.has(r):n}export function setPdfReady(t){"undefined"!=typeof document&&(t?document.body.setAttribute("data-pdf-ready","true"):document.body.removeAttribute("data-pdf-ready"))}
|
|
@@ -26,9 +26,16 @@ export interface PartScoreInfo {
|
|
|
26
26
|
export interface ReviewTransformResult {
|
|
27
27
|
parts: ReviewQuestionPart[];
|
|
28
28
|
studentAnswers: ReviewAnswerMap;
|
|
29
|
+
correctAnswersMap: Map<string, unknown>;
|
|
29
30
|
isCorrectMap: CorrectMap;
|
|
30
31
|
correctAnswerTextMap: Record<string, string>;
|
|
31
32
|
correctOptionIdMap: Record<string, string>;
|
|
33
|
+
cellAnswersMap: Record<string, {
|
|
34
|
+
cells: Record<string, {
|
|
35
|
+
correctAnswer: string;
|
|
36
|
+
}>;
|
|
37
|
+
caseSensitive: boolean;
|
|
38
|
+
}>;
|
|
32
39
|
essayGradingMap: Record<string, EssayGradingInfo>;
|
|
33
40
|
partScoresMap: Record<number, PartScoreInfo>;
|
|
34
41
|
rawCorrectAnswerMap: Record<string, unknown>;
|
|
@@ -44,6 +51,13 @@ export interface ResultReviewTemplatePart {
|
|
|
44
51
|
instructions?: string;
|
|
45
52
|
audioUrl?: string;
|
|
46
53
|
questionType?: string;
|
|
54
|
+
sections?: Array<{
|
|
55
|
+
sectionId?: number;
|
|
56
|
+
name?: string;
|
|
57
|
+
instructions?: string;
|
|
58
|
+
startIndex?: number;
|
|
59
|
+
endIndex?: number;
|
|
60
|
+
}>;
|
|
47
61
|
}
|
|
48
62
|
export type ReviewQuestion = ExamTakingQuestion & {
|
|
49
63
|
explanation?: string | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SpeakingResultAudioPlayerVariant = 'student' | 'admin';
|
|
2
|
+
export interface SpeakingResultAudioPlayerProps {
|
|
3
|
+
/** S3 key or displayable URL for the recorded audio answer */
|
|
4
|
+
audioKey: string;
|
|
5
|
+
/**
|
|
6
|
+
* Visual theme variant.
|
|
7
|
+
* - student (default): ocean-blue
|
|
8
|
+
* - admin: neutral gray
|
|
9
|
+
*/
|
|
10
|
+
variant?: SpeakingResultAudioPlayerVariant;
|
|
11
|
+
/**
|
|
12
|
+
* Optional verification token. Hosts that resolve storage keys can pass this
|
|
13
|
+
* through their own fetcher; the default SDK hook is a pass-through.
|
|
14
|
+
*/
|
|
15
|
+
token?: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function SpeakingResultAudioPlayer({ audioKey, variant, token: _token, }: SpeakingResultAudioPlayerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useCallback as r,useRef as a,useState as n}from"react";import{Loader2 as i,Mic as s,Pause as l,Play as o,RefreshCw as d}from"lucide-react";import{usePresignedFileUrl as u}from"../../../shared/lib/hooks/usePresignedFileUrl";import{cn as c}from"../../../shared/lib/utils";function m(e){if(!isFinite(e)||isNaN(e))return"--:--";return`${Math.floor(e/60)}:${Math.floor(e%60).toString().padStart(2,"0")}`}export function SpeakingResultAudioPlayer({audioKey:h,variant:b="student",token:g=null}){const{previewUrl:p,isLoading:x,error:f}=u(h),w=a(null),[y,N]=n(!1),[v,k]=n(0),[_,A]=n(0),j=r(()=>{const e=w.current;e&&p&&(y?e.pause():e.play().catch(e=>{console.error("Audio play failed:",e)}))},[y,p]),T=r(e=>{const t=w.current;if(!t)return;const r=Number(e.target.value);t.currentTime=r,k(r)},[]),P=r(()=>{const e=w.current;e&&k(e.currentTime)},[]),M=r(()=>{const e=w.current;e&&A(e.duration||0)},[]),S=r(()=>{N(!1),k(0);const e=w.current;e&&(e.currentTime=0)},[]),U=_>0?v/_*100:0,$="admin"===b;return t("div",x?{className:c("flex items-center justify-center gap-2 rounded-xl px-4 py-3.5",$?"bg-gray-100":"bg-gradient-to-r from-[#2874A6] to-[#3498DB]"),children:[e(i,{className:c("h-4 w-4 animate-spin",$?"text-gray-500":"text-white/80")}),e("span",{className:c("text-sm font-medium",$?"text-gray-600":"text-white/80"),children:"Đang tải âm thanh..."})]}:f||!p?{className:"flex items-center gap-2.5 rounded-xl border border-red-200 bg-red-50 px-4 py-3",children:[e(d,{className:"h-4 w-4 text-red-400"}),e("span",{className:"text-sm font-medium text-red-600",children:"Không thể phát âm thanh"})]}:{className:"overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-[0_2px_12px_rgba(0,0,0,0.02)] transition-all hover:shadow-[0_4px_16px_rgba(0,0,0,0.04)] ring-1 ring-gray-900/5",children:[e("audio",{ref:w,src:p,onPlay:()=>N(!0),onPause:()=>N(!1),onTimeUpdate:P,onLoadedMetadata:M,onEnded:S,preload:"metadata"}),t("div",{className:"flex items-center gap-3 px-4 py-3",children:[e("div",{className:c("flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-all",y?"bg-blue-50 ring-4 ring-blue-50/50":"bg-gray-50"),children:e(s,{className:c("h-4 w-4 transition-transform",y?"text-[#2874A6] animate-pulse":"text-gray-400")})}),e("button",{type:"button",onClick:j,className:c("flex h-10 w-10 shrink-0 items-center justify-center rounded-full shadow-sm transition-all active:scale-95",$?y?"bg-gray-600 text-white hover:bg-gray-700":"bg-gray-500 text-white hover:bg-gray-600":y?"bg-[#1A5276] text-white hover:bg-[#154360]":"bg-[#2874A6] text-white hover:bg-[#1A5276]"),"aria-label":y?"Tạm dừng":"Phát",children:y?e(l,{className:"h-4 w-4"}):e(o,{className:"h-4 w-4 ml-0.5"})}),t("div",{className:"flex flex-1 flex-col gap-1.5 px-2",children:[t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-[10px] font-bold tabular-nums text-gray-500 tracking-wide",children:m(v)}),e("span",{className:"text-[10px] font-bold tabular-nums text-gray-400 tracking-wide",children:_>0?m(_):"--:--"})]}),t("div",{className:"relative flex h-2 items-center",children:[e("input",{type:"range",min:0,max:_||1,step:.1,value:v,onChange:T,className:c("absolute inset-0 z-10 h-1.5 w-full cursor-pointer appearance-none rounded-full bg-transparent","[&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:w-3.5","[&::-webkit-slider-thumb]:appearance-none","[&::-webkit-slider-thumb]:rounded-full","[&::-webkit-slider-thumb]:bg-white","[&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(0,0,0,0.2)]","[&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-gray-200","[&::-webkit-slider-thumb]:transition-transform","[&::-webkit-slider-thumb]:hover:scale-110"),"aria-label":"Tiến trình phát"}),e("div",{className:"pointer-events-none absolute left-0 right-0 h-1.5 rounded-full bg-gray-100"}),e("div",{className:c("pointer-events-none absolute left-0 h-1.5 rounded-full transition-all duration-100 ease-linear",$?"bg-blue-500":"bg-gradient-to-r from-[#2874A6] to-[#3498DB]"),style:{width:`${U}%`}})]})]})]})]})}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { SpeakingRubric } from '../../../api/result-review';
|
|
3
|
+
import type { SpeakingReviewData } from '../utils/transform-speaking-review';
|
|
4
|
+
export declare const SPEAKING_REVIEW_POLL_INTERVAL_MS = 5000;
|
|
5
|
+
export interface SpeakingReviewResultProps {
|
|
6
|
+
reviewData: SpeakingReviewData;
|
|
7
|
+
studentName?: string | null;
|
|
8
|
+
studentCode?: string | null;
|
|
9
|
+
branchName?: string | null;
|
|
10
|
+
examDateText?: string;
|
|
11
|
+
examTitle?: string | null;
|
|
12
|
+
showDetailedResults?: boolean;
|
|
13
|
+
speakingRubric?: SpeakingRubric | null;
|
|
14
|
+
reviewUrl?: string;
|
|
15
|
+
assetBaseUrl?: string;
|
|
16
|
+
canExportPdf?: boolean;
|
|
17
|
+
isExporting?: boolean;
|
|
18
|
+
/** True when any question is still in a pollable grading status. */
|
|
19
|
+
hasPendingQuestions?: boolean;
|
|
20
|
+
onBack?: () => void;
|
|
21
|
+
onExportPdf?: () => void | Promise<void>;
|
|
22
|
+
topActionsSlot?: ReactNode;
|
|
23
|
+
beforeReportSlot?: ReactNode;
|
|
24
|
+
afterReportSlot?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Speaking review page shell (toolbar + banners + report).
|
|
28
|
+
* Source: oe-exam-fe/.../speaking-review/page.tsx (SDK-owned portion only).
|
|
29
|
+
*/
|
|
30
|
+
export declare function SpeakingReviewResult({ reviewData, studentName, studentCode, branchName, examDateText, examTitle, showDetailedResults, speakingRubric, reviewUrl, assetBaseUrl, canExportPdf, isExporting: isExportingProp, hasPendingQuestions, onBack, onExportPdf, topActionsSlot, beforeReportSlot, afterReportSlot, }: SpeakingReviewResultProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as t,Fragment as a}from"react/jsx-runtime";import{useMemo as s,useState as r}from"react";import{AlertTriangle as i,ChevronLeft as n,Download as l,Loader2 as o,Sparkles as d}from"lucide-react";import{OceanEduSpeakingReport as c}from"../report/OceanEduSpeakingReport";export const SPEAKING_REVIEW_POLL_INTERVAL_MS=5e3;export function SpeakingReviewResult({reviewData:m,studentName:h,studentCode:u,branchName:f,examDateText:p,examTitle:x,showDetailedResults:b,speakingRubric:g,reviewUrl:v,assetBaseUrl:w,canExportPdf:N=!1,isExporting:y,hasPendingQuestions:E=!1,onBack:A,onExportPdf:D,topActionsSlot:k,beforeReportSlot:R,afterReportSlot:S}){const[F,P]=r(!1),T=y??F,j=s(()=>{if(p)return p;if(m.submittedAt){const e=new Date(m.submittedAt);if(!Number.isNaN(e.getTime()))return e.toLocaleDateString("vi-VN")}return(new Date).toLocaleDateString("vi-VN")},[p,m.submittedAt]);return e("div",{className:"min-h-screen bg-[#e0e0e0] py-4 md:py-6",children:t("div",{className:"mx-auto flex w-full max-w-[1160px] flex-col gap-4 px-0 sm:px-4",children:[t("div",{className:"flex items-center justify-between gap-3 px-3 print:hidden sm:px-0",children:[t("button",{type:"button",onClick:()=>{A?A():"undefined"!=typeof window&&window.history.back()},className:"inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm transition hover:border-slate-300 hover:bg-slate-50",children:[e(n,{className:"h-4 w-4"}),"Quay lại"]}),t("div",{className:"flex items-center gap-2",children:[N&&D?t("button",{type:"button",onClick:()=>{(async()=>{if(D){void 0===y&&P(!0);try{await D()}finally{void 0===y&&P(!1)}}})()},disabled:T,className:"group relative inline-flex items-center gap-2.5 overflow-hidden rounded-xl bg-gradient-to-r from-[#00AEEF] via-[#0099D6] to-[#00AEEF] bg-[length:200%_100%] px-5 py-3 text-sm font-bold text-white shadow-lg shadow-[#00AEEF]/30 transition-all duration-300 hover:bg-[position:100%_0] hover:shadow-xl hover:shadow-[#00AEEF]/40 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00AEEF] focus-visible:ring-offset-2",children:[e("span",{className:"absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/20 to-transparent transition-transform duration-1000 group-hover:translate-x-full"}),t(a,T?{children:[e(o,{className:"relative z-10 h-4 w-4 animate-spin"}),e("span",{className:"relative z-10",children:"Đang xuất..."})]}:{children:[e(l,{className:"relative z-10 h-4 w-4 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:scale-110"}),e("span",{className:"relative z-10",children:"Xuất PDF"})]})]}):null,k]})]}),E?t("div",{className:"flex items-center gap-3 rounded-2xl border border-[#b3dfff] bg-gradient-to-r from-[#e8f6ff] to-[#f0f8ff] px-5 py-4 shadow-sm",children:[e("div",{className:"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#d0edff]",children:e(d,{className:"h-5 w-5 animate-pulse text-[#00AEEF]"})}),t("div",{className:"flex-1",children:[e("p",{className:"text-sm font-bold text-[#0054A6]",children:"AI đang chấm điểm..."}),t("p",{className:"text-xs text-[#0078c8]",children:["Trang sẽ tự động cập nhật sau mỗi ",5," giây cho đến khi hoàn tất."]})]})]}):null,m.isPendingGrading?t("div",{className:"flex items-center gap-3 rounded-2xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 px-5 py-4 shadow-sm",children:[e("div",{className:"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-100",children:e(i,{className:"h-5 w-5 text-amber-600"})}),t("div",{children:[e("p",{className:"text-sm font-bold text-amber-800",children:"Đang chờ chấm điểm"}),e("p",{className:"text-xs text-amber-600",children:"Giáo viên sẽ chấm điểm bài thi của bạn sớm nhất có thể."})]})]}):null,R,e(c,{reviewData:m,studentName:h??m.studentName,studentCode:u,branchName:f,examDateText:j,examTitle:x,showDetailedResults:b,reviewUrl:v,speakingRubric:g,assetBaseUrl:w}),S]})})}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Score display helpers for the general (non-speaking) exam result report.
|
|
3
|
+
* Source thresholds mirror oe-exam-fe OceanEduExamReport.
|
|
4
|
+
*/
|
|
5
|
+
/** Percentage bands for general exam total-score labels / messages. */
|
|
6
|
+
export declare const ReviewScoreBand: {
|
|
7
|
+
readonly EXCELLENT: 90;
|
|
8
|
+
readonly GOOD: 80;
|
|
9
|
+
readonly FAIR: 65;
|
|
10
|
+
readonly AVERAGE: 50;
|
|
11
|
+
};
|
|
12
|
+
export declare function formatScore(score: number | null | undefined, digits?: number): string;
|
|
13
|
+
/** Format total score for "ĐIỂM TỔNG" display. */
|
|
14
|
+
export declare function formatTotalScore(score: number | null | undefined): string;
|
|
15
|
+
export declare function getScoreStroke(totalScore: number | null, maxScore: number, isPendingGrading: boolean): string;
|
|
16
|
+
export declare function getScoreLabel(totalScore: number | null, maxScore: number, isPendingGrading: boolean): string;
|
|
17
|
+
/**
|
|
18
|
+
* Score-aware mascot message. Mirrors {@link getScoreLabel} thresholds so the
|
|
19
|
+
* encouragement text matches the actual result.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getScoreMessage(totalScore: number | null, maxScore: number, isPendingGrading: boolean): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ReviewScoreBand={EXCELLENT:90,GOOD:80,FAIR:65,AVERAGE:50};export function formatScore(n,e=1){if(null==n||Number.isNaN(n))return"—";return n.toFixed(e).replace(/\.0+$/,"").replace(/(\.\d*[1-9])0+$/,"$1")}export function formatTotalScore(n){return null!=n&&Number.isFinite(n)?n<10?n.toFixed(1):String(Math.round(n)):"—"}export function getScoreStroke(n,e,i){if(i||null==n||e<=0)return"conic-gradient(#cbd5e1 0% 100%, #eef5ff 100% 100%)";const t=Math.max(0,Math.min(100,n/e*100));return`conic-gradient(#00AEEF 0% ${t}%, #eef5ff ${t}% 100%)`}export function getScoreLabel(n,e,i){if(i||null==n||e<=0)return"Đang chấm điểm";const t=n/e*100;return t>=ReviewScoreBand.EXCELLENT?"Xuất sắc":t>=ReviewScoreBand.GOOD?"Giỏi":t>=ReviewScoreBand.FAIR?"Khá":t>=ReviewScoreBand.AVERAGE?"Trung bình":"Cần cố gắng"}export function getScoreMessage(n,e,i){if(i)return"Bài làm của bạn đang có câu hỏi tự luận chờ giáo viên chấm điểm. Điểm số sẽ được cập nhật sớm nhất.";if(null==n||e<=0)return"Đừng nản nhé! Hãy xem lại chi tiết bài làm phía dưới và ôn lại kiến thức nhé.";const t=n/e*100;return t>=ReviewScoreBand.EXCELLENT?"Xuất sắc! Bạn nắm rất vững kiến thức. Xem lại chi tiết phía dưới nhé.":t>=ReviewScoreBand.GOOD?"Làm tốt lắm! Xem lại chi tiết bài làm phía dưới để hoàn thiện hơn nhé.":t>=ReviewScoreBand.FAIR?"Khá tốt! Hãy xem lại các câu chưa đúng phía dưới để tiến bộ hơn nhé.":t>=ReviewScoreBand.AVERAGE?"Bạn đã cố gắng! Xem lại kỹ phần chi tiết phía dưới để cải thiện nhé.":"Đừng nản nhé! Hãy xem lại chi tiết bài làm phía dưới và ôn lại kiến thức nhé."}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared logic for Speaking Review "NHẬN XÉT TỔNG QUAN TỪ AI".
|
|
3
|
+
* Source: oe-exam-fe/.../speaking-review/_utils/speaking-summary.ts
|
|
4
|
+
*/
|
|
5
|
+
import type { SpeakingReviewData, SpeakingReviewQuestion } from './transform-speaking-review';
|
|
6
|
+
export type SpeakingSkillKey = 'pronunciation' | 'fluency' | 'vocabulary' | 'grammar' | 'coherence';
|
|
7
|
+
export interface SkillScores {
|
|
8
|
+
pronunciation: number | null;
|
|
9
|
+
fluency: number | null;
|
|
10
|
+
vocabulary: number | null;
|
|
11
|
+
grammar: number | null;
|
|
12
|
+
coherence: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface SkillMetric {
|
|
15
|
+
key: SpeakingSkillKey;
|
|
16
|
+
label: string;
|
|
17
|
+
sublabel: string;
|
|
18
|
+
score: number;
|
|
19
|
+
color: string;
|
|
20
|
+
}
|
|
21
|
+
export type SummaryPointIcon = 'check' | 'star';
|
|
22
|
+
export interface SummaryPoint {
|
|
23
|
+
icon: SummaryPointIcon;
|
|
24
|
+
text: string;
|
|
25
|
+
highlight?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare const SPEAKING_SKILL_COLORS: {
|
|
28
|
+
readonly pronunciation: "#4CAF50";
|
|
29
|
+
readonly fluency: "#2196F3";
|
|
30
|
+
readonly vocabulary: "#FF9800";
|
|
31
|
+
readonly grammar: "#9C27B0";
|
|
32
|
+
readonly coherence: "#00BCD4";
|
|
33
|
+
};
|
|
34
|
+
export declare function deriveSkillScores(reviewData: SpeakingReviewData): SkillScores;
|
|
35
|
+
export declare function buildSkillMetrics(scores: SkillScores): SkillMetric[];
|
|
36
|
+
export declare function deriveSkillMetrics(reviewData: SpeakingReviewData): SkillMetric[];
|
|
37
|
+
export declare function deriveQuestionSkillMetrics(question: SpeakingReviewQuestion): SkillMetric[];
|
|
38
|
+
export declare function getOverallStatusText(totalScore: number | null, maxScore: number, isPendingGrading: boolean): string;
|
|
39
|
+
export declare function getScoreLabel(totalScore: number | null, maxScore: number, isPendingGrading: boolean): string;
|
|
40
|
+
export declare function deriveFeedback(reviewData: SpeakingReviewData): string;
|
|
41
|
+
export declare function deriveSummaryPoints(reviewData: SpeakingReviewData, metrics: SkillMetric[]): SummaryPoint[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SPEAKING_SKILL_COLORS={pronunciation:"#4CAF50",fluency:"#2196F3",vocabulary:"#FF9800",grammar:"#9C27B0",coherence:"#00BCD4"};function n(n){return null!=n&&Number.isFinite(n)?Math.round(Math.max(0,Math.min(100,n))):0}function t(n,t){if(t<=0)return null;return n.reduce((n,t)=>n+("number"==typeof t&&Number.isFinite(t)?t:0),0)/t}function e(n){return n&&"object"==typeof n?n:null}function c(n){const t=e(n);return e(t?.pronunciation)}export function deriveSkillScores(n){const r=function(n){return n.parts.flatMap(n=>n.questions)}(n),i=r.filter(n=>!n.isExample),o=i.length;return{pronunciation:t(i.map(n=>{const t=c(n.aiAssessment)?.pronunciationScore;return"number"==typeof t?t:null}),o),fluency:t(i.map(n=>{const t=c(n.aiAssessment)?.fluencyScore;return"number"==typeof t?t:null}),o),vocabulary:t(i.map(n=>{const t=e(n.llmAssessment)?.contentScore;return"number"==typeof t?t:null}),o),grammar:t(i.map(n=>{const t=c(n.aiAssessment)?.accuracyScore;return"number"==typeof t?t:null}),o),coherence:t(i.map(n=>{const t=c(n.aiAssessment)?.prosodyScore;return"number"==typeof t?t:null}),o)}}export function buildSkillMetrics(t){return[{key:"pronunciation",label:"Pronunciation",sublabel:"Phát âm",score:n(t.pronunciation),color:SPEAKING_SKILL_COLORS.pronunciation},{key:"fluency",label:"Fluency",sublabel:"Độ trôi chảy",score:n(t.fluency),color:SPEAKING_SKILL_COLORS.fluency},{key:"grammar",label:"Accuracy",sublabel:"Chính xác phát âm",score:n(t.grammar),color:SPEAKING_SKILL_COLORS.grammar},{key:"coherence",label:"Prosody",sublabel:"Ngữ điệu",score:n(t.coherence),color:SPEAKING_SKILL_COLORS.coherence},{key:"vocabulary",label:"Content",sublabel:"Nội dung",score:n(t.vocabulary),color:SPEAKING_SKILL_COLORS.vocabulary}]}export function deriveSkillMetrics(n){return buildSkillMetrics(deriveSkillScores(n))}export function deriveQuestionSkillMetrics(n){const t=c(n.aiAssessment),r=e(n.llmAssessment);return buildSkillMetrics({pronunciation:"number"==typeof t?.pronunciationScore?t.pronunciationScore:null,fluency:"number"==typeof t?.fluencyScore?t.fluencyScore:null,vocabulary:"number"==typeof r?.contentScore?r.contentScore:null,grammar:"number"==typeof t?.accuracyScore?t.accuracyScore:null,coherence:"number"==typeof t?.prosodyScore?t.prosodyScore:null})}export function getOverallStatusText(n,t,e){if(e||null==n)return"Đang chờ hoàn tất chấm điểm. Kết quả tổng quan sẽ được cập nhật ngay khi có đủ dữ liệu.";const c=t>0?n/t*100:0;return c>=85?"Trình độ rất tốt. Tiếp tục phát huy nhé!":c>=70?"Kết quả tốt. Hãy tiếp tục luyện tập để bứt phá hơn nữa!":c>=50?"Bạn đang tiến bộ tốt. Hãy luyện tập thêm để cải thiện toàn diện hơn.":"Cần luyện tập thêm để nâng cao kỹ năng nói và sự tự tin khi giao tiếp."}export function getScoreLabel(n,t,e){if(e||null==n)return"ĐANG CHỜ";const c=t>0?n/t*100:0;return c>=85?"XUẤT SẮC":c>=70?"TỐT":c>=50?"KHÁ":"CẦN CỐ GẮNG HƠN"}export function deriveFeedback(n){if(n.isPendingGrading)return"Bài thi đang chờ hệ thống hoặc giáo viên hoàn tất chấm điểm. Kết quả chi tiết sẽ được cập nhật sớm nhất có thể.";const t=n.maxScore>0?(n.totalScore??0)/n.maxScore*100:0;return t>=85?"Tuyệt vời! Bạn đã thể hiện khả năng nói tiếng Anh rất tốt. Hãy tiếp tục phát huy và chinh phục những thử thách cao hơn nhé!":t>=70?"Bạn đã có nền tảng nói tiếng Anh khá tốt. Tiếp tục luyện phát âm, mở rộng vốn từ và duy trì sự tự tin khi trả lời.":t>=50?"Bạn cần cố gắng hơn nữa. Hãy dành thêm thời gian luyện phát âm rõ ràng hơn và trả lời mạch lạc hơn ở từng phần thi.":"Bạn cần cố gắng nhiều hơn. Hãy luyện tập từng ngày để nâng cao khả năng nói, sự trôi chảy và mức độ tự tin khi giao tiếp bằng tiếng Anh."}export function deriveSummaryPoints(n,t){const e=Object.fromEntries(t.map(n=>[n.key,n.score])),c=[];return(e.pronunciation??0)>=75?c.push({icon:"check",text:"Phát âm rõ ràng, tự nhiên, ngữ điệu khá tốt."}):c.push({icon:"check",text:"Cần luyện thêm phát âm từng âm và nhấn trọng âm để nói rõ hơn."}),(e.grammar??0)>=75?c.push({icon:"check",text:"Phát âm từng âm chuẩn xác, ít mắc lỗi sai âm."}):c.push({icon:"check",text:"Cần luyện phát âm chuẩn từng âm tiết, đặc biệt các âm tiếng Anh dễ nhầm."}),(e.fluency??0)>=75?c.push({icon:"check",text:"Diễn đạt trôi chảy, tốc độ nói khá tốt."}):c.push({icon:"check",text:"Cần tăng độ trôi chảy và giảm các khoảng ngập ngừng khi trả lời."}),(e.coherence??0)>=75?c.push({icon:"check",text:"Ngữ điệu tự nhiên, nhấn nhá và nhịp điệu khi nói khá tốt."}):c.push({icon:"check",text:"Nên luyện ngữ điệu lên xuống tự nhiên hơn và chú ý nhịp điệu khi nói."}),(e.vocabulary??0)>=75?c.push({icon:"check",text:"Nội dung trả lời bám sát câu hỏi và đầy đủ ý chính."}):c.push({icon:"check",text:"Cần trả lời đúng trọng tâm câu hỏi và đầy đủ ý chính hơn."}),c.push({icon:"star",text:getOverallStatusText(n.totalScore,n.maxScore,n.isPendingGrading),highlight:!0}),c.slice(0,6)}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform DetailedResult API response to SpeakingReviewData format.
|
|
3
|
+
* Source: oe-exam-fe/.../speaking-review/_utils/transform-speaking-review.ts
|
|
4
|
+
*/
|
|
5
|
+
import { ReviewGradingStatus, type ResultReviewDetail, type ResultReviewTemplatePart } from '../../../api/result-review';
|
|
6
|
+
export declare const SPEAKING_ANSWER_COMPLETED: "completed";
|
|
7
|
+
export declare const SPEAKING_STORY_IMAGES_TYPE: "LISTEN_AND_SPEAK_WITH_STORY_IMAGES";
|
|
8
|
+
export interface SpeakingReviewQuestion {
|
|
9
|
+
questionId: string;
|
|
10
|
+
/** DB answer record ID — used for individual polling. Falls back to questionId. */
|
|
11
|
+
answerId: string;
|
|
12
|
+
questionNumber: number;
|
|
13
|
+
/** Question prompt audio URL — S3 key that needs presigned URL resolution */
|
|
14
|
+
audioUrl: string;
|
|
15
|
+
/** S3 key for student's recorded answer, "completed", or null */
|
|
16
|
+
studentAnswerKey: string | null;
|
|
17
|
+
score: number | null;
|
|
18
|
+
maxScore: number;
|
|
19
|
+
isPendingGrading: boolean;
|
|
20
|
+
isCorrect: boolean | null;
|
|
21
|
+
/** Whether this is a partner example question (responder: PARTNER) — not graded */
|
|
22
|
+
isExample: boolean;
|
|
23
|
+
/** S3 key for partner's pre-recorded example answer audio */
|
|
24
|
+
partnerAudioUrl: string;
|
|
25
|
+
questionType: string;
|
|
26
|
+
questionContent: unknown;
|
|
27
|
+
studentAnswer: unknown;
|
|
28
|
+
correctAnswer: unknown;
|
|
29
|
+
questionGroup: {
|
|
30
|
+
id?: string | number;
|
|
31
|
+
payload?: unknown;
|
|
32
|
+
} | null;
|
|
33
|
+
aiAssessment?: unknown;
|
|
34
|
+
llmAssessment?: unknown;
|
|
35
|
+
classification?: unknown;
|
|
36
|
+
gradingStatus?: ReviewGradingStatus | string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface SpeakingReviewPart {
|
|
39
|
+
partId: string;
|
|
40
|
+
partNo: number;
|
|
41
|
+
partName: string;
|
|
42
|
+
partSectionName: string;
|
|
43
|
+
instructions: string;
|
|
44
|
+
questionType: string;
|
|
45
|
+
questions: SpeakingReviewQuestion[];
|
|
46
|
+
groupPayload: unknown;
|
|
47
|
+
}
|
|
48
|
+
export interface SpeakingReviewData {
|
|
49
|
+
parts: SpeakingReviewPart[];
|
|
50
|
+
examName: string;
|
|
51
|
+
examSubTitle: string;
|
|
52
|
+
studentName: string;
|
|
53
|
+
submittedAt: string;
|
|
54
|
+
totalScore: number | null;
|
|
55
|
+
maxScore: number;
|
|
56
|
+
isPendingGrading: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Main transformer: convert ResultReviewDetail to SpeakingReviewData.
|
|
60
|
+
*/
|
|
61
|
+
export declare function transformSpeakingResultForReview(result: ResultReviewDetail, studentName: string, templateParts?: ResultReviewTemplatePart[] | null): SpeakingReviewData;
|
|
62
|
+
export declare function isSpeakingTemplateType(type: string | null | undefined): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ResultTemplateType as t}from"../../../api/result-review";export const SPEAKING_ANSWER_COMPLETED="completed";export const SPEAKING_STORY_IMAGES_TYPE="LISTEN_AND_SPEAK_WITH_STORY_IMAGES";function e(t){if(null==t)return null;if("string"==typeof t)return t||null;if("object"==typeof t&&null!==t){const e=t;if("string"==typeof e.answer)return e.answer||null;if("string"==typeof e.audioKey)return e.audioKey||null;if("string"==typeof e.objectId)return"completed"}return null}function n(t){if(!t||"object"!=typeof t)return"";const e=t;return"string"==typeof e.audioUrl?e.audioUrl:"string"==typeof e.audio?e.audio:"string"==typeof e.questionAudio?e.questionAudio:"string"==typeof e.interlocutorAudioUrl?e.interlocutorAudioUrl:""}function o(t){if(!t||"object"!=typeof t)return"";const e=t;return"string"==typeof e.partnerAudioUrl?e.partnerAudioUrl:""}function r(t){const e=null!=t.sectionId?`_${t.sectionId}`:"";return`${t.partId??"unknown"}_${t.questionType}${e}`}function s(t,e,n){if(n<=1)return`Part ${t}`;return`Part ${t}${String.fromCharCode(97+e)}`}export function transformSpeakingResultForReview(t,i,u){const{answers:a,examName:c,submittedAt:l,totalScore:p,maxScore:d,isPendingGrading:f}=t,m=Boolean(f),g=new Map;for(const t of a){const e=r(t),n=g.get(e);n?n.push(t):g.set(e,[t])}const A=new Map,S=new Map;for(const[,t]of g){const e=t[0]?.partNo??0;S.set(e,(S.get(e)??0)+1)}const q=[];for(const[,t]of g){t.sort((t,e)=>t.questionNumber-e.questionNumber);const r=t[0];if(!r)continue;const i=r.partId??"unknown",a=r.partNo??0,c=r.questionType,l=r.sectionId,p=A.get(a)??0;A.set(a,p+1);const d=u?.find(t=>String(t.id)===String(i));let f,g=d?.instructions??"";if(d?.sections&&null!=l){const t=d.sections.find(t=>t.sectionId===l);if(t?.name)f=t.name;else{f=s(a,p,S.get(a)??1)}t?.instructions&&(g=t.instructions)}else{f=s(a,p,S.get(a)??1)}const N=`Part ${a}`,y=t.map((t,r)=>{const s=(i=t.studentAnswer)&&"object"==typeof i?i:null;var i;return{questionId:t.questionId,answerId:t.id??t.questionId,questionNumber:r+1,audioUrl:n(t.questionContent),studentAnswerKey:e(t.studentAnswer),isExample:t.isExample??!1,partnerAudioUrl:o(t.questionContent),score:t.score,maxScore:t.maxScore,isPendingGrading:m,isCorrect:t.isCorrect,questionType:t.questionType,questionContent:t.questionContent,studentAnswer:t.studentAnswer,correctAnswer:t.correctAnswer,questionGroup:t.questionGroup??null,aiAssessment:s?.aiAssessment,llmAssessment:s?.llmAssessment,classification:s?.classification,gradingStatus:t.gradingStatus??null}});let I=r.questionGroup?.payload??null;if(c===SPEAKING_STORY_IMAGES_TYPE&&!I){const t=r.questionContent;t?.images&&(I={images:t.images})}q.push({partId:i,partNo:a,partName:N,partSectionName:f,instructions:g,questionType:c,questions:y,groupPayload:I})}return q.sort((t,e)=>t.partNo!==e.partNo?t.partNo-e.partNo:(t.questions[0]?.questionNumber??0)-(e.questions[0]?.questionNumber??0)),{parts:q,examName:c??"",examSubTitle:t.roomName??"",studentName:i,submittedAt:String(l??""),totalScore:m?null:p,maxScore:d,isPendingGrading:m}}export function isSpeakingTemplateType(e){return e===t.SPEAKING}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface SpeakingAudioPlayerProps {
|
|
2
|
+
audioUrl: string;
|
|
3
|
+
onPlayingChange?: (isPlaying: boolean) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Auto-play the audio a few seconds (3s) after it loads or changes — e.g.
|
|
6
|
+
* when navigating to the next question. Defaults to false so other usages
|
|
7
|
+
* (such as model-pronunciation playback) are not auto-triggered.
|
|
8
|
+
*/
|
|
9
|
+
autoPlay?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function SpeakingAudioPlayer({ audioUrl, onPlayingChange, autoPlay }: SpeakingAudioPlayerProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{useRef as t,useState as n,useEffect as r}from"react";import{Pause as i,Play as o}from"lucide-react";import{usePresignedFileUrl as d}from"../../../shared/lib/hooks/usePresignedFileUrl";import{useDelayedAutoPlay as l}from"../../../shared/lib/hooks/useDelayedAutoPlay";import s from"./SpeakingAudioPlayer.module.css";const u=[.4,.6,.85,.55,.75,1,.65,.45,.7,.9,.6,.5,.8,.65,.95,.7,.5,.6,.8,.45];export function SpeakingAudioPlayer({audioUrl:c,onPlayingChange:m,autoPlay:p=!1}){const v=t(null),[h,y]=n(!1),[g,f]=n(0),[L,E]=n(0),{previewUrl:N}=d(c||void 0);r(()=>{const e=v.current;if(!e)return;const a=()=>{e.duration&&!isNaN(e.duration)&&isFinite(e.duration)&&E(e.duration)},t=()=>{f(e.currentTime),a()},n=()=>{y(!0),m?.(!0)},r=()=>{y(!1),m?.(!1)},i=()=>{y(!1),f(0),m?.(!1)};return a(),e.addEventListener("loadedmetadata",a),e.addEventListener("durationchange",a),e.addEventListener("canplay",a),e.addEventListener("timeupdate",t),e.addEventListener("play",n),e.addEventListener("pause",r),e.addEventListener("ended",i),()=>{e.removeEventListener("loadedmetadata",a),e.removeEventListener("durationchange",a),e.removeEventListener("canplay",a),e.removeEventListener("timeupdate",t),e.removeEventListener("play",n),e.removeEventListener("pause",r),e.removeEventListener("ended",i)}},[N]),r(()=>{y(!1),f(0),E(0)},[N]),l({audioRef:v,audioUrl:N,enabled:p});const b=L>0?g/L:0,P=!N;return a("div",{className:s.audioPill,children:[e("audio",{ref:v,...N?{src:N}:{},preload:"metadata"}),e("button",{type:"button",className:`${s.playButton} ${h?s.playButtonPlaying:""} ${P?s.playButtonDisabled:""}`,onClick:P?void 0:()=>{const e=v.current;e&&(h?e.pause():e.play().catch(e=>{console.warn("Audio play request was interrupted:",e)}))},disabled:P,"aria-label":P?"Không có âm thanh đính kèm":h?"Tạm dừng":"Nghe câu hỏi",title:P?"Không có âm thanh đính kèm":h?"Tạm dừng":"Nghe câu hỏi",children:h?e(i,{className:s.playIcon,fill:"currentColor"}):e(o,{className:s.playIcon,fill:"currentColor",style:{marginLeft:"2px"}})}),e("div",{className:s.waveform,"aria-hidden":"true",children:u.map((a,t)=>{const n=(t+.5)/u.length<=b;return e("span",{className:`${s.waveBar} ${h&&n?s.waveBarPlaying:""}`,style:{height:100*a+"%",background:n?"linear-gradient(180deg, #ffc93c, #ff9900)":"#d8e0ea",animationDelay:h&&n?t%6*.1+"s":void 0}},t)})})]})}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* ===== Speaking Audio Player — "Listen to the question" ===== */
|
|
2
|
+
/* Small, simple, kid-friendly player for light backgrounds (Summer Sky theme):
|
|
3
|
+
a sky-blue play/replay button + a sunny-amber waveform progress indicator.
|
|
4
|
+
No time digits, no seek — compact and tap-friendly. */
|
|
5
|
+
|
|
6
|
+
.audioPill {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 10px;
|
|
10
|
+
width: 100%;
|
|
11
|
+
padding: 5px 14px 5px 5px;
|
|
12
|
+
background: #ffffff;
|
|
13
|
+
border: 2px solid #dcebff;
|
|
14
|
+
border-radius: 999px;
|
|
15
|
+
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ===== Play / Pause button ===== */
|
|
20
|
+
.playButton {
|
|
21
|
+
flex-shrink: 0;
|
|
22
|
+
height: 42px;
|
|
23
|
+
width: 42px;
|
|
24
|
+
min-width: 42px;
|
|
25
|
+
border: none;
|
|
26
|
+
border-radius: 50%;
|
|
27
|
+
background: linear-gradient(135deg, #5ba8e8, #357abd);
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
padding: 0;
|
|
34
|
+
box-shadow: 0 3px 8px rgba(53, 122, 189, 0.35);
|
|
35
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.playButton:hover {
|
|
39
|
+
transform: scale(1.08);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.playButton:active {
|
|
43
|
+
transform: scale(0.95);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.playIcon {
|
|
47
|
+
width: 20px;
|
|
48
|
+
height: 20px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Playing — coral with a gentle pulsing ring */
|
|
52
|
+
.playButtonPlaying {
|
|
53
|
+
background: linear-gradient(135deg, #ff8a8a, #ff6b6b);
|
|
54
|
+
box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.5);
|
|
55
|
+
animation: playPulse 1.5s infinite;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@keyframes playPulse {
|
|
59
|
+
0% {
|
|
60
|
+
box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.5);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
70% {
|
|
64
|
+
box-shadow: 0 0 0 8px rgba(255, 107, 107, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
100% {
|
|
68
|
+
box-shadow: 0 0 0 0 rgba(255, 107, 107, 0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Disabled — no audio attached */
|
|
73
|
+
.playButtonDisabled {
|
|
74
|
+
background: #cbd5e1;
|
|
75
|
+
box-shadow: none;
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
opacity: 0.7;
|
|
78
|
+
animation: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.playButtonDisabled:hover {
|
|
82
|
+
transform: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ===== Waveform progress indicator (display-only, fills L→R with progress) ===== */
|
|
86
|
+
.waveform {
|
|
87
|
+
flex: 1;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
height: 20px;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 2px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.waveBar {
|
|
96
|
+
flex: 1;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
border-radius: 999px;
|
|
99
|
+
transform-origin: center;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Subtle liveliness on the already-played region while playing */
|
|
103
|
+
.waveBarPlaying {
|
|
104
|
+
animation: waveBounce 0.9s ease-in-out infinite alternate;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@keyframes waveBounce {
|
|
108
|
+
0% {
|
|
109
|
+
transform: scaleY(0.7);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
100% {
|
|
113
|
+
transform: scaleY(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
Binary file
|
|
Binary file
|