@tinyweb_dev/oe-exam-sdk 0.1.28 → 0.1.29
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 +4 -4
- package/dist/api/contest-rooms.js +54 -1
- package/dist/api/contest-rooms.types.js +1 -1
- package/dist/api/exam-create.js +179 -1
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-entry.js +68 -1
- package/dist/api/exam-entry.types.js +1 -1
- package/dist/api/exam-questions.js +149 -1
- package/dist/api/exam-questions.types.js +1 -1
- package/dist/api/exam-taking.js +84 -1
- package/dist/api/exam-taking.types.d.ts +7 -0
- package/dist/api/exam-taking.types.js +1 -1
- package/dist/api/index.js +8 -1
- package/dist/api/result-review.js +62 -1
- package/dist/api/result-review.types.js +16 -1
- package/dist/components/CueCardEditor.js +58 -1
- package/dist/components/common/ResolvedImage.js +15 -1
- package/dist/components/exams/CompletionDialog.js +8 -1
- package/dist/components/exams/CreateExamInfoForm.js +84 -1
- package/dist/components/exams/CreateExamPageContainer.js +110 -1
- package/dist/components/exams/ExamCreator.js +287 -1
- package/dist/components/exams/ExamPartTabs.js +138 -1
- package/dist/components/exams/ExamPreviewDialog.d.ts +6 -7
- package/dist/components/exams/ExamPreviewDialog.js +121 -1
- package/dist/components/exams/ExamPreviewPartContent.d.ts +18 -0
- package/dist/components/exams/ExamPreviewPartContent.js +27 -0
- package/dist/components/exams/ExamPreviewSidebar.d.ts +17 -0
- package/dist/components/exams/ExamPreviewSidebar.js +37 -0
- package/dist/components/exams/ExamQuestionGrid.js +37 -1
- package/dist/components/exams/ExamQuestionsPageContainer.js +112 -1
- package/dist/components/exams/ExamViewDialog.js +61 -1
- package/dist/components/exams/PartAudioUploader.js +9 -1
- package/dist/components/exams/TemplateSelector.js +76 -1
- package/dist/components/exams/UnsavedChangesDialog.js +7 -1
- package/dist/components/exams/api.js +3 -1
- package/dist/components/exams/create-exam.texts.js +1 -1
- package/dist/components/exams/create-exam.validation.js +18 -1
- package/dist/components/exams/entry/EntryRestartDialog.js +11 -1
- package/dist/components/exams/entry/StudentExamEntryPageContainer.js +358 -1
- package/dist/components/exams/entry/exam-entry.states.js +26 -1
- package/dist/components/exams/entry/exam-entry.utils.js +55 -1
- package/dist/components/exams/entry/fullscreen-loading.js +18 -1
- package/dist/components/exams/entry/index.js +4 -1
- package/dist/components/exams/exam-preview.types.d.ts +30 -0
- package/dist/components/exams/exam-preview.types.js +62 -0
- package/dist/components/exams/exam-question-status.js +45 -1
- package/dist/components/exams/exam-template.utils.js +18 -1
- package/dist/components/exams/index.d.ts +1 -0
- package/dist/components/exams/index.js +19 -1
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +183 -1
- package/dist/components/exams/rooms/contest-rooms.utils.js +93 -1
- package/dist/components/exams/rooms/index.js +2 -1
- package/dist/components/exams/take/ExamTakingApiContext.js +25 -1
- package/dist/components/exams/take/ExamTakingPageContainer.js +955 -1
- package/dist/components/exams/take/GenericQuestionRenderer.js +14 -1
- package/dist/components/exams/take/components/QuestionRenderer.js +207 -1
- package/dist/components/exams/take/components/index.js +11 -1
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +8 -1
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +30 -1
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +63 -1
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +136 -1
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +379 -1
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.d.ts +20 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +141 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +131 -1
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +76 -1
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +68 -1
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +42 -1
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +131 -1
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +76 -1
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +68 -1
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +62 -1
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +51 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +171 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +198 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderAndMatchGroupRenderer.d.ts +15 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderAndMatchGroupRenderer.js +36 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +92 -1
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +11 -1
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +56 -1
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +25 -1
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +85 -1
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +10 -1
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +15 -1
- package/dist/components/exams/take/components/speaking/SpeakingHeader.js +6 -1
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +8 -1
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +59 -1
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +7 -1
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +5 -1
- package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +120 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +73 -1
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +8 -1
- package/dist/components/exams/take/components/speaking/SpeakingSpontaneousQA.module.css +344 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +32 -1
- package/dist/components/exams/take/components/speaking/index.js +1 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +63 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +95 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +35 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +147 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +232 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +193 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +155 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +212 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +77 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +64 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +69 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +64 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +92 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +202 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +339 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +76 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +56 -1
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQACambridgeLayout.d.ts +24 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQACambridgeLayout.js +120 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQAInlineAudioPlayer.d.ts +8 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQAInlineAudioPlayer.js +94 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQASummerSkyAudioPlayer.d.ts +8 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQASummerSkyAudioPlayer.js +101 -0
- package/dist/components/exams/take/components/speaking/renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/renderers/index.js +14 -1
- package/dist/components/exams/take/exam-taking.utils.d.ts +13 -0
- package/dist/components/exams/take/exam-taking.utils.js +203 -1
- package/dist/components/exams/take/hooks/index.js +2 -1
- package/dist/components/exams/take/hooks/useAnswerAutosave.js +71 -1
- package/dist/components/exams/take/hooks/useAttemptSubmit.js +38 -1
- package/dist/components/exams/take/hooks/useAutoSave.js +83 -1
- package/dist/components/exams/take/hooks/useExamCountdown.js +35 -1
- package/dist/components/exams/take/hooks/useExamTimer.js +97 -1
- package/dist/components/exams/take/hooks/useProctoringCamera.js +118 -1
- package/dist/components/exams/take/hooks/useProctoringSnapshot.js +76 -1
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +178 -1
- package/dist/components/exams/take/hooks/useSpeakingNavigation.js +51 -1
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +406 -1
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +136 -1
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRecorder.d.ts +11 -0
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRecorder.js +352 -0
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.d.ts +42 -0
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +166 -0
- package/dist/components/exams/take/hooks/useToeicAudio.js +35 -1
- package/dist/components/exams/take/index.js +6 -1
- package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -1
- package/dist/components/exams/take/types/speaking.types.js +7 -1
- package/dist/components/exams/take/types.js +4 -1
- package/dist/components/exams/take/utils/answer-transformers.js +452 -1
- package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.d.ts +21 -0
- package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.js +44 -0
- package/dist/components/exams/take/utils/coloring-assignments.js +40 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.d.ts +21 -0
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +83 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +43 -2
- package/dist/components/exams/take/utils/question-transformers.js +1137 -1
- package/dist/components/features/exam-entry/entry-state-cards.js +19 -1
- package/dist/components/features/exam-entry/index.js +2 -1
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +121 -1
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +37 -1
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +15 -1
- package/dist/components/features/exam-entry/themed/cambridge-yle-waiting-parts.js +80 -1
- package/dist/components/features/exam-entry/themed/components/CloseButton.js +6 -1
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +17 -1
- package/dist/components/features/exam-entry/themed/components/ModeSelector.js +6 -1
- package/dist/components/features/exam-entry/themed/components/StartButton.js +12 -1
- package/dist/components/features/exam-entry/themed/components/index.js +4 -1
- package/dist/components/features/exam-entry/themed/index.js +3 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +43 -1
- package/dist/components/questions/_shared/config/question-types.config.js +362 -1
- package/dist/components/questions/_shared/constants/color-palette.js +21 -1
- package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +105 -1
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +222 -1
- package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +140 -1
- package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +123 -1
- package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +50 -1
- package/dist/components/questions/_shared/hooks/useQuestionFormState.js +54 -1
- package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +123 -1
- package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +121 -1
- package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +70 -1
- package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +194 -1
- package/dist/components/questions/_shared/index.js +7 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
- package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
- package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +3 -1
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/color-region.config.js +1 -1
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +2 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
- package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
- package/dist/components/questions/_shared/types/index.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.js +14 -1
- package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +3 -1
- package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +3 -1
- package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
- package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
- package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/multiple-choice.type.d.ts +5 -0
- package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
- package/dist/components/questions/_shared/types/question-group.type.js +11 -1
- package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
- package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
- package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
- package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +37 -0
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.js +1 -0
- package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
- package/dist/components/questions/_shared/types/true-false-group.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +20 -11
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
- package/dist/components/questions/_shared/types/word-order-and-match-group.type.d.ts +38 -0
- package/dist/components/questions/_shared/types/word-order-and-match-group.type.js +1 -0
- package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
- package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
- package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +3 -1
- package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
- package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/utils/answer-builder.js +79 -1
- package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +39 -1
- package/dist/components/questions/_shared/utils/id-generator.js +3 -1
- package/dist/components/questions/_shared/utils/parseFillBlankStem.d.ts +30 -0
- package/dist/components/questions/_shared/utils/parseFillBlankStem.js +69 -0
- package/dist/components/questions/_shared/utils/question-validation.js +21 -1
- package/dist/components/questions/components/ColorNode.js +85 -1
- package/dist/components/questions/components/ColorRegionClient.js +545 -1
- package/dist/components/questions/components/ColorRegionCreator.js +420 -1
- package/dist/components/questions/components/ColorRegionNode.js +59 -1
- package/dist/components/questions/components/CreatorGuide.js +103 -1
- package/dist/components/questions/components/NameNode.js +26 -1
- package/dist/components/questions/components/QuestionBankOption.js +46 -1
- package/dist/components/questions/components/QuestionBasicInfoForm.js +9 -1
- package/dist/components/questions/components/QuestionCommonFields.js +33 -1
- package/dist/components/questions/components/QuestionSearchDropdown.js +258 -1
- package/dist/components/questions/components/QuestionTypeSelector.js +32 -1
- package/dist/components/questions/components/QuestionTypeSpecificForms.js +6 -1
- package/dist/components/questions/components/RegionNode.js +94 -1
- package/dist/components/questions/components/index.js +14 -1
- package/dist/components/questions/creator/QuestionCreator.js +90 -1
- package/dist/components/questions/creator/QuestionGroupCreator.js +119 -1
- package/dist/components/questions/creator/dedicated-component-router.js +79 -1
- package/dist/components/questions/creator/index.js +3 -1
- package/dist/components/questions/creator/question-type-registry.js +90 -1
- package/dist/components/questions/groups/ArticlesGroupCard.js +121 -1
- package/dist/components/questions/groups/BasicQuestionGroupCard.js +50 -1
- package/dist/components/questions/groups/DocumentGroupCard.js +114 -1
- package/dist/components/questions/groups/MatchAnswerGroupCard.js +25 -1
- package/dist/components/questions/index.d.ts +1 -0
- package/dist/components/questions/index.js +11 -1
- package/dist/components/questions/question-bank/EditQuestionBankDialog.d.ts +2 -0
- package/dist/components/questions/question-bank/EditQuestionBankDialog.js +114 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +23 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.js +75 -0
- package/dist/components/questions/question-bank/difficulty.d.ts +15 -0
- package/dist/components/questions/question-bank/difficulty.js +33 -0
- package/dist/components/questions/question-bank/editor-adapters/choice-adapters.d.ts +5 -0
- package/dist/components/questions/question-bank/editor-adapters/choice-adapters.js +101 -0
- package/dist/components/questions/question-bank/editor-adapters/index.d.ts +5 -0
- package/dist/components/questions/question-bank/editor-adapters/index.js +4 -0
- package/dist/components/questions/question-bank/editor-adapters/media-adapters.d.ts +6 -0
- package/dist/components/questions/question-bank/editor-adapters/media-adapters.js +140 -0
- package/dist/components/questions/question-bank/editor-adapters/types.d.ts +1 -0
- package/dist/components/questions/question-bank/editor-adapters/types.js +1 -0
- package/dist/components/questions/question-bank/editor-adapters/unsupported-fallback.d.ts +8 -0
- package/dist/components/questions/question-bank/editor-adapters/unsupported-fallback.js +6 -0
- package/dist/components/questions/question-bank/editor-adapters/writing-adapters.d.ts +5 -0
- package/dist/components/questions/question-bank/editor-adapters/writing-adapters.js +114 -0
- package/dist/components/questions/question-bank/index.d.ts +4 -0
- package/dist/components/questions/question-bank/index.js +3 -0
- package/dist/components/questions/question-bank/types.d.ts +59 -0
- package/dist/components/questions/question-bank/types.js +1 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +37 -1
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +351 -1
- package/dist/components/questions/types/answer-the-question/register.js +67 -1
- package/dist/components/questions/types/answer-the-question/transform.js +36 -1
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +141 -1
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +118 -1
- package/dist/components/questions/types/arrange-letters-into-words/index.js +2 -1
- package/dist/components/questions/types/arrange-letters-into-words/register.js +4 -1
- package/dist/components/questions/types/arrange-letters-into-words/transform.js +24 -1
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +74 -1
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +169 -1
- package/dist/components/questions/types/choose-correct-adjective/index.js +2 -1
- package/dist/components/questions/types/choose-correct-adjective/register.js +55 -1
- package/dist/components/questions/types/choose-correct-adjective/transform.js +16 -1
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +149 -1
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +568 -1
- package/dist/components/questions/types/choose-the-correct-answer/index.js +3 -1
- package/dist/components/questions/types/choose-the-correct-answer/normalize.d.ts +9 -0
- package/dist/components/questions/types/choose-the-correct-answer/normalize.js +88 -1
- package/dist/components/questions/types/choose-the-correct-answer/register.js +99 -1
- package/dist/components/questions/types/choose-the-correct-answer/transform.js +70 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +22 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +163 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/index.js +3 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +94 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/register.js +36 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +55 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +27 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +231 -1
- package/dist/components/questions/types/cross-out-word-group/index.js +5 -1
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +110 -1
- package/dist/components/questions/types/cross-out-word-group/register.js +36 -1
- package/dist/components/questions/types/cross-out-word-group/transform.js +67 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +275 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +464 -1
- package/dist/components/questions/types/fill-in-blank/index.js +2 -1
- package/dist/components/questions/types/fill-in-blank/register.js +148 -1
- package/dist/components/questions/types/fill-in-blank/transform.js +88 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +117 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +671 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/index.js +2 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/register.js +37 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +15 -1
- package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +262 -1
- package/dist/components/questions/types/gn-speaking-interview/register.js +121 -1
- package/dist/components/questions/types/gn-speaking-interview/transform.js +48 -1
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +16 -1
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +27 -1
- package/dist/components/questions/types/image-object-matching/index.js +2 -1
- package/dist/components/questions/types/image-object-matching/register.js +3 -1
- package/dist/components/questions/types/image-object-matching/transform.js +2 -1
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +102 -1
- package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +344 -1
- package/dist/components/questions/types/label-the-picture/index.js +2 -1
- package/dist/components/questions/types/label-the-picture/register.js +54 -1
- package/dist/components/questions/types/label-the-picture/transform.js +28 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +78 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +232 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +2 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +82 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +27 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +70 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +386 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +2 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +100 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +33 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +106 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +457 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +2 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +110 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +35 -1
- package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +8 -1
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +22 -1
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +291 -1
- package/dist/components/questions/types/listen-and-speak-answer/index.js +2 -1
- package/dist/components/questions/types/listen-and-speak-answer/register.js +84 -1
- package/dist/components/questions/types/listen-and-speak-answer/transform.js +27 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +32 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +295 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/register.js +79 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +27 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +34 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +285 -1
- package/dist/components/questions/types/listen-and-speak-image-group/register.js +81 -1
- package/dist/components/questions/types/listen-and-speak-image-group/transform.js +26 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +38 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +385 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +87 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +32 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +26 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +247 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +59 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +18 -1
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +26 -1
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +256 -1
- package/dist/components/questions/types/listen-and-speak-question-list/register.js +81 -1
- package/dist/components/questions/types/listen-and-speak-question-list/transform.js +26 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +26 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +230 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +56 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +16 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +72 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +230 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +66 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +42 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +96 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +238 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +2 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +76 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +19 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +134 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +617 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/index.js +2 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/register.js +82 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +29 -1
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +38 -1
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +359 -1
- package/dist/components/questions/types/match-by-writing-answer/index.js +3 -1
- package/dist/components/questions/types/match-by-writing-answer/register.js +58 -1
- package/dist/components/questions/types/match-by-writing-answer/transform.js +61 -1
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +16 -1
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +23 -1
- package/dist/components/questions/types/read-and-color-objects/index.js +2 -1
- package/dist/components/questions/types/read-and-color-objects/register.js +3 -1
- package/dist/components/questions/types/read-and-color-objects/transform.js +36 -1
- package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +125 -1
- package/dist/components/questions/types/read-displayed-content/register.js +64 -1
- package/dist/components/questions/types/read-displayed-content/transform.js +12 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +65 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +511 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/index.js +2 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/register.js +58 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +28 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +373 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +41 -1
- package/dist/components/questions/types/speaking-conversation/register.js +110 -1
- package/dist/components/questions/types/speaking-conversation/transform.js +34 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +74 -1
- package/dist/components/questions/types/speaking-cue-card/register.js +52 -1
- package/dist/components/questions/types/speaking-cue-card/transform.js +12 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +10 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +161 -1
- package/dist/components/questions/types/speaking-describe-image/index.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/register.js +69 -1
- package/dist/components/questions/types/speaking-describe-image/transform.js +16 -1
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +234 -1
- package/dist/components/questions/types/spontaneous-qa/register.js +93 -1
- package/dist/components/questions/types/spontaneous-qa/transform.js +34 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +9 -0
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +179 -0
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.d.ts +5 -0
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +43 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +14 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +69 -0
- package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/index.js +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/register.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/register.js +139 -0
- package/dist/components/questions/types/spontaneous-qa-group/transform.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +52 -0
- package/dist/components/questions/types/true-false/TrueFalseClient.js +75 -1
- package/dist/components/questions/types/true-false/TrueFalseCreator.js +244 -1
- package/dist/components/questions/types/true-false/index.js +2 -1
- package/dist/components/questions/types/true-false/register.js +77 -1
- package/dist/components/questions/types/true-false/transform.js +32 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +25 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupCreator.js +129 -1
- package/dist/components/questions/types/true-false-group/index.js +5 -1
- package/dist/components/questions/types/true-false-group/map-true-false-group-data.js +69 -1
- package/dist/components/questions/types/true-false-group/register.js +36 -1
- package/dist/components/questions/types/true-false-group/transform.js +43 -1
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +233 -1
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +631 -1
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +28 -1
- package/dist/components/questions/types/word-fill-paragraph/index.js +2 -1
- package/dist/components/questions/types/word-fill-paragraph/register.js +136 -1
- package/dist/components/questions/types/word-fill-paragraph/transform.js +56 -1
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +86 -1
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +246 -1
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.d.ts +2 -0
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +484 -1
- package/dist/components/questions/types/word-fill-structured-form/index.js +3 -1
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +113 -1
- package/dist/components/questions/types/word-fill-structured-form/register.js +36 -1
- package/dist/components/questions/types/word-fill-structured-form/transform.js +77 -1
- package/dist/components/questions/types/word-fill-structured-form/wfsf-word-bank.utils.js +110 -1
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.d.ts +3 -0
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +190 -0
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.d.ts +3 -0
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.js +182 -0
- package/dist/components/questions/types/word-order-and-match-group/index.d.ts +4 -0
- package/dist/components/questions/types/word-order-and-match-group/index.js +4 -0
- package/dist/components/questions/types/word-order-and-match-group/register.d.ts +2 -0
- package/dist/components/questions/types/word-order-and-match-group/register.js +121 -0
- package/dist/components/questions/types/word-order-and-match-group/transform.d.ts +2 -0
- package/dist/components/questions/types/word-order-and-match-group/transform.js +70 -0
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +84 -1
- package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +192 -1
- package/dist/components/questions/types/word-ordering/index.js +2 -1
- package/dist/components/questions/types/word-ordering/register.js +46 -1
- package/dist/components/questions/types/word-ordering/transform.js +24 -1
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +99 -1
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +320 -1
- package/dist/components/questions/types/write-a-short-letter/index.js +2 -1
- package/dist/components/questions/types/write-a-short-letter/register.js +68 -1
- package/dist/components/questions/types/write-a-short-letter/transform.js +25 -1
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +61 -1
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +196 -1
- package/dist/components/questions/types/write-correct-verb-form/index.js +2 -1
- package/dist/components/questions/types/write-correct-verb-form/register.js +62 -1
- package/dist/components/questions/types/write-correct-verb-form/transform.js +17 -1
- package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +22 -1
- package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +174 -1
- package/dist/components/questions/types/write-sentences/index.js +2 -1
- package/dist/components/questions/types/write-sentences/register.js +58 -1
- package/dist/components/questions/types/write-sentences/transform.js +28 -1
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +96 -1
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +161 -1
- package/dist/components/questions/types/write-short-paragraph/index.js +2 -1
- package/dist/components/questions/types/write-short-paragraph/register.js +4 -1
- package/dist/components/questions/types/write-short-paragraph/transform.js +21 -1
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +11 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +8 -1
- package/dist/components/questions/viewer/QuestionGroupViewer.js +36 -1
- package/dist/components/questions/viewer/QuestionViewer.js +692 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +2 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +82 -1
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +11 -1
- package/dist/components/questions/viewer/WordFillStructuredFormViewer.js +14 -1
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +95 -1
- package/dist/components/questions/viewer/index.js +8 -1
- package/dist/components/results/ResultReviewPageContainer.js +220 -1
- package/dist/components/results/ReviewQuestionRenderer.js +55 -1
- package/dist/components/results/hooks/useResultReviewPolling.js +76 -1
- package/dist/components/results/index.js +22 -1
- package/dist/components/results/renderers/QuestionExplanations.js +19 -1
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +94 -1
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +75 -1
- package/dist/components/results/renderers/ReviewColoringRenderer.js +110 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +48 -1
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +87 -1
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +104 -1
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +123 -1
- package/dist/components/results/renderers/ReviewListeningHeader.js +104 -1
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +116 -1
- package/dist/components/results/renderers/ReviewSpeakingMedia.js +48 -1
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +42 -1
- package/dist/components/results/renderers/ReviewSpeakingRenderer.js +12 -1
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +85 -1
- package/dist/components/results/renderers/review-question-body-dedicated.js +104 -1
- package/dist/components/results/renderers/review-question-body-movers.js +148 -1
- package/dist/components/results/renderers/review-speaking-map.js +226 -1
- package/dist/components/results/renderers/review-spontaneous-qa-group.d.ts +4 -0
- package/dist/components/results/renderers/review-spontaneous-qa-group.js +74 -0
- package/dist/components/results/report/BadgeTitle.js +5 -1
- package/dist/components/results/report/ExamSkillRow.js +20 -1
- package/dist/components/results/report/OceanEduExamReport.js +58 -1
- package/dist/components/results/report/OceanEduSpeakingReport.js +62 -1
- package/dist/components/results/report/OverallAssessmentSection.js +17 -1
- package/dist/components/results/report/ResultErrorState.js +18 -1
- package/dist/components/results/report/ResultLoadingState.js +10 -1
- package/dist/components/results/report/ResultNotPublicState.js +27 -1
- package/dist/components/results/report/SpeakingReportParts.js +14 -1
- package/dist/components/results/report/SpeakingReportShared.js +86 -1
- package/dist/components/results/report/SpeakingRubricSection.js +171 -1
- package/dist/components/results/report/speaking-report.utils.js +198 -1
- package/dist/components/results/result-review.utils.js +6 -1
- package/dist/components/results/review-data.js +222 -1
- package/dist/components/results/review-renderer.utils.js +97 -1
- package/dist/components/results/review-status.utils.js +36 -1
- package/dist/components/results/review-types.js +1 -1
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +77 -1
- package/dist/components/results/speaking/SpeakingReviewResult.js +48 -1
- package/dist/components/results/utils/review-score.utils.js +72 -1
- package/dist/components/results/utils/speaking-summary.js +224 -1
- package/dist/components/results/utils/transform-speaking-review.js +183 -1
- package/dist/components/shared/RichTextEditor.d.ts +9 -0
- package/dist/components/shared/RichTextEditor.js +47 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.js +100 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +140 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +10 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.d.ts +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +18 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +11 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +11 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.d.ts +2 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +19 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +13 -1
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.d.ts +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +3 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +6 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +5 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +9 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.d.ts +2 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +61 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.d.ts +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +25 -1
- package/dist/components/themes/cambridge-yle/index.js +13 -1
- package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +25 -1
- package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +89 -1
- package/dist/components/themes/english-certification/EcWordFillParagraph.js +56 -1
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +192 -1
- package/dist/components/themes/english-certification/EcWritingTask.js +60 -1
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +108 -1
- package/dist/components/themes/english-certification/EnglishCertificationBrand.js +11 -1
- package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +34 -1
- package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +15 -1
- package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +19 -1
- package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +166 -1
- package/dist/components/themes/english-certification/EnglishCertificationHeader.js +31 -1
- package/dist/components/themes/english-certification/EnglishCertificationInstructionBanner.js +79 -1
- package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +71 -1
- package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +79 -1
- package/dist/components/themes/english-certification/EnglishCertificationPartHeader.js +47 -1
- package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +77 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +25 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +36 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +81 -1
- package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +95 -1
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +18 -1
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +32 -1
- package/dist/components/themes/english-certification/english-cert-scroll.js +163 -1
- package/dist/components/themes/english-certification/english-certification-theme.js +58 -1
- package/dist/components/themes/english-certification/grouped-question-utils.js +243 -1
- package/dist/components/themes/english-certification/index.js +24 -1
- package/dist/components/themes/index.js +25 -1
- package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +170 -1
- package/dist/components/themes/summer-sky/SummerSkyExitModal.js +21 -1
- package/dist/components/themes/summer-sky/SummerSkyLayout.js +221 -1
- package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +18 -1
- package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +193 -1
- package/dist/components/themes/types.js +1 -1
- package/dist/components/ui/alert-dialog.js +26 -1
- package/dist/components/ui/alert.js +22 -1
- package/dist/components/ui/avatar.js +12 -1
- package/dist/components/ui/badge.js +38 -1
- package/dist/components/ui/button.js +35 -1
- package/dist/components/ui/calendar.js +76 -1
- package/dist/components/ui/card.js +16 -1
- package/dist/components/ui/carousel.js +91 -1
- package/dist/components/ui/checkbox.js +9 -1
- package/dist/components/ui/collapsible.js +13 -1
- package/dist/components/ui/command.js +34 -1
- package/dist/components/ui/confirm-dialog.js +49 -1
- package/dist/components/ui/dialog.js +23 -1
- package/dist/components/ui/drawer.js +35 -1
- package/dist/components/ui/dropdown-menu.js +33 -1
- package/dist/components/ui/file-upload.js +155 -1
- package/dist/components/ui/form.js +62 -1
- package/dist/components/ui/hover-card.js +10 -1
- package/dist/components/ui/image-preview.js +29 -1
- package/dist/components/ui/input.js +8 -1
- package/dist/components/ui/label.js +9 -1
- package/dist/components/ui/language-switcher.js +15 -1
- package/dist/components/ui/multi-select.js +90 -1
- package/dist/components/ui/points-input.js +33 -1
- package/dist/components/ui/popover.js +26 -1
- package/dist/components/ui/progress.js +16 -1
- package/dist/components/ui/radio-group.js +12 -1
- package/dist/components/ui/scroll-area.js +11 -1
- package/dist/components/ui/select.js +38 -1
- package/dist/components/ui/separator.js +8 -1
- package/dist/components/ui/sheet.js +40 -1
- package/dist/components/ui/single-select.js +76 -1
- package/dist/components/ui/skeleton.js +6 -1
- package/dist/components/ui/switch.js +8 -1
- package/dist/components/ui/table.js +20 -1
- package/dist/components/ui/tabs.js +17 -1
- package/dist/components/ui/textarea.js +8 -1
- package/dist/components/ui/tooltip.js +11 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +24 -1
- package/dist/shared/constants/ApiConstant.d.ts +1 -0
- package/dist/shared/constants/ApiConstant.js +278 -1
- package/dist/shared/constants/QueryKeyConstant.js +112 -1
- package/dist/shared/constants/exam-level.enum.js +41 -1
- package/dist/shared/constants/landings/placement-test.constants.js +431 -1
- package/dist/shared/constants/question-skills.js +277 -1
- package/dist/shared/constants/routes.js +81 -1
- package/dist/shared/index.js +10 -1
- package/dist/shared/lib/hooks/index.js +10 -1
- package/dist/shared/lib/hooks/useAlertDialog.js +57 -1
- package/dist/shared/lib/hooks/useAudioPlayCounter.js +30 -1
- package/dist/shared/lib/hooks/useAudioPreview.js +81 -1
- package/dist/shared/lib/hooks/useAudioUpload.js +24 -1
- package/dist/shared/lib/hooks/useConfirmDialog.js +89 -1
- package/dist/shared/lib/hooks/useDebouncedCallback.js +32 -1
- package/dist/shared/lib/hooks/useDelayedAutoPlay.js +12 -1
- package/dist/shared/lib/hooks/useFileUpload.js +24 -1
- package/dist/shared/lib/hooks/useImageUrl.js +5 -1
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +108 -1
- package/dist/shared/lib/hooks/useReactHookForm.js +31 -1
- package/dist/shared/lib/hooks/useToast.js +62 -1
- package/dist/shared/lib/html-content.js +22 -1
- package/dist/shared/lib/i18n/index.js +54 -1
- package/dist/shared/lib/i18n/messages/en/admin.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/en/admin.js +2111 -1
- package/dist/shared/lib/i18n/messages/en/auth.js +86 -1
- package/dist/shared/lib/i18n/messages/en/common.js +140 -1
- package/dist/shared/lib/i18n/messages/en/file-upload.js +18 -1
- package/dist/shared/lib/i18n/messages/en/student.js +133 -1
- package/dist/shared/lib/i18n/messages/en/terms.js +37 -1
- package/dist/shared/lib/i18n/messages/en.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/en.js +15 -1
- package/dist/shared/lib/i18n/messages/vi/admin.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/vi/admin.js +2111 -1
- package/dist/shared/lib/i18n/messages/vi/auth.js +86 -1
- package/dist/shared/lib/i18n/messages/vi/common.js +140 -1
- package/dist/shared/lib/i18n/messages/vi/file-upload.js +18 -1
- package/dist/shared/lib/i18n/messages/vi/student.js +133 -1
- package/dist/shared/lib/i18n/messages/vi/terms.js +37 -1
- package/dist/shared/lib/i18n/messages/vi.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/vi.js +15 -1
- package/dist/shared/lib/i18n/types.js +1 -1
- package/dist/shared/lib/stores/examQuestionStore.js +212 -1
- package/dist/shared/lib/stores/examTakingStore.js +240 -1
- package/dist/shared/lib/stores/gradingTransientStore.js +39 -1
- package/dist/shared/lib/stores/localeStore.js +10 -1
- package/dist/shared/lib/themes/config/starters.config.js +71 -1
- package/dist/shared/lib/themes/index.js +49 -1
- package/dist/shared/lib/themes/types.js +2 -1
- package/dist/shared/lib/toast.js +43 -1
- package/dist/shared/lib/utils/public-page.js +44 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +438 -1
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform-validation.js +194 -1
- package/dist/shared/lib/utils/question-transform.js +199 -1
- package/dist/shared/lib/utils.js +168 -1
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +111 -1
- package/dist/shared/lib/validation/label-the-picture.validation.js +19 -1
- package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +46 -1
- package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +21 -1
- package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +34 -1
- package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +40 -1
- package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +45 -1
- package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +19 -1
- package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +39 -1
- package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +32 -1
- package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +19 -1
- package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +28 -1
- package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +12 -1
- package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +47 -1
- package/dist/shared/lib/validation/read-and-color-objects.validation.js +29 -1
- package/dist/shared/lib/validation/read-displayed-content.validation.js +28 -1
- package/dist/shared/lib/validation/speaking-conversation.validation.js +75 -1
- package/dist/shared/lib/validation/speaking-describe-image.validation.js +31 -1
- package/dist/shared/lib/validation/spontaneous-qa.validation.js +48 -1
- package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +38 -1
- package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +39 -1
- package/dist/shared/lib/validation/word-fill-paragraph.validation.js +43 -1
- package/dist/shared/lib/validation/word-ordering.validation.js +33 -1
- package/dist/shared/types/branch.types.js +6 -1
- package/dist/shared/types/common.types.js +49 -1
- package/dist/shared/types/entities/exam-schedule.types.js +5 -1
- package/dist/shared/types/entities/exam.types.js +1 -1
- package/dist/shared/types/entities/question.types.js +4 -1
- package/dist/shared/types/entities/result.types.js +4 -1
- package/dist/shared/types/entities/user.types.js +4 -1
- package/dist/shared/types/exam-taking.types.js +2 -1
- package/dist/shared/types/history.types.js +34 -1
- package/dist/shared/types/index.js +1 -1
- package/dist/shared/types/questions/choose-correct-adjective.js +4 -1
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
- package/dist/shared/types/questions/choose-the-correct-answer-group.js +1 -1
- package/dist/shared/types/questions/choose-the-correct-answer.js +9 -1
- package/dist/shared/types/questions/cross-out-word-group.d.ts +2 -0
- package/dist/shared/types/questions/cross-out-word-group.js +1 -1
- package/dist/shared/types/questions/fill-missing-words-in-grid.js +11 -1
- package/dist/shared/types/questions/index.d.ts +1 -0
- package/dist/shared/types/questions/index.js +46 -1
- package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-info-exchange.js +3 -1
- package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +3 -1
- package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-with-story-images.js +3 -1
- package/dist/shared/types/questions/look-picture-choose-correct-answer.js +5 -1
- package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +9 -1
- package/dist/shared/types/questions/look-picture-fill-word-hint.js +7 -1
- package/dist/shared/types/questions/read-displayed-content.js +1 -1
- package/dist/shared/types/questions/speaking-conversation.js +7 -1
- package/dist/shared/types/questions/speaking-describe-image.js +1 -1
- package/dist/shared/types/questions/spontaneous-qa.js +1 -1
- package/dist/shared/types/questions/word-order-and-match-group.d.ts +33 -0
- package/dist/shared/types/questions/word-order-and-match-group.js +1 -0
- package/dist/shared/types/questions/write-correct-verb-form.js +12 -1
- package/dist/shared/types/role.types.js +1 -1
- package/dist/shared/types/structure-management.js +28 -1
- package/dist/shared/types/student.types.js +57 -1
- package/dist/shared/types/teacher-dashboard-rooms.js +2 -1
- package/package.json +184 -166
- package/dist/mcp/config.d.ts +0 -12
- package/dist/mcp/config.js +0 -1
- package/dist/mcp/http-client.d.ts +0 -6
- package/dist/mcp/http-client.js +0 -1
- package/dist/mcp/index.d.ts +0 -6
- package/dist/mcp/index.js +0 -2
- package/dist/mcp/tools/_helpers.d.ts +0 -4
- package/dist/mcp/tools/_helpers.js +0 -1
- package/dist/mcp/tools/exams.d.ts +0 -2
- package/dist/mcp/tools/exams.js +0 -1
- package/dist/mcp/tools/index.d.ts +0 -2
- package/dist/mcp/tools/index.js +0 -1
- package/dist/mcp/tools/results.d.ts +0 -2
- package/dist/mcp/tools/results.js +0 -1
|
@@ -1 +1,955 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useEffect as i,useRef as s,useState as r,useMemo as a,useCallback as o}from"react";import d from"next/image";import l from"../../../components/themes/summer-sky/assets/images/clock.png";import c from"../../../components/themes/summer-sky/assets/images/submit.png";import m from"../../../components/themes/summer-sky/assets/images/has_answered.png";import{toast as p}from"sonner";import{Loader2 as u}from"lucide-react";import{t as g}from"../../../shared/lib/i18n";import{transformAnswersForSubmission as h}from"./utils/answer-transformers";import{getThemeForLevel as x,normalizeExamTheme as b}from"../../../shared/lib/themes";import{useExamTakingStore as f}from"../../../shared/lib/stores/examTakingStore";import{useGradingTransientStore as I}from"../../../shared/lib/stores/gradingTransientStore";import{AlertDialog as w,AlertDialogAction as E,AlertDialogCancel as S,AlertDialogContent as y,AlertDialogDescription as _,AlertDialogFooter as v,AlertDialogHeader as N,AlertDialogTitle as A}from"../../../components/ui/alert-dialog";import{useExamTimer as T}from"./hooks/useExamTimer";import{useAutoSave as C}from"./hooks/useAutoSave";import{useProctoringCamera as k}from"./hooks/useProctoringCamera";import{getStandardExamTheme as P}from"../../../components/themes";import{QuestionRenderer as q}from"./components/QuestionRenderer";import{FullscreenLoading as M}from"../entry/fullscreen-loading";import{SummerSkySubmitModal as R}from"../../../components/themes/summer-sky/SummerSkySubmitModal";import{SummerSkyExitModal as D}from"../../../components/themes/summer-sky/SummerSkyExitModal";import{EnglishCertificationExamLayout as L,EnglishCertificationQuestionRenderer as G,scrollToEnglishCertQuestion as Q}from"../../../components/themes/english-certification";import{groupQuestionsByPart as B}from"./utils/question-transformers";import{unwrap as O,unwrapAttempt as Y}from"./exam-taking.utils";import{ExamTakingApiProvider as F}from"./ExamTakingApiContext";function K({roomId:F,api:K,onNavigate:U,resultPath:$=e=>`/student/results/${e}`,fallbackPath:V="/student/contests/discover",proctoringEnabled:j=!1,achieversExamTitle:H,onSubmitted:z}){const W=f(),J=s(!1),X=o((e,t=!1)=>{U?U(e,{replace:t}):"undefined"!=typeof window&&(t?window.location.replace(e):window.location.href=e)},[U]),{isCameraActive:Z}=k({api:K,attemptId:W.attemptId,intervalMs:1e4,enabled:j}),[ee,te]=r(!0),ne=W.currentPartIndex,ie=W.currentQuestionGlobalIndex,se=o(e=>{const t=f.getState().currentPartIndex,n="function"==typeof e?e(t):e;f.getState().setCurrentPartIndex(n)},[]),re=o(e=>{const t=f.getState().currentQuestionGlobalIndex,n="function"==typeof e?e(t):e;f.getState().setCurrentQuestionGlobalIndex(n)},[]),[ae,oe]=r([]),[de,le]=r("SUMMER_SKY"),[ce,me]=r(""),[pe,ue]=r(!1),[ge,he]=r(!1),[xe,be]=r(null),[fe,Ie]=r(""),we=o(e=>{f.getState().markIntroPlayed(e)},[]),Ee="TOEIC"===W.examType||"ENGLISH_CERTIFICATION"===ce,Se=a(()=>{if(!Ee||0===W.parts.length)return ne;const e=W.currentIndex;for(let t=0;t<W.parts.length;t++){const n=W.parts[t],i=n.startIndex??0,s=n.endIndex??i;if(e>=i&&e<=s)return t}return 0},[Ee,W.currentIndex,W.parts,ne]);i(()=>{(async()=>{try{te(!0);let e=O(await K.getActiveAttempt(F));e||(e=Y(O(await K.startAttempt(F))));let t=null;try{t=K.getRoom?O(await K.getRoom(F)):null,t&&be(t)}catch{}const{examTemplate:n,questions:i}=O(await K.getQuestions(e.id));n?.title&&Ie(n.title);const s=n?.parts??e.exam?.template?.parts,r=B(i,s),a=n?.subTitle??null,o=(r.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),r.some(e=>"SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)));oe(r);const d=n?.theme||e?.exam?.template?.theme||e?.exam?.theme||t?.exam?.template?.theme||t?.exam?.theme||"",l=b(d);if(o){const e=["SUMMER_SKY"].includes(d);le(e?d:"SUMMER_SKY")}const c=new Date(e.startedAt),m=60*e.durationMinutes*1e3,u=c.getTime()+m,h=Date.now();if(0===Math.max(0,u-h)){p.info(g("student.exam.take.timeExpired"));try{await K.submitAttempt(e.id)}catch{}return f.persist.clearStorage(),void X($(e.id))}W.hydrateFromServer({attemptId:e.id,examRoomId:F,questions:i.map(e=>({id:e.id,question_type:e.question_type,content:e.content,is_example:e.is_example,total_points:e.total_points,part_id:e.part_id,part_no:e.part_no,question_number:e.question_number})),durationMinutes:e.durationMinutes,startedAt:e.startedAt,examType:a??void 0,examTitle:n?.title??void 0,parts:s}),a&&W.setExamType(a),me(l),s&&W.setParts(s),te(!1)}catch(e){const t=e?.response?.data?.errorCode;if("ATTEMPT_ALREADY_SUBMITTED"===t)return p.info(g("student.exam.take.submitted")),void X(V,!0);console.error("Failed to load exam data:",e),p.error(g("student.exam.take.loadError")),X(V,!0)}})()},[F]);const ye=a(()=>ae.length>0&&ae.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),[ae]),_e=a(()=>{const e=ae[ne];return!!e&&(("SPEAKING_DESCRIBE_IMAGE"!==e.questionType&&"SPONTANEOUS_QA"!==e.questionType&&"SPEAKING_CUE_CARD"!==e.questionType&&"READ_DISPLAYED_CONTENT"!==e.questionType||"CAMBRIDGE_YLE"!==ce)&&("SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)))},[ae,ne,ce]),{remainingSeconds:ve,warningLevel:Ne}=T({onTimeExpired:async()=>{p.info(g("student.exam.take.autoSubmit")),await ke()},onWarningThreshold:ye||Ee?void 0:()=>{p.warning(g("student.exam.take.timeWarning"))},warningThresholdSeconds:ye||Ee?void 0:300,warningThresholds:Ee?[{seconds:900,level:"warning",callback:()=>p.warning("Còn 15 phút!",{duration:3e3})},{seconds:300,level:"critical",callback:()=>p.warning("Còn 5 phút! Hãy nộp bài ngay!",{duration:5e3})}]:ye?[{seconds:300,level:"warning",callback:()=>p.warning("Còn 5 phút! Hãy hoàn thành bài thi.",{duration:5e3})},{seconds:120,level:"critical",callback:()=>p.warning("Còn 2 phút! Bài thi sẽ được tự động nộp.",{duration:5e3})}]:void 0}),Ae=C({api:K,attemptId:W.attemptId||"",parts:ae,intervalMs:Ee?15e3:3e4,onSaveError:e=>{console.error("Save error:",e)}}),Te=s(Ae.saveNow),Ce=s(ae);i(()=>{Te.current=Ae.saveNow,Ce.current=ae}),i(()=>{const e=()=>{const e=f.getState();if(!e.attemptId||!e.isDirty||e.isSubmitting)return;const t=h(e.answers,Ce.current);if(0!==t.length)try{K.saveAnswers?.({attemptId:e.attemptId,answers:t,keepalive:!0})}catch{}},t=()=>{"hidden"===document.visibilityState&&e()},n=()=>{const e=f.getState();e.isDirty&&!e.isSubmitting&&Te.current()};return document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",e),window.addEventListener("online",n),()=>{document.removeEventListener("visibilitychange",t),window.removeEventListener("pagehide",e),window.removeEventListener("online",n)}},[]),i(()=>{ye||Te.current()},[W.currentIndex,ne,ie,ye]);const ke=async()=>{if(!J.current){J.current=!0,W.setIsSubmitting(!0),ue(!1);try{W.isDirty&&await Ae.saveNow();const e=O(await K.submitAttempt(W.attemptId)),t=e?.pendingAnswerIds??[];W.attemptId&&(I.getState().setPending(W.attemptId,t),z?.({attemptId:W.attemptId,pendingAnswerIds:t})),0===t.length?p.success(g("student.exam.take.submitSuccess")):p.success(g("student.exam.take.submittedAIGradingInProgress")),f.persist.clearStorage(),X($(W.attemptId),!0)}catch(e){console.error("Failed to submit exam:",e);const t=e?.response?.data?.errorCode;if("ATTEMPT_ALREADY_SUBMITTED"===t)return f.persist.clearStorage(),void X($(W.attemptId),!0);p.warning(g("student.exam.take.submitError")),J.current=!1,W.setIsSubmitting(!1)}}},Pe=o(()=>{ue(!0)},[]),qe=s(new Map),Me=s([]),Re=o((e,t)=>{W.setAnswer(e,t);const n=qe.current.get(e)||e,i=Me.current.findIndex(e=>e.questionId===n);-1!==i&&re(e=>e!==i?i:e)},[]),De=s("forward"),Le=e=>{if("CAMBRIDGE_YLE"!==ce)return!1;const t=ae[e];return"SPEAKING_DESCRIBE_IMAGE"===t?.questionType||"SPONTANEOUS_QA"===t?.questionType||"SPEAKING_CUE_CARD"===t?.questionType||"READ_DISPLAYED_CONTENT"===t?.questionType},Ge=()=>{const e=()=>{window.scrollTo({top:0,behavior:"smooth"});const e=document.getElementById("exam-main-scroll-area");if(e){e.scrollTo({top:0,behavior:"smooth"});e.querySelectorAll(".overflow-y-auto").forEach(e=>{e.scrollTo({top:0,behavior:"smooth"})})}};requestAnimationFrame(e),setTimeout(e,100),setTimeout(e,250)},Qe=()=>{if("ENGLISH_CERTIFICATION"===ce){const e=W.parts[Se+1];e&&W.goToQuestion(e.startIndex??0)}else if(Ee)W.goToQuestion(Math.min(W.currentIndex+1,W.questions.length-1));else if("CAMBRIDGE_YLE"===ce&&Le(ne)){const e=ie+1,t=We[e];t&&(t.partIndex!==ne&&(De.current="forward",se(t.partIndex)),re(e))}else De.current="forward",se(e=>{const t=Math.min(e+1,ae.length-1);if("CAMBRIDGE_YLE"===ce){const e=We.find(e=>e.partIndex===t);e&&re(e.globalIndex)}return t});Ge()},Be=()=>{if("ENGLISH_CERTIFICATION"===ce){const e=W.parts[Se-1];e&&W.goToQuestion(e.startIndex??0)}else if(Ee)W.goToQuestion(Math.max(W.currentIndex-1,0));else if("CAMBRIDGE_YLE"===ce&&Le(ne)){const e=ie-1,t=We[e];t&&(t.partIndex!==ne&&(De.current="backward",se(t.partIndex)),re(e))}else De.current="backward",se(e=>{const t=Math.max(e-1,0);if("CAMBRIDGE_YLE"===ce){const e=We.find(e=>e.partIndex===t);e&&re(e.globalIndex)}return t});Ge()},[Oe,Ye]=r(!1),Fe=()=>{he(!0)},Ke=a(()=>ae.reduce((e,t)=>e+("CAMBRIDGE_YLE"===ce?t.questions.length:t.questions.filter(e=>!e.is_example).length),0),[ae,ce]),Ue=a(()=>{const e=new Map;return ae.forEach(t=>{t.questions.forEach(n=>{if(n.is_example)return;const i=n.content;if("FILL_MISSING_WORDS_IN_GRID"===t.questionType&&i?.grid&&i.grid.forEach((t,i)=>{t.forEach((t,s)=>{e.set(`${i}-${s}`,n.id)})}),"LABEL_THE_PICTURE"===t.questionType&&i?.labels&&i.labels.forEach(t=>{t.id&&e.set(t.id,n.id)}),"READ_AND_COLOR_OBJECTS"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("IMAGE_OBJECT_MATCHING"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("LOOK_PICTURE_FILL_WORD_HINT"===t.questionType){const t=i?.word;if(t&&"string"==typeof t){const i=/\{(\d+)\}/g;let s;for(;null!==(s=i.exec(t));){const t=s[1];e.set(`${n.id}-blank-${t}`,n.id)}}}})}),e},[ae]);i(()=>{qe.current=Ue},[Ue]);const $e=a(()=>{if(!W.answers)return new Set;const e=new Set(ae.flatMap(e=>e.questions.filter(e=>!e.is_example).map(e=>e.id))),t=new Set;return Object.keys(W.answers).forEach(n=>{const i=W.answers[n];if(void 0===i||""===i)return;if(e.has(n))return void t.add(n);const s=Ue.get(n);s&&e.has(s)&&t.add(s)}),t},[W.answers,ae,Ue]),Ve=$e.size+("CAMBRIDGE_YLE"===ce?ae.reduce((e,t)=>e+t.questions.filter(e=>e.is_example).length,0):0),je=a(()=>xe?.exam?.name||xe?.name||(Ee?"TOEIC Listening and Reading Test":"Bài thi"),[xe,Ee]),He=a(()=>x(xe?.exam?.level).displayName,[xe?.exam?.level]),{sidebarParts:ze,sidebarQuestions:We}=a(()=>{const e=[],t=[];let n=0;return ae.forEach((i,s)=>{const r="CAMBRIDGE_YLE"===ce?i.questions:i.questions.filter(e=>!e.is_example);let a=0;r.forEach(e=>{const r=!("CAMBRIDGE_YLE"!==ce||!e.is_example)||$e.has(e.id);r&&a++;const o=i.questions.findIndex(t=>t.id===e.id);t.push({globalIndex:n,partIndex:s,indexInPart:o,questionNumber:e.question_number,isAnswered:r,questionId:e.id,isExample:e.is_example}),n++}),e.push({partIndex:s,partNo:i.partNo,name:i.name,category:i.category,points:i.points,totalQuestions:"CAMBRIDGE_YLE"===ce?i.questions.length:i.questions.filter(e=>!e.is_example).length,answeredCount:a})}),{sidebarParts:e,sidebarQuestions:t}},[ae,$e,ce]);if(i(()=>{Me.current=We},[We]),ee)return e(M,{});const Je=ne,Xe=ae[Je];if(!Xe)return e("div",{className:"flex min-h-screen items-center justify-center",children:t("div",{className:"text-center",children:[e("p",{className:"text-lg font-semibold text-red-500",children:"Không tìm thấy câu há»i"}),e("button",{type:"button",onClick:()=>X(V),className:"mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600",children:"Quay lại"})]})});if(_e)return t(n,{children:[e(q,{part:Xe,answers:W.answers||{},onAnswerChange:Re,isReviewMode:!1,onExit:Fe,onPartComplete:Je<ae.length-1?Qe:Pe,onPartBack:Je>0?Be:void 0,speakingTheme:de,initialQuestionIndex:"backward"===De.current?Xe.questions.length-1:0,skipTeacherVideo:W.playedIntroParts.has(Je)||fe!==H,onTeacherIntroPlayed:()=>we(Je)},`${Xe.partId}-${Je}`),"SUMMER_SKY"===de?pe&&e(R,{onClose:()=>ue(!1),onSubmit:ke,isSubmitting:W.isSubmitting}):e(w,{open:pe,onOpenChange:ue,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(S,{disabled:W.isSubmitting,children:"Quay lại"}),e(E,{onClick:ke,disabled:W.isSubmitting,children:W.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang nộp..."]}):"Nộp bài"})]})]})}),t("div",{className:"fixed top-8 right-6 z-[100] flex flex-col items-end gap-4 sm:gap-6 lg:top-10 lg:right-16",children:[t("div",{className:"flex items-center gap-4 sm:gap-6",children:[ve>0&&t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] backdrop-blur-[8px] transition-all duration-300 ease-in-out border-none shadow-[0_4px_20px_rgba(0,0,0,0.25)] "+("critical"===Ne?"bg-[rgba(220,38,38,0.85)] shadow-[0_0_24px_rgba(220,38,38,0.5)] animate-pulse":"warning"===Ne?"bg-[rgba(180,120,0,0.85)]":"bg-[#001590]"),children:[e(d,{src:l,alt:"clock",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:"'SVN-Volte Rounded', 'Quicksand', sans-serif",textShadow:"0 2px 4px rgba(0, 0, 0, 0.2)"},children:[Math.floor(ve/60).toString().padStart(2,"0"),":",(ve%60).toString().padStart(2,"0")]})]}),t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] bg-[#FF9900] shadow-[0_4px_20px_rgba(0,0,0,0.25)] border-none shrink-0",title:"Số câu hỏi đã hoàn thành",children:[e(d,{src:m,alt:"Progress",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:[Ve,"/",Ke]})]})]}),t("button",{onClick:Pe,disabled:W.isSubmitting,className:"group cursor-pointer flex items-center h-[40px] sm:h-[50px] w-[130px] sm:w-[170px] rounded-[33.5px] shadow-[0_4px_20px_rgba(0,0,0,0.25)] transition-all duration-300 hover:scale-105 active:scale-95 disabled:pointer-events-none disabled:opacity-60 border-none "+(W.isSubmitting?"bg-slate-400":"bg-[#16CA3B] hover:bg-[#15b736]"),children:[e(d,{src:c,alt:"Submit",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none transition-transform duration-300 group-hover:scale-110 z-10"}),W.isSubmitting?e("div",{className:"flex-1 flex justify-center pr-[10px] sm:pr-[16px]",children:e(u,{className:"h-6 w-6 animate-spin text-white"})}):e("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:"Nộp bài"})]})]}),W.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]});if(Ee){const i=ae[Se],s=W.parts[Se],r=s?Math.max(0,W.currentIndex-(s.startIndex??0)):0,a=(s?.startIndex??0)+1,o=(s?.endIndex??a-1)+1,d={examName:je,remainingSeconds:W.remainingSeconds,totalQuestions:Ke,answeredCount:Ve,isSubmitting:W.isSubmitting,onSubmit:ke,onClose:Fe,lastSavedAt:W.lastSavedAt,isSaving:Ae.isSaving,partLabel:(s?.name||i?.name||"").toUpperCase()||void 0,partSubLabel:s?.category||void 0,questionRange:a===o?`${a}`:`${a}–${o}`},l=i?.questions?.[r],c="ENGLISH_CERTIFICATION"===ce,m={onPrevious:Be,onClear:c?void 0:()=>{l&&Re(l.id,{optionId:null})},onToggleMark:c?void 0:()=>{l&&W.toggleFlag(l.id)},onNext:Qe,isMarked:!!l&&W.flaggedQuestions.has(l.id),showSecondaryActions:!c,isPreviousDisabled:c?Se<=0:W.currentIndex<=0,isNextDisabled:c?Se>=W.parts.length-1:W.currentIndex>=W.questions.length-1},p={parts:W.parts,questions:W.questions,answers:W.answers,flaggedQuestions:W.flaggedQuestions,currentIndex:W.currentIndex,onQuestionClick:e=>{W.goToQuestion(e);const t=W.questions[e];t&&Q(t.id,{behavior:"smooth",block:"start"})},onPartClick:e=>{const t=W.parts[e];if(t){const n=t.startIndex??0;W.goToQuestion(n),W.setCurrentPartIndex(e);const i=W.questions[n];i&&Q(i.id,{behavior:"smooth",block:"start"})}}};if(i&&s)return t(n,{children:[e(L,{headerProps:d,navigationProps:p,footerProps:m,children:e(G,{part:i,partConfig:s,currentQuestionIndex:r,answers:W.answers,flaggedQuestions:W.flaggedQuestions,onAnswerChange:Re,onToggleFlag:e=>W.toggleFlag(e),isReviewMode:!1,isAudioPlayed:W.playedListeningAudioParts.has(i.partId),onAudioFirstPlay:()=>W.markListeningAudioPlayed(i.partId)})}),W.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})}return t(n,{children:[(()=>{const{Layout:n,QuestionCard:i}=P(ce),s={headerProps:{examName:je,examType:"EXAM",levelLabel:He,totalQuestions:Ke,answeredCount:Ve,remainingSeconds:W.remainingSeconds,isSubmitting:W.isSubmitting,isCameraActive:Z,onSubmit:Pe},onPrevious:Be,onNext:Qe,onClose:Fe,isNavDisabled:W.isNavDisabled,partName:Xe?.name||(Xe?.partNo?`PART ${Xe.partNo}`:void 0)};if("CAMBRIDGE_YLE"===ce){const e=Le(Je);s.isFirstQuestion=e?0===ie:0===Je,s.isLastQuestion=e?ie===We.length-1:Je===ae.length-1,s.sidebarProps={parts:ze,questions:We,currentQuestionGlobalIndex:ie,isNavDisabled:W.isNavDisabled,onPartClick:e=>{if(W.isNavDisabled)return;De.current=e>Je?"forward":"backward",se(e);const t=We.find(t=>t.partIndex===e);t&&re(t.globalIndex),Ge()},onQuestionClick:e=>{if(W.isNavDisabled)return;const t=We[e];t&&t.partIndex!==Je&&(De.current=t.partIndex>Je?"forward":"backward",se(t.partIndex)),re(e),t&&setTimeout(()=>{const e=document.getElementById(`question-${t.questionId}`);e&&e.scrollIntoView({behavior:"smooth",block:"center"})},100)}}}else s.isFirstQuestion=0===Je,s.isLastQuestion=Je===ae.length-1;const r="CAMBRIDGE_YLE"===ce&&("SPEAKING_DESCRIBE_IMAGE"===Xe?.questionType||"SPONTANEOUS_QA"===Xe?.questionType||"SPEAKING_CUE_CARD"===Xe?.questionType||"READ_DISPLAYED_CONTENT"===Xe?.questionType),a=We[ie],o=r&&a?.partIndex===Je?a.indexInPart:void 0;return t(n,{...s,children:[e(i,{partName:Xe?.name||(Xe?.partNo?`PART ${Xe.partNo}`:void 0),children:e(q,{part:Xe,answers:W.answers||{},onAnswerChange:Re,isReviewMode:!1,currentQuestionIndex:o,onExit:Fe,onPartComplete:Je<ae.length-1?Qe:Pe,onPartBack:Je>0?Be:void 0,speakingTheme:"CAMBRIDGE_YLE"===ce?"CAMBRIDGE_YLE":de,initialQuestionIndex:"backward"===De.current?Xe?.questions.length-1:0,skipTeacherVideo:W.playedIntroParts.has(Je)||fe!==H,onTeacherIntroPlayed:()=>we(Je)})}),W.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})})(),"CAMBRIDGE_YLE"===ce?pe&&e(R,{onClose:()=>ue(!1),onSubmit:ke,isSubmitting:W.isSubmitting}):e(w,{open:pe,onOpenChange:ue,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(S,{disabled:W.isSubmitting,children:"Quay lại"}),e(E,{onClick:ke,disabled:W.isSubmitting,children:W.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Äang ná»™p..."]}):"Nộp bài"})]})]})}),ge&&e(D,{onClose:()=>he(!1),onConfirmExit:async()=>{if(!Oe){Ye(!0);try{const e=W.attemptId;e&&K.cancelAttempt&&await K.cancelAttempt(e)}catch(e){console.error("Failed to cancel attempt:",e)}finally{Ye(!1),he(!1),f.persist.clearStorage(),X(V)}}},isCancelling:Oe})]})}export function ExamTakingPageContainer(t){return e(F,{api:t.api,children:e(K,{...t})})}
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import summerSkyClockImg from '../../../components/themes/summer-sky/assets/images/clock.png';
|
|
6
|
+
import submitImg from '../../../components/themes/summer-sky/assets/images/submit.png';
|
|
7
|
+
import hasAnsweredImg from '../../../components/themes/summer-sky/assets/images/has_answered.png';
|
|
8
|
+
import { toast } from 'sonner';
|
|
9
|
+
import { Loader2 } from 'lucide-react';
|
|
10
|
+
import { t } from '../../../shared/lib/i18n';
|
|
11
|
+
import { transformAnswersForSubmission } from './utils/answer-transformers';
|
|
12
|
+
import { getThemeForLevel, normalizeExamTheme } from '../../../shared/lib/themes';
|
|
13
|
+
import { useExamTakingStore } from '../../../shared/lib/stores/examTakingStore';
|
|
14
|
+
import { useGradingTransientStore } from '../../../shared/lib/stores/gradingTransientStore';
|
|
15
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '../../../components/ui/alert-dialog';
|
|
16
|
+
import { useExamTimer } from './hooks/useExamTimer';
|
|
17
|
+
import { useAutoSave } from './hooks/useAutoSave';
|
|
18
|
+
import { useProctoringCamera } from './hooks/useProctoringCamera';
|
|
19
|
+
import { getStandardExamTheme } from '../../../components/themes';
|
|
20
|
+
import { QuestionRenderer } from './components/QuestionRenderer';
|
|
21
|
+
import { FullscreenLoading } from '../entry/fullscreen-loading';
|
|
22
|
+
import { SummerSkySubmitModal } from '../../../components/themes/summer-sky/SummerSkySubmitModal';
|
|
23
|
+
import { SummerSkyExitModal } from '../../../components/themes/summer-sky/SummerSkyExitModal';
|
|
24
|
+
import { EnglishCertificationExamLayout, EnglishCertificationQuestionRenderer, scrollToEnglishCertQuestion, } from '../../../components/themes/english-certification';
|
|
25
|
+
import { groupQuestionsByPart, } from './utils/question-transformers';
|
|
26
|
+
import { findSidebarQuestionForPart, getCambridgeYlePartQuestionCount, getPartInitialQuestionIndex, isCambridgeYleInlineSpeakingType, listSpontaneousQaGroupChildIds, } from './utils/cambridge-yle-inline-speaking';
|
|
27
|
+
import { unwrap, unwrapAttempt } from './exam-taking.utils';
|
|
28
|
+
import { ExamTakingApiProvider } from './ExamTakingApiContext';
|
|
29
|
+
function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (attemptId) => `/student/results/${attemptId}`, fallbackPath = '/student/contests/discover', proctoringEnabled = false, achieversExamTitle, onSubmitted, }) {
|
|
30
|
+
const store = useExamTakingStore();
|
|
31
|
+
const submittingRef = useRef(false);
|
|
32
|
+
const navigate = useCallback((href, replace = false) => {
|
|
33
|
+
if (onNavigate)
|
|
34
|
+
onNavigate(href, { replace });
|
|
35
|
+
else if (typeof window !== 'undefined') {
|
|
36
|
+
if (replace)
|
|
37
|
+
window.location.replace(href);
|
|
38
|
+
else
|
|
39
|
+
window.location.href = href;
|
|
40
|
+
}
|
|
41
|
+
}, [onNavigate]);
|
|
42
|
+
// Proctoring camera: starts capturing automatically once attemptId is available
|
|
43
|
+
const { isCameraActive } = useProctoringCamera({
|
|
44
|
+
api,
|
|
45
|
+
attemptId: store.attemptId,
|
|
46
|
+
intervalMs: 10000,
|
|
47
|
+
enabled: proctoringEnabled,
|
|
48
|
+
});
|
|
49
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
50
|
+
// Navigation position lives in the persisted store so it survives a reload.
|
|
51
|
+
// Wrappers accept either a value or an updater to match the previous
|
|
52
|
+
// useState-style call sites (e.g. setCurrentPartIndex(prev => prev + 1)).
|
|
53
|
+
const currentPartIndex = store.currentPartIndex;
|
|
54
|
+
const currentQuestionGlobalIndex = store.currentQuestionGlobalIndex;
|
|
55
|
+
const setCurrentPartIndex = useCallback((updater) => {
|
|
56
|
+
const prev = useExamTakingStore.getState().currentPartIndex;
|
|
57
|
+
const next = typeof updater === 'function' ? updater(prev) : updater;
|
|
58
|
+
useExamTakingStore.getState().setCurrentPartIndex(next);
|
|
59
|
+
}, []);
|
|
60
|
+
const setCurrentQuestionGlobalIndex = useCallback((updater) => {
|
|
61
|
+
const prev = useExamTakingStore.getState().currentQuestionGlobalIndex;
|
|
62
|
+
const next = typeof updater === 'function' ? updater(prev) : updater;
|
|
63
|
+
useExamTakingStore.getState().setCurrentQuestionGlobalIndex(next);
|
|
64
|
+
}, []);
|
|
65
|
+
const [parts, setParts] = useState([]);
|
|
66
|
+
const [speakingTheme, setSpeakingTheme] = useState('SUMMER_SKY');
|
|
67
|
+
const [examTheme, setExamTheme] = useState('CAMBRIDGE_YLE');
|
|
68
|
+
const [showSubmitConfirm, setShowSubmitConfirm] = useState(false);
|
|
69
|
+
const [showExitConfirm, setShowExitConfirm] = useState(false);
|
|
70
|
+
const [room, setRoom] = useState(null);
|
|
71
|
+
const [examTemplateTitle, setExamTemplateTitle] = useState('');
|
|
72
|
+
// Track which parts have already played their AI teacher intro video.
|
|
73
|
+
// Stored in the persisted store so a reload doesn't replay them.
|
|
74
|
+
const handleTeacherIntroPlayed = useCallback((partIndex) => {
|
|
75
|
+
useExamTakingStore.getState().markIntroPlayed(partIndex);
|
|
76
|
+
}, []);
|
|
77
|
+
// Use the branded English Certification layout when the exam is a TOEIC exam
|
|
78
|
+
// (detected via template subTitle) OR when the template explicitly selects the
|
|
79
|
+
// ENGLISH_CERTIFICATION theme (shared across TOEIC/IELTS certification exams).
|
|
80
|
+
const isToeicExam = store.examType === 'TOEIC' || examTheme === 'ENGLISH_CERTIFICATION';
|
|
81
|
+
// TOEIC: derive currentPartIndex from store.currentIndex and parts metadata
|
|
82
|
+
// This ensures the nav panel and question renderer stay in sync
|
|
83
|
+
const toeicCurrentPartIndex = useMemo(() => {
|
|
84
|
+
if (!isToeicExam || store.parts.length === 0)
|
|
85
|
+
return currentPartIndex;
|
|
86
|
+
const idx = store.currentIndex;
|
|
87
|
+
// Find which part contains the current question index
|
|
88
|
+
for (let i = 0; i < store.parts.length; i++) {
|
|
89
|
+
const p = store.parts[i];
|
|
90
|
+
const start = p.startIndex ?? 0;
|
|
91
|
+
const end = p.endIndex ?? start;
|
|
92
|
+
if (idx >= start && idx <= end)
|
|
93
|
+
return i;
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
}, [isToeicExam, store.currentIndex, store.parts, currentPartIndex]);
|
|
97
|
+
// Load exam data with attempt validation
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const loadExamData = async () => {
|
|
100
|
+
try {
|
|
101
|
+
setIsLoading(true);
|
|
102
|
+
// Query the IN_PROGRESS attempt for THIS room specifically so a reload
|
|
103
|
+
// resumes the same attempt instead of creating a duplicate.
|
|
104
|
+
let activeAttempt = unwrap(await api.getActiveAttempt(roomId));
|
|
105
|
+
// No IN_PROGRESS attempt for this room → start a new one. (start() is
|
|
106
|
+
// idempotent server-side and rejects with ALREADY_SUBMITTED when the
|
|
107
|
+
// student has already submitted and re-takes aren't allowed.)
|
|
108
|
+
if (!activeAttempt) {
|
|
109
|
+
activeAttempt = unwrapAttempt(unwrap(await api.startAttempt(roomId)));
|
|
110
|
+
}
|
|
111
|
+
// Fetch room data for title/level display
|
|
112
|
+
let fetchedRoom = null;
|
|
113
|
+
try {
|
|
114
|
+
fetchedRoom = api.getRoom ? unwrap(await api.getRoom(roomId)) : null;
|
|
115
|
+
if (fetchedRoom)
|
|
116
|
+
setRoom(fetchedRoom);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Non-critical: fallback to defaults if room fetch fails
|
|
120
|
+
}
|
|
121
|
+
// Case 4: Valid IN_PROGRESS attempt - load questions
|
|
122
|
+
// Backend returns { examTemplate, questions } — destructure accordingly
|
|
123
|
+
const { examTemplate, questions: rawQuestions } = unwrap(await api.getQuestions(activeAttempt.id));
|
|
124
|
+
// Store examTemplate title for conditional logic (e.g. skip teacher AI video)
|
|
125
|
+
if (examTemplate?.title) {
|
|
126
|
+
setExamTemplateTitle(examTemplate.title);
|
|
127
|
+
}
|
|
128
|
+
// Extract template parts from the questions response (preferred source)
|
|
129
|
+
// Falls back to attempt.exam.template.parts for backward compatibility
|
|
130
|
+
const templateParts = examTemplate?.parts ??
|
|
131
|
+
activeAttempt.exam?.template?.parts;
|
|
132
|
+
// Group questions by part with template metadata
|
|
133
|
+
const groupedParts = groupQuestionsByPart(rawQuestions, templateParts);
|
|
134
|
+
// Detect exam type from template metadata
|
|
135
|
+
const detectedExamType = examTemplate?.subTitle ?? null;
|
|
136
|
+
// Detect speaking exam: all questions have skill type SPEAKING
|
|
137
|
+
// This covers LISTEN_AND_CHOOSE_* and LISTEN_AND_DRAG_* types in Starters Part 1
|
|
138
|
+
// which do NOT start with LISTEN_AND_SPEAK_ but are still part of speaking exams.
|
|
139
|
+
const isSpeakingExam = groupedParts.every((p) => p.questions.every((q) => q.question_skill_type === 'SPEAKING'));
|
|
140
|
+
const hasAnySpeakingParts = groupedParts.some((p) => p.category === 'SPEAKING' || p.questions.every((q) => q.question_skill_type === 'SPEAKING'));
|
|
141
|
+
setParts(groupedParts);
|
|
142
|
+
const resolvedExamTheme = examTemplate?.theme ||
|
|
143
|
+
activeAttempt?.exam?.template?.theme ||
|
|
144
|
+
activeAttempt?.exam?.theme ||
|
|
145
|
+
fetchedRoom?.exam?.template?.theme ||
|
|
146
|
+
fetchedRoom?.exam?.theme ||
|
|
147
|
+
'';
|
|
148
|
+
const normalizedExamTheme = normalizeExamTheme(resolvedExamTheme) ||
|
|
149
|
+
(detectedExamType === 'TOEIC' ? 'ENGLISH_CERTIFICATION' : 'CAMBRIDGE_YLE');
|
|
150
|
+
// Extract speakingTheme if this is a speaking exam
|
|
151
|
+
if (hasAnySpeakingParts) {
|
|
152
|
+
// If the resolved theme isn't a known speaking theme, fallback to SUMMER_SKY
|
|
153
|
+
const isKnownSpeakingTheme = ['SUMMER_SKY'].includes(resolvedExamTheme);
|
|
154
|
+
const theme = isKnownSpeakingTheme ? resolvedExamTheme : 'SUMMER_SKY';
|
|
155
|
+
setSpeakingTheme(theme);
|
|
156
|
+
}
|
|
157
|
+
// Calculate remaining time
|
|
158
|
+
const startedAt = new Date(activeAttempt.startedAt);
|
|
159
|
+
const durationMs = activeAttempt.durationMinutes * 60 * 1000;
|
|
160
|
+
const endTime = startedAt.getTime() + durationMs;
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const remainingMs = Math.max(0, endTime - now);
|
|
163
|
+
// If time already elapsed before the exam could load, auto-submit the
|
|
164
|
+
// attempt (the server no longer auto-submits on expiry, so we must never
|
|
165
|
+
// leave it IN_PROGRESS/ungraded) and send the student to their results
|
|
166
|
+
// instead of dropping them on the discover page.
|
|
167
|
+
if (remainingMs === 0) {
|
|
168
|
+
toast.info(t('student.exam.take.timeExpired'));
|
|
169
|
+
try {
|
|
170
|
+
await api.submitAttempt(activeAttempt.id);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// ALREADY_SUBMITTED / finalize race is fine — the attempt is
|
|
174
|
+
// finalized either way; still route to the results page.
|
|
175
|
+
}
|
|
176
|
+
useExamTakingStore.persist.clearStorage();
|
|
177
|
+
navigate(resultPath(activeAttempt.id));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
store.hydrateFromServer({
|
|
181
|
+
attemptId: activeAttempt.id,
|
|
182
|
+
examRoomId: roomId,
|
|
183
|
+
questions: rawQuestions.map((q) => ({
|
|
184
|
+
id: q.id,
|
|
185
|
+
question_type: q.question_type,
|
|
186
|
+
content: q.content,
|
|
187
|
+
is_example: q.is_example,
|
|
188
|
+
total_points: q.total_points,
|
|
189
|
+
part_id: q.part_id,
|
|
190
|
+
part_no: q.part_no,
|
|
191
|
+
question_number: q.question_number,
|
|
192
|
+
})),
|
|
193
|
+
durationMinutes: activeAttempt.durationMinutes,
|
|
194
|
+
startedAt: activeAttempt.startedAt,
|
|
195
|
+
examType: detectedExamType ?? undefined,
|
|
196
|
+
examTitle: examTemplate?.title ?? undefined,
|
|
197
|
+
parts: templateParts,
|
|
198
|
+
});
|
|
199
|
+
if (detectedExamType) {
|
|
200
|
+
store.setExamType(detectedExamType);
|
|
201
|
+
}
|
|
202
|
+
// Store normalized exam theme for layout selection with fallback from room/attempt metadata
|
|
203
|
+
setExamTheme(normalizedExamTheme);
|
|
204
|
+
if (templateParts) {
|
|
205
|
+
store.setParts(templateParts);
|
|
206
|
+
}
|
|
207
|
+
setIsLoading(false);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
const errorCode = error?.response?.data?.errorCode;
|
|
211
|
+
if (errorCode === 'ATTEMPT_ALREADY_SUBMITTED') {
|
|
212
|
+
toast.info(t('student.exam.take.submitted'));
|
|
213
|
+
navigate(fallbackPath, true);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
console.error('Failed to load exam data:', error);
|
|
217
|
+
toast.error(t('student.exam.take.loadError'));
|
|
218
|
+
navigate(fallbackPath, true);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
loadExamData();
|
|
222
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
223
|
+
}, [roomId]);
|
|
224
|
+
// Detect speaking exam from loaded parts: all questions must have skill type SPEAKING.
|
|
225
|
+
// This covers Starters Part 1 types (LISTEN_AND_CHOOSE_*, LISTEN_AND_DRAG_*)
|
|
226
|
+
// which are speaking exam parts but don't start with LISTEN_AND_SPEAK_.
|
|
227
|
+
const isSpeakingExam = useMemo(() => parts.length > 0 &&
|
|
228
|
+
parts.every((p) => p.questions.every((q) => q.question_skill_type === 'SPEAKING')), [parts]);
|
|
229
|
+
// Check if current part is a speaking part based on category or question skill type
|
|
230
|
+
// On CAMBRIDGE_YLE these speaking types are routed through CambridgeYleExamLayout instead of the
|
|
231
|
+
// full-page speaking flow, so we exclude them here. On other themes (e.g. SUMMER_SKY) they go
|
|
232
|
+
// through the normal speaking flow.
|
|
233
|
+
const isCurrentPartSpeaking = useMemo(() => {
|
|
234
|
+
const part = parts[currentPartIndex];
|
|
235
|
+
if (!part)
|
|
236
|
+
return false;
|
|
237
|
+
const currentTheme = examTheme || 'CAMBRIDGE_YLE';
|
|
238
|
+
if (isCambridgeYleInlineSpeakingType(part.questionType) && currentTheme === 'CAMBRIDGE_YLE')
|
|
239
|
+
return false;
|
|
240
|
+
if (part.category === 'SPEAKING')
|
|
241
|
+
return true;
|
|
242
|
+
return part.questions.every((q) => q.question_skill_type === 'SPEAKING');
|
|
243
|
+
}, [parts, currentPartIndex, examTheme]);
|
|
244
|
+
// Auto-submit handler
|
|
245
|
+
const handleAutoSubmit = async () => {
|
|
246
|
+
toast.info(t('student.exam.take.autoSubmit'));
|
|
247
|
+
await handleSubmit();
|
|
248
|
+
};
|
|
249
|
+
// Timer hook — always called (React rules).
|
|
250
|
+
// For TOEIC, we use multiple warning thresholds (15min + 5min).
|
|
251
|
+
// For Speaking, we use 5min + 2min warnings then auto-submit.
|
|
252
|
+
const { remainingSeconds, warningLevel } = useExamTimer({
|
|
253
|
+
onTimeExpired: handleAutoSubmit,
|
|
254
|
+
onWarningThreshold: isSpeakingExam || isToeicExam
|
|
255
|
+
? undefined
|
|
256
|
+
: () => {
|
|
257
|
+
toast.warning(t('student.exam.take.timeWarning'));
|
|
258
|
+
},
|
|
259
|
+
warningThresholdSeconds: isSpeakingExam || isToeicExam ? undefined : 300,
|
|
260
|
+
warningThresholds: isToeicExam
|
|
261
|
+
? [
|
|
262
|
+
{
|
|
263
|
+
seconds: 900,
|
|
264
|
+
level: 'warning',
|
|
265
|
+
callback: () => toast.warning('Còn 15 phút!', { duration: 3000 }),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
seconds: 300,
|
|
269
|
+
level: 'critical',
|
|
270
|
+
callback: () => toast.warning('Còn 5 phút! Hãy nộp bài ngay!', { duration: 5000 }),
|
|
271
|
+
},
|
|
272
|
+
]
|
|
273
|
+
: isSpeakingExam
|
|
274
|
+
? [
|
|
275
|
+
{
|
|
276
|
+
seconds: 300,
|
|
277
|
+
level: 'warning',
|
|
278
|
+
callback: () => toast.warning('Còn 5 phút! Hãy hoàn thành bài thi.', { duration: 5000 }),
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
seconds: 120,
|
|
282
|
+
level: 'critical',
|
|
283
|
+
callback: () => toast.warning('Còn 2 phút! Bài thi sẽ được tự động nộp.', { duration: 5000 }),
|
|
284
|
+
},
|
|
285
|
+
]
|
|
286
|
+
: undefined,
|
|
287
|
+
});
|
|
288
|
+
// Auto-save hook — 15s interval for TOEIC, 30s for others
|
|
289
|
+
const autoSave = useAutoSave({
|
|
290
|
+
api,
|
|
291
|
+
attemptId: store.attemptId || '',
|
|
292
|
+
parts,
|
|
293
|
+
intervalMs: isToeicExam ? 15000 : 30000,
|
|
294
|
+
// No success toast: saves run on every answer change / navigation, so a
|
|
295
|
+
// "Äã lưu" toast each time would disrupt the student. Errors still notify.
|
|
296
|
+
// No error toast: saves run continuously while the student is working, and a
|
|
297
|
+
// red toast mid-exam is disruptive. localStorage + the keepalive flush are the
|
|
298
|
+
// safety net, so a transient save failure is logged silently and retried.
|
|
299
|
+
onSaveError: (error) => {
|
|
300
|
+
console.error('Save error:', error);
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
// Flush pending answers to the server when the tab is hidden/closed or the
|
|
304
|
+
// network reconnects, to shrink the up-to-30s auto-save gap. localStorage is
|
|
305
|
+
// the primary safety net; this just minimizes server-side data loss.
|
|
306
|
+
// Refs keep the listeners bound once while always reading the latest values.
|
|
307
|
+
const saveNowRef = useRef(autoSave.saveNow);
|
|
308
|
+
const partsRef = useRef(parts);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
saveNowRef.current = autoSave.saveNow;
|
|
311
|
+
partsRef.current = parts;
|
|
312
|
+
});
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
// Best-effort delivery that survives page unload (keepalive). Uses the same
|
|
315
|
+
// cookie-authenticated JSON endpoint as the regular auto-save.
|
|
316
|
+
const flushKeepalive = () => {
|
|
317
|
+
const s = useExamTakingStore.getState();
|
|
318
|
+
if (!s.attemptId || !s.isDirty || s.isSubmitting)
|
|
319
|
+
return;
|
|
320
|
+
const answersArray = transformAnswersForSubmission(s.answers, partsRef.current);
|
|
321
|
+
if (answersArray.length === 0)
|
|
322
|
+
return;
|
|
323
|
+
try {
|
|
324
|
+
// Best-effort delivery that survives page unload (keepalive) via the
|
|
325
|
+
// injected adapter's saveAnswers.
|
|
326
|
+
void api.saveAnswers?.({ attemptId: s.attemptId, answers: answersArray, keepalive: true });
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
/* ignore */
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const handleVisibility = () => {
|
|
333
|
+
if (document.visibilityState === 'hidden')
|
|
334
|
+
flushKeepalive();
|
|
335
|
+
};
|
|
336
|
+
const handleOnline = () => {
|
|
337
|
+
const s = useExamTakingStore.getState();
|
|
338
|
+
if (s.isDirty && !s.isSubmitting)
|
|
339
|
+
saveNowRef.current();
|
|
340
|
+
};
|
|
341
|
+
document.addEventListener('visibilitychange', handleVisibility);
|
|
342
|
+
window.addEventListener('pagehide', flushKeepalive);
|
|
343
|
+
window.addEventListener('online', handleOnline);
|
|
344
|
+
return () => {
|
|
345
|
+
document.removeEventListener('visibilitychange', handleVisibility);
|
|
346
|
+
window.removeEventListener('pagehide', flushKeepalive);
|
|
347
|
+
window.removeEventListener('online', handleOnline);
|
|
348
|
+
};
|
|
349
|
+
}, []);
|
|
350
|
+
// Persist answers on every question navigation (Next / Previous / sidebar jump)
|
|
351
|
+
// so a device crash or network drop doesn't lose answers the student already
|
|
352
|
+
// entered. Speaking exams upload audio per question and are graded per answer,
|
|
353
|
+
// so they're excluded here. Grading itself still runs only on submit.
|
|
354
|
+
//
|
|
355
|
+
// Deps are the navigation position ONLY (not autoSave.saveNow) so this effect
|
|
356
|
+
// fires on navigation rather than on every render — the per-second timer
|
|
357
|
+
// re-render would otherwise reset an interval-based trigger. saveNowRef always
|
|
358
|
+
// holds the latest saveNow, which itself no-ops when there are no dirty answers
|
|
359
|
+
// or a save is already in flight.
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (isSpeakingExam)
|
|
362
|
+
return;
|
|
363
|
+
void saveNowRef.current();
|
|
364
|
+
}, [store.currentIndex, currentPartIndex, currentQuestionGlobalIndex, isSpeakingExam]);
|
|
365
|
+
// Submit handler
|
|
366
|
+
const handleSubmit = async () => {
|
|
367
|
+
// Guard: use ref for synchronous mutex (store snapshot can be stale)
|
|
368
|
+
if (submittingRef.current)
|
|
369
|
+
return;
|
|
370
|
+
submittingRef.current = true;
|
|
371
|
+
store.setIsSubmitting(true);
|
|
372
|
+
setShowSubmitConfirm(false);
|
|
373
|
+
try {
|
|
374
|
+
// Save any pending changes first
|
|
375
|
+
if (store.isDirty) {
|
|
376
|
+
await autoSave.saveNow();
|
|
377
|
+
}
|
|
378
|
+
const submitResult = unwrap(await api.submitAttempt(store.attemptId));
|
|
379
|
+
const pendingAnswerIds = submitResult?.pendingAnswerIds ?? [];
|
|
380
|
+
// Stash pending IDs into transient store for the result page poller to pick up
|
|
381
|
+
if (store.attemptId) {
|
|
382
|
+
useGradingTransientStore.getState().setPending(store.attemptId, pendingAnswerIds);
|
|
383
|
+
onSubmitted?.({ attemptId: store.attemptId, pendingAnswerIds });
|
|
384
|
+
}
|
|
385
|
+
if (pendingAnswerIds.length === 0) {
|
|
386
|
+
toast.success(t('student.exam.take.submitSuccess'));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
toast.success(t('student.exam.take.submittedAIGradingInProgress'));
|
|
390
|
+
}
|
|
391
|
+
// Drop persisted progress so a reload after submit doesn't restore the
|
|
392
|
+
// already-submitted attempt. Clear storage only (keep in-memory state) to
|
|
393
|
+
// avoid flashing an empty exam before navigation completes.
|
|
394
|
+
useExamTakingStore.persist.clearStorage();
|
|
395
|
+
navigate(resultPath(store.attemptId), true);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
console.error('Failed to submit exam:', error);
|
|
399
|
+
// ALREADY_SUBMITTED is not a real error: the attempt is already finalized
|
|
400
|
+
// server-side (e.g. submitted in another tab, timer auto-submit, or a first
|
|
401
|
+
// submit that succeeded but the client saw a network blip). Treat it as done
|
|
402
|
+
// — drop persisted progress and send the student to their result page. No
|
|
403
|
+
// red toast to disrupt the exam experience.
|
|
404
|
+
const errorCode = error?.response?.data?.errorCode;
|
|
405
|
+
if (errorCode === 'ATTEMPT_ALREADY_SUBMITTED') {
|
|
406
|
+
useExamTakingStore.persist.clearStorage();
|
|
407
|
+
navigate(resultPath(store.attemptId), true);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Genuine failure (network / 500): keep the student on the page with their
|
|
411
|
+
// answers (auto-save + localStorage preserved them) and let them retry.
|
|
412
|
+
// Use a non-red warning toast so it informs without alarming mid-exam.
|
|
413
|
+
toast.warning(t('student.exam.take.submitError'));
|
|
414
|
+
submittingRef.current = false;
|
|
415
|
+
store.setIsSubmitting(false);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
// Request submit handler — shows confirm dialog for speaking exams
|
|
419
|
+
const handleRequestSubmit = useCallback(() => {
|
|
420
|
+
setShowSubmitConfirm(true);
|
|
421
|
+
}, []);
|
|
422
|
+
const subItemToQuestionMapRef = useRef(new Map());
|
|
423
|
+
const sidebarQuestionsRef = useRef([]);
|
|
424
|
+
// Answer change handler - stores answer by questionId
|
|
425
|
+
// Memoized to prevent child re-renders from timer updates
|
|
426
|
+
const handleAnswerChange = useCallback((questionId, value) => {
|
|
427
|
+
store.setAnswer(questionId, value);
|
|
428
|
+
const actualQuestionId = subItemToQuestionMapRef.current.get(questionId) || questionId;
|
|
429
|
+
const qIndex = sidebarQuestionsRef.current.findIndex((q) => q.questionId === actualQuestionId);
|
|
430
|
+
if (qIndex !== -1) {
|
|
431
|
+
setCurrentQuestionGlobalIndex((prev) => prev !== qIndex ? qIndex : prev);
|
|
432
|
+
}
|
|
433
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
434
|
+
}, []);
|
|
435
|
+
// Track navigation direction to initialize the first question when moving backward
|
|
436
|
+
const navDirectionRef = useRef('forward');
|
|
437
|
+
// Navigation handlers
|
|
438
|
+
// Helper: check if a part renders one question per screen (speaking parts in CambridgeYLE)
|
|
439
|
+
const isOneQuestionPerScreen = (partIdx) => {
|
|
440
|
+
const currentTheme = examTheme || 'CAMBRIDGE_YLE';
|
|
441
|
+
if (currentTheme !== 'CAMBRIDGE_YLE')
|
|
442
|
+
return false;
|
|
443
|
+
const part = parts[partIdx];
|
|
444
|
+
return isCambridgeYleInlineSpeakingType(part?.questionType);
|
|
445
|
+
};
|
|
446
|
+
const scrollToTop = () => {
|
|
447
|
+
const doScroll = () => {
|
|
448
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
449
|
+
const mainScroll = document.getElementById('exam-main-scroll-area');
|
|
450
|
+
if (mainScroll) {
|
|
451
|
+
mainScroll.scrollTo({ top: 0, behavior: 'smooth' });
|
|
452
|
+
// The actual scrollable element may be a nested child (e.g. overflow-y-auto)
|
|
453
|
+
// when the outer wrapper itself doesn't overflow. We might have multiple (e.g. split layout)
|
|
454
|
+
const nestedScrolls = mainScroll.querySelectorAll('.overflow-y-auto');
|
|
455
|
+
nestedScrolls.forEach(el => {
|
|
456
|
+
el.scrollTo({ top: 0, behavior: 'smooth' });
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
// Execute immediately, then after short delays to ensure DOM has updated
|
|
461
|
+
requestAnimationFrame(doScroll);
|
|
462
|
+
setTimeout(doScroll, 100);
|
|
463
|
+
setTimeout(doScroll, 250);
|
|
464
|
+
};
|
|
465
|
+
const handleNext = () => {
|
|
466
|
+
if (examTheme === 'ENGLISH_CERTIFICATION') {
|
|
467
|
+
// English Certification shows a whole part/section per screen — advance to
|
|
468
|
+
// the first question of the next part/section.
|
|
469
|
+
const nextPart = store.parts[toeicCurrentPartIndex + 1];
|
|
470
|
+
if (nextPart)
|
|
471
|
+
store.goToQuestion(nextPart.startIndex ?? 0);
|
|
472
|
+
}
|
|
473
|
+
else if (isToeicExam) {
|
|
474
|
+
store.goToQuestion(Math.min(store.currentIndex + 1, store.questions.length - 1));
|
|
475
|
+
}
|
|
476
|
+
else if (examTheme === 'CAMBRIDGE_YLE' && isOneQuestionPerScreen(currentPartIndex)) {
|
|
477
|
+
// Speaking: navigate question-by-question
|
|
478
|
+
const nextGlobalIdx = currentQuestionGlobalIndex + 1;
|
|
479
|
+
const nextQ = sidebarQuestions[nextGlobalIdx];
|
|
480
|
+
if (nextQ) {
|
|
481
|
+
if (nextQ.partIndex !== currentPartIndex) {
|
|
482
|
+
// Crossed into next part
|
|
483
|
+
navDirectionRef.current = 'forward';
|
|
484
|
+
setCurrentPartIndex(nextQ.partIndex);
|
|
485
|
+
}
|
|
486
|
+
setCurrentQuestionGlobalIndex(nextGlobalIdx);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// Last question in the entire exam — treat as submit
|
|
490
|
+
// (handled by isLastQuestion prop on the layout)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
navDirectionRef.current = 'forward';
|
|
495
|
+
setCurrentPartIndex((prev) => {
|
|
496
|
+
const nextPartIndex = Math.min(prev + 1, parts.length - 1);
|
|
497
|
+
if (examTheme === 'CAMBRIDGE_YLE') {
|
|
498
|
+
const firstQ = findSidebarQuestionForPart(sidebarQuestions, nextPartIndex, 'forward');
|
|
499
|
+
if (firstQ)
|
|
500
|
+
setCurrentQuestionGlobalIndex(firstQ.globalIndex);
|
|
501
|
+
}
|
|
502
|
+
return nextPartIndex;
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
scrollToTop();
|
|
506
|
+
};
|
|
507
|
+
const handlePrevious = () => {
|
|
508
|
+
if (examTheme === 'ENGLISH_CERTIFICATION') {
|
|
509
|
+
// Move to the first question of the previous part/section.
|
|
510
|
+
const prevPart = store.parts[toeicCurrentPartIndex - 1];
|
|
511
|
+
if (prevPart)
|
|
512
|
+
store.goToQuestion(prevPart.startIndex ?? 0);
|
|
513
|
+
}
|
|
514
|
+
else if (isToeicExam) {
|
|
515
|
+
store.goToQuestion(Math.max(store.currentIndex - 1, 0));
|
|
516
|
+
}
|
|
517
|
+
else if (examTheme === 'CAMBRIDGE_YLE' && isOneQuestionPerScreen(currentPartIndex)) {
|
|
518
|
+
// Speaking: navigate question-by-question
|
|
519
|
+
const prevGlobalIdx = currentQuestionGlobalIndex - 1;
|
|
520
|
+
const prevQ = sidebarQuestions[prevGlobalIdx];
|
|
521
|
+
if (prevQ) {
|
|
522
|
+
if (prevQ.partIndex !== currentPartIndex) {
|
|
523
|
+
// Crossed into previous part
|
|
524
|
+
navDirectionRef.current = 'backward';
|
|
525
|
+
setCurrentPartIndex(prevQ.partIndex);
|
|
526
|
+
}
|
|
527
|
+
setCurrentQuestionGlobalIndex(prevGlobalIdx);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
navDirectionRef.current = 'backward';
|
|
532
|
+
setCurrentPartIndex((prev) => {
|
|
533
|
+
const prevPartIndex = Math.max(prev - 1, 0);
|
|
534
|
+
if (examTheme === 'CAMBRIDGE_YLE') {
|
|
535
|
+
const lastQ = findSidebarQuestionForPart(sidebarQuestions, prevPartIndex, 'backward');
|
|
536
|
+
if (lastQ)
|
|
537
|
+
setCurrentQuestionGlobalIndex(lastQ.globalIndex);
|
|
538
|
+
}
|
|
539
|
+
return prevPartIndex;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
scrollToTop();
|
|
543
|
+
};
|
|
544
|
+
const [isCancelling, setIsCancelling] = useState(false);
|
|
545
|
+
// Close handler - show exit confirm modal before going back
|
|
546
|
+
const handleClose = () => {
|
|
547
|
+
setShowExitConfirm(true);
|
|
548
|
+
};
|
|
549
|
+
const handleConfirmExit = async () => {
|
|
550
|
+
if (isCancelling)
|
|
551
|
+
return;
|
|
552
|
+
setIsCancelling(true);
|
|
553
|
+
try {
|
|
554
|
+
const attemptId = store.attemptId;
|
|
555
|
+
if (attemptId && api.cancelAttempt) {
|
|
556
|
+
await api.cancelAttempt(attemptId);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
console.error('Failed to cancel attempt:', error);
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
setIsCancelling(false);
|
|
564
|
+
setShowExitConfirm(false);
|
|
565
|
+
useExamTakingStore.persist.clearStorage();
|
|
566
|
+
navigate(fallbackPath);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
// Calculate totals (exclude example questions unless CAMBRIDGE_YLE)
|
|
570
|
+
const totalQuestions = useMemo(() => {
|
|
571
|
+
return parts.reduce((sum, part) => sum +
|
|
572
|
+
(examTheme === 'CAMBRIDGE_YLE'
|
|
573
|
+
? getCambridgeYlePartQuestionCount(part)
|
|
574
|
+
: part.questions.filter((q) => !q.is_example).length), 0);
|
|
575
|
+
}, [parts, examTheme]);
|
|
576
|
+
// Build mapping: subItemKey -> questionId for complex question types
|
|
577
|
+
// This maps sub-item keys (grid cells, label IDs, region IDs) back to their parent question
|
|
578
|
+
const subItemToQuestionMap = useMemo(() => {
|
|
579
|
+
const map = new Map();
|
|
580
|
+
parts.forEach((part) => {
|
|
581
|
+
part.questions.forEach((question) => {
|
|
582
|
+
if (question.is_example)
|
|
583
|
+
return;
|
|
584
|
+
const content = question.content;
|
|
585
|
+
// FILL_MISSING_WORDS_IN_GRID: keys are "row-col" like "4-5"
|
|
586
|
+
if (part.questionType === 'FILL_MISSING_WORDS_IN_GRID' && content?.grid) {
|
|
587
|
+
content.grid.forEach((row, rowIdx) => {
|
|
588
|
+
row.forEach((_, colIdx) => {
|
|
589
|
+
map.set(`${rowIdx}-${colIdx}`, question.id);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
// LABEL_THE_PICTURE: keys are label IDs
|
|
594
|
+
if (part.questionType === 'LABEL_THE_PICTURE' && content?.labels) {
|
|
595
|
+
content.labels.forEach((label) => {
|
|
596
|
+
if (label.id)
|
|
597
|
+
map.set(label.id, question.id);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
// READ_AND_COLOR_OBJECTS: keys are region IDs
|
|
601
|
+
if (part.questionType === 'READ_AND_COLOR_OBJECTS') {
|
|
602
|
+
const regions = content?.regions || content?.regionNodes;
|
|
603
|
+
regions?.forEach((region) => {
|
|
604
|
+
if (region.id)
|
|
605
|
+
map.set(region.id, question.id);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
// IMAGE_OBJECT_MATCHING: same structure as READ_AND_COLOR_OBJECTS — keys are region IDs
|
|
609
|
+
if (part.questionType === 'IMAGE_OBJECT_MATCHING') {
|
|
610
|
+
const regions = content?.regions || content?.regionNodes;
|
|
611
|
+
regions?.forEach((region) => {
|
|
612
|
+
if (region.id)
|
|
613
|
+
map.set(region.id, question.id);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
// LOOK_PICTURE_FILL_WORD_HINT: single question with {0}, {1} placeholders → synthetic IDs
|
|
617
|
+
if (part.questionType === 'LOOK_PICTURE_FILL_WORD_HINT') {
|
|
618
|
+
const word = content?.word;
|
|
619
|
+
if (word && typeof word === 'string') {
|
|
620
|
+
const placeholderRegex = /\{(\d+)\}/g;
|
|
621
|
+
let match;
|
|
622
|
+
while ((match = placeholderRegex.exec(word)) !== null) {
|
|
623
|
+
const blankIndex = match[1];
|
|
624
|
+
map.set(`${question.id}-blank-${blankIndex}`, question.id);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (part.questionType === 'CROSS_OUT_WORD_GROUP' &&
|
|
629
|
+
Array.isArray(content?.items)) {
|
|
630
|
+
content.items.forEach((_, index) => {
|
|
631
|
+
map.set(`${question.id}-item-${index}`, question.id);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
return map;
|
|
637
|
+
}, [parts]);
|
|
638
|
+
useEffect(() => {
|
|
639
|
+
subItemToQuestionMapRef.current = subItemToQuestionMap;
|
|
640
|
+
}, [subItemToQuestionMap]);
|
|
641
|
+
const answeredQuestionsSet = useMemo(() => {
|
|
642
|
+
if (!store.answers)
|
|
643
|
+
return new Set();
|
|
644
|
+
// Get IDs of non-example questions only
|
|
645
|
+
const countableQuestionIds = new Set();
|
|
646
|
+
parts.forEach((part) => {
|
|
647
|
+
if (part.questionType === 'SPONTANEOUS_QA_GROUP') {
|
|
648
|
+
const parent = part.questions[0];
|
|
649
|
+
if (parent && !parent.is_example) {
|
|
650
|
+
listSpontaneousQaGroupChildIds(parent.id, parent.content).forEach((id) => {
|
|
651
|
+
countableQuestionIds.add(id);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
part.questions
|
|
657
|
+
.filter((q) => !q.is_example)
|
|
658
|
+
.forEach((q) => countableQuestionIds.add(q.id));
|
|
659
|
+
});
|
|
660
|
+
// Track which questions have been answered
|
|
661
|
+
const answeredQuestions = new Set();
|
|
662
|
+
Object.keys(store.answers).forEach((key) => {
|
|
663
|
+
const answer = store.answers[key];
|
|
664
|
+
if (answer === undefined || answer === '')
|
|
665
|
+
return;
|
|
666
|
+
// Direct question ID match
|
|
667
|
+
if (countableQuestionIds.has(key)) {
|
|
668
|
+
answeredQuestions.add(key);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
// Check if it's a sub-item key that maps to a question
|
|
672
|
+
const questionId = subItemToQuestionMap.get(key);
|
|
673
|
+
if (questionId && countableQuestionIds.has(questionId)) {
|
|
674
|
+
answeredQuestions.add(questionId);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
return answeredQuestions;
|
|
678
|
+
}, [store.answers, parts, subItemToQuestionMap]);
|
|
679
|
+
const answeredCount = answeredQuestionsSet.size + (examTheme === 'CAMBRIDGE_YLE'
|
|
680
|
+
? parts.reduce((sum, part) => sum + part.questions.filter(q => q.is_example).length, 0)
|
|
681
|
+
: 0);
|
|
682
|
+
// Compute exam title: always prioritize the real exam name from room details API
|
|
683
|
+
const examTitle = useMemo(() => {
|
|
684
|
+
return room?.exam?.name || room?.name || (isToeicExam ? 'TOEIC Listening and Reading Test' : 'Bài thi');
|
|
685
|
+
}, [room, isToeicExam]);
|
|
686
|
+
// Compute level label from the exam level
|
|
687
|
+
const levelLabel = useMemo(() => {
|
|
688
|
+
return getThemeForLevel(room?.exam?.level).displayName;
|
|
689
|
+
}, [room?.exam?.level]);
|
|
690
|
+
const { sidebarParts, sidebarQuestions } = useMemo(() => {
|
|
691
|
+
const sParts = [];
|
|
692
|
+
const sQuestions = [];
|
|
693
|
+
let globalIndex = 0;
|
|
694
|
+
parts.forEach((part, partIdx) => {
|
|
695
|
+
const parent = part.questions[0];
|
|
696
|
+
const shouldFlattenGroup = examTheme === 'CAMBRIDGE_YLE' &&
|
|
697
|
+
part.questionType === 'SPONTANEOUS_QA_GROUP' &&
|
|
698
|
+
!!parent;
|
|
699
|
+
const questionsToList = shouldFlattenGroup
|
|
700
|
+
? listSpontaneousQaGroupChildIds(parent.id, parent.content).map((childId, childIndex) => ({
|
|
701
|
+
id: childId,
|
|
702
|
+
is_example: false,
|
|
703
|
+
question_number: parent.question_number,
|
|
704
|
+
sub_question_number: childIndex + 1,
|
|
705
|
+
indexInPart: childIndex,
|
|
706
|
+
}))
|
|
707
|
+
: (examTheme === 'CAMBRIDGE_YLE'
|
|
708
|
+
? part.questions
|
|
709
|
+
: part.questions.filter((q) => !q.is_example)).map((q) => ({
|
|
710
|
+
id: q.id,
|
|
711
|
+
is_example: q.is_example,
|
|
712
|
+
question_number: q.question_number,
|
|
713
|
+
sub_question_number: undefined,
|
|
714
|
+
indexInPart: part.questions.findIndex((partQ) => partQ.id === q.id),
|
|
715
|
+
}));
|
|
716
|
+
let partAnsweredCount = 0;
|
|
717
|
+
questionsToList.forEach((q) => {
|
|
718
|
+
const isAnswered = examTheme === 'CAMBRIDGE_YLE' && q.is_example
|
|
719
|
+
? true
|
|
720
|
+
: answeredQuestionsSet.has(q.id);
|
|
721
|
+
if (isAnswered)
|
|
722
|
+
partAnsweredCount++;
|
|
723
|
+
sQuestions.push({
|
|
724
|
+
globalIndex,
|
|
725
|
+
partIndex: partIdx,
|
|
726
|
+
indexInPart: q.indexInPart,
|
|
727
|
+
questionNumber: q.question_number,
|
|
728
|
+
subQuestionNumber: q.sub_question_number,
|
|
729
|
+
isAnswered,
|
|
730
|
+
questionId: q.id,
|
|
731
|
+
isExample: q.is_example,
|
|
732
|
+
});
|
|
733
|
+
globalIndex++;
|
|
734
|
+
});
|
|
735
|
+
sParts.push({
|
|
736
|
+
partIndex: partIdx,
|
|
737
|
+
partNo: part.partNo,
|
|
738
|
+
name: part.name,
|
|
739
|
+
category: part.category,
|
|
740
|
+
points: part.points,
|
|
741
|
+
totalQuestions: examTheme === 'CAMBRIDGE_YLE'
|
|
742
|
+
? getCambridgeYlePartQuestionCount(part)
|
|
743
|
+
: part.questions.filter((q) => !q.is_example).length,
|
|
744
|
+
answeredCount: partAnsweredCount,
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
return { sidebarParts: sParts, sidebarQuestions: sQuestions };
|
|
748
|
+
}, [parts, answeredQuestionsSet, examTheme]);
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
sidebarQuestionsRef.current = sidebarQuestions;
|
|
751
|
+
}, [sidebarQuestions]);
|
|
752
|
+
if (isLoading) {
|
|
753
|
+
return _jsx(FullscreenLoading, {});
|
|
754
|
+
}
|
|
755
|
+
// Determine actual part index based on exam theme
|
|
756
|
+
const activePartIndex = currentPartIndex;
|
|
757
|
+
const currentPart = parts[activePartIndex];
|
|
758
|
+
if (!currentPart) {
|
|
759
|
+
return (_jsx("div", { className: "flex min-h-screen items-center justify-center", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-lg font-semibold text-red-500", children: "Kh\u00C3\u00B4ng t\u00C3\u00ACm th\u00E1\u00BA\u00A5y c\u00C3\u00A2u h\u00E1\u00BB\u008Fi" }), _jsx("button", { type: "button", onClick: () => navigate(fallbackPath), className: "mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600", children: "Quay l\u1EA1i" })] }) }));
|
|
760
|
+
}
|
|
761
|
+
// -------------------------------------------------------
|
|
762
|
+
// Speaking exam: renderers manage their own full-page layout.
|
|
763
|
+
// QuestionRenderer delegates to SpeakingQuestionRenderer →
|
|
764
|
+
// SpeakingExamLayout (full-screen, wooden-themed UI).
|
|
765
|
+
// We skip MoversExamLayout and MoversQuestionCard entirely.
|
|
766
|
+
// -------------------------------------------------------
|
|
767
|
+
if (isCurrentPartSpeaking) {
|
|
768
|
+
return (_jsxs(_Fragment, { children: [_jsx(QuestionRenderer, { part: currentPart, answers: store.answers || {}, onAnswerChange: handleAnswerChange, isReviewMode: false, onExit: handleClose, onPartComplete: activePartIndex < parts.length - 1 ? handleNext : handleRequestSubmit, onPartBack: activePartIndex > 0 ? handlePrevious : undefined, speakingTheme: speakingTheme, initialQuestionIndex: getPartInitialQuestionIndex(currentPart, navDirectionRef.current), skipTeacherVideo: store.playedIntroParts.has(activePartIndex) || examTemplateTitle !== achieversExamTitle, onTeacherIntroPlayed: () => handleTeacherIntroPlayed(activePartIndex) }, `${currentPart.partId}-${activePartIndex}`), speakingTheme === 'SUMMER_SKY' ? (showSubmitConfirm && (_jsx(SummerSkySubmitModal, { onClose: () => setShowSubmitConfirm(false), onSubmit: handleSubmit, isSubmitting: store.isSubmitting }))) : (_jsx(AlertDialog, { open: showSubmitConfirm, onOpenChange: setShowSubmitConfirm, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "N\u1ED9p b\u00E0i thi" }), _jsx(AlertDialogDescription, { children: "B\u1EA1n c\u00F3 ch\u1EAFc ch\u1EAFn mu\u1ED1n n\u1ED9p b\u00E0i kh\u00F4ng? Sau khi n\u1ED9p, b\u1EA1n s\u1EBD kh\u00F4ng th\u1EC3 thay \u0111\u1ED5i c\u00E2u tr\u1EA3 l\u1EDDi." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: store.isSubmitting, children: "Quay l\u1EA1i" }), _jsx(AlertDialogAction, { onClick: handleSubmit, disabled: store.isSubmitting, children: store.isSubmitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "\u0110ang n\u1ED9p..."] })) : ('Nộp bài') })] })] }) })), _jsxs("div", { className: "fixed top-8 right-6 z-[100] flex flex-col items-end gap-4 sm:gap-6 lg:top-10 lg:right-16", children: [_jsxs("div", { className: "flex items-center gap-4 sm:gap-6", children: [remainingSeconds > 0 && (_jsxs("div", { className: `flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] backdrop-blur-[8px] transition-all duration-300 ease-in-out border-none shadow-[0_4px_20px_rgba(0,0,0,0.25)] ${warningLevel === 'critical' ? 'bg-[rgba(220,38,38,0.85)] shadow-[0_0_24px_rgba(220,38,38,0.5)] animate-pulse' :
|
|
769
|
+
warningLevel === 'warning' ? 'bg-[rgba(180,120,0,0.85)]' :
|
|
770
|
+
'bg-[#001590]'}`, children: [_jsx(Image, { src: summerSkyClockImg, alt: "clock", width: 75, height: 75, className: "w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10" }), _jsxs("span", { className: "flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]", style: {
|
|
771
|
+
fontFamily: "'SVN-Volte Rounded', 'Quicksand', sans-serif",
|
|
772
|
+
textShadow: "0 2px 4px rgba(0, 0, 0, 0.2)"
|
|
773
|
+
}, children: [Math.floor(remainingSeconds / 60).toString().padStart(2, '0'), ":", (remainingSeconds % 60).toString().padStart(2, '0')] })] })), _jsxs("div", { className: "flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] bg-[#FF9900] shadow-[0_4px_20px_rgba(0,0,0,0.25)] border-none shrink-0", title: "S\u1ED1 c\u00E2u h\u1ECFi \u0111\u00E3 ho\u00E0n th\u00E0nh", children: [_jsx(Image, { src: hasAnsweredImg, alt: "Progress", width: 75, height: 75, className: "w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10" }), _jsxs("span", { className: "flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]", style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif', textShadow: '0 2px 4px rgba(0,0,0,0.2)' }, children: [answeredCount, "/", totalQuestions] })] })] }), _jsxs("button", { onClick: handleRequestSubmit, disabled: store.isSubmitting, className: `group cursor-pointer flex items-center h-[40px] sm:h-[50px] w-[130px] sm:w-[170px] rounded-[33.5px] shadow-[0_4px_20px_rgba(0,0,0,0.25)] transition-all duration-300 hover:scale-105 active:scale-95 disabled:pointer-events-none disabled:opacity-60 border-none ${store.isSubmitting ? 'bg-slate-400' : 'bg-[#16CA3B] hover:bg-[#15b736]'}`, children: [_jsx(Image, { src: submitImg, alt: "Submit", width: 75, height: 75, className: "w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none transition-transform duration-300 group-hover:scale-110 z-10" }), store.isSubmitting ? (_jsx("div", { className: "flex-1 flex justify-center pr-[10px] sm:pr-[16px]", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-white" }) })) : (_jsx("span", { className: "flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]", style: { fontFamily: '"SVN-Volte Rounded", "Quicksand", sans-serif', textShadow: '0 2px 4px rgba(0,0,0,0.2)' }, children: "N\u1ED9p b\u00E0i" }))] })] }), store.isSubmitting && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm", children: _jsxs("div", { className: "rounded-xl bg-white p-8 text-center shadow-2xl", children: [_jsx(Loader2, { className: "mx-auto h-12 w-12 animate-spin text-green-500" }), _jsx("p", { className: "mt-4 text-lg font-semibold text-gray-700", children: "\u0110ang n\u1ED9p b\u00E0i..." }), _jsx("p", { className: "mt-2 text-sm text-gray-500", children: "Vui l\u00F2ng \u0111\u1EE3i trong gi\u00E2y l\u00E1t" })] }) }))] }));
|
|
774
|
+
}
|
|
775
|
+
// -------------------------------------------------------
|
|
776
|
+
// TOEIC exam: use ToeicExamLayout with ToeicQuestionRenderer
|
|
777
|
+
// -------------------------------------------------------
|
|
778
|
+
if (isToeicExam) {
|
|
779
|
+
const toeicCurrentPart = parts[toeicCurrentPartIndex];
|
|
780
|
+
const toeicCurrentPartConfig = store.parts[toeicCurrentPartIndex];
|
|
781
|
+
// Index within current part (direct computation — no useMemo needed)
|
|
782
|
+
const toeicQuestionIndexInPart = toeicCurrentPartConfig
|
|
783
|
+
? Math.max(0, store.currentIndex - (toeicCurrentPartConfig.startIndex ?? 0))
|
|
784
|
+
: 0;
|
|
785
|
+
const toeicPartStart = (toeicCurrentPartConfig?.startIndex ?? 0) + 1;
|
|
786
|
+
const toeicPartEnd = (toeicCurrentPartConfig?.endIndex ?? toeicPartStart - 1) + 1;
|
|
787
|
+
const toeicHeaderProps = {
|
|
788
|
+
examName: examTitle,
|
|
789
|
+
remainingSeconds: store.remainingSeconds,
|
|
790
|
+
totalQuestions: totalQuestions,
|
|
791
|
+
answeredCount,
|
|
792
|
+
isSubmitting: store.isSubmitting,
|
|
793
|
+
onSubmit: handleSubmit,
|
|
794
|
+
onClose: handleClose,
|
|
795
|
+
lastSavedAt: store.lastSavedAt,
|
|
796
|
+
isSaving: autoSave.isSaving,
|
|
797
|
+
partLabel: (toeicCurrentPartConfig?.name || toeicCurrentPart?.name || '').toUpperCase() || undefined,
|
|
798
|
+
partSubLabel: toeicCurrentPartConfig?.category || undefined,
|
|
799
|
+
questionRange: toeicPartStart === toeicPartEnd ? `${toeicPartStart}` : `${toeicPartStart}–${toeicPartEnd}`,
|
|
800
|
+
};
|
|
801
|
+
const toeicCurrentQuestion = toeicCurrentPart?.questions?.[toeicQuestionIndexInPart];
|
|
802
|
+
// English Certification shows a whole part/section per screen: footer keeps
|
|
803
|
+
// only Previous/Next (section navigation); marking happens per question card.
|
|
804
|
+
const isEnglishCert = examTheme === 'ENGLISH_CERTIFICATION';
|
|
805
|
+
const toeicFooterProps = {
|
|
806
|
+
onPrevious: handlePrevious,
|
|
807
|
+
onClear: isEnglishCert
|
|
808
|
+
? undefined
|
|
809
|
+
: () => {
|
|
810
|
+
if (toeicCurrentQuestion)
|
|
811
|
+
handleAnswerChange(toeicCurrentQuestion.id, { optionId: null });
|
|
812
|
+
},
|
|
813
|
+
onToggleMark: isEnglishCert
|
|
814
|
+
? undefined
|
|
815
|
+
: () => {
|
|
816
|
+
if (toeicCurrentQuestion)
|
|
817
|
+
store.toggleFlag(toeicCurrentQuestion.id);
|
|
818
|
+
},
|
|
819
|
+
onNext: handleNext,
|
|
820
|
+
isMarked: toeicCurrentQuestion
|
|
821
|
+
? store.flaggedQuestions.has(toeicCurrentQuestion.id)
|
|
822
|
+
: false,
|
|
823
|
+
showSecondaryActions: !isEnglishCert,
|
|
824
|
+
isPreviousDisabled: isEnglishCert
|
|
825
|
+
? toeicCurrentPartIndex <= 0
|
|
826
|
+
: store.currentIndex <= 0,
|
|
827
|
+
isNextDisabled: isEnglishCert
|
|
828
|
+
? toeicCurrentPartIndex >= store.parts.length - 1
|
|
829
|
+
: store.currentIndex >= store.questions.length - 1,
|
|
830
|
+
};
|
|
831
|
+
const toeicNavProps = {
|
|
832
|
+
parts: store.parts,
|
|
833
|
+
questions: store.questions,
|
|
834
|
+
answers: store.answers,
|
|
835
|
+
flaggedQuestions: store.flaggedQuestions,
|
|
836
|
+
currentIndex: store.currentIndex,
|
|
837
|
+
onQuestionClick: (globalIdx) => {
|
|
838
|
+
store.goToQuestion(globalIdx);
|
|
839
|
+
// Scroll the chosen question card into view once the new part renders.
|
|
840
|
+
// Retries handle cross-part mounts; nested-scroll aware helper handles
|
|
841
|
+
// Reading split-view columns and sticky Listening audio clearance.
|
|
842
|
+
const targetQuestion = store.questions[globalIdx];
|
|
843
|
+
if (targetQuestion) {
|
|
844
|
+
scrollToEnglishCertQuestion(targetQuestion.id, {
|
|
845
|
+
behavior: 'smooth',
|
|
846
|
+
block: 'start',
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
onPartClick: (partIdx) => {
|
|
851
|
+
const partConfig = store.parts[partIdx];
|
|
852
|
+
if (partConfig) {
|
|
853
|
+
const firstQuestionIdx = partConfig.startIndex ?? 0;
|
|
854
|
+
store.goToQuestion(firstQuestionIdx);
|
|
855
|
+
store.setCurrentPartIndex(partIdx);
|
|
856
|
+
const targetQuestion = store.questions[firstQuestionIdx];
|
|
857
|
+
if (targetQuestion) {
|
|
858
|
+
scrollToEnglishCertQuestion(targetQuestion.id, {
|
|
859
|
+
behavior: 'smooth',
|
|
860
|
+
block: 'start',
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
if (toeicCurrentPart && toeicCurrentPartConfig) {
|
|
867
|
+
return (_jsxs(_Fragment, { children: [_jsx(EnglishCertificationExamLayout, { headerProps: toeicHeaderProps, navigationProps: toeicNavProps, footerProps: toeicFooterProps, children: _jsx(EnglishCertificationQuestionRenderer, { part: toeicCurrentPart, partConfig: toeicCurrentPartConfig, currentQuestionIndex: toeicQuestionIndexInPart, answers: store.answers, flaggedQuestions: store.flaggedQuestions, onAnswerChange: handleAnswerChange, onToggleFlag: (questionId) => store.toggleFlag(questionId), isReviewMode: false, isAudioPlayed: store.playedListeningAudioParts.has(toeicCurrentPart.partId), onAudioFirstPlay: () => store.markListeningAudioPlayed(toeicCurrentPart.partId) }) }), store.isSubmitting && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: _jsxs("div", { className: "rounded-xl bg-white p-8 text-center shadow-2xl", children: [_jsx(Loader2, { className: "mx-auto h-12 w-12 animate-spin text-green-500" }), _jsx("p", { className: "mt-4 text-lg font-semibold text-gray-700", children: "\u0110ang n\u1ED9p b\u00E0i..." }), _jsx("p", { className: "mt-2 text-sm text-gray-500", children: "Vui l\u00F2ng \u0111\u1EE3i trong gi\u00E2y l\u00E1t" })] }) }))] }));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// -------------------------------------------------------
|
|
871
|
+
// Standard exam (Reading / Listening / Writing)
|
|
872
|
+
// -------------------------------------------------------
|
|
873
|
+
return (_jsxs(_Fragment, { children: [(() => {
|
|
874
|
+
const { Layout: ExamLayout, QuestionCard } = getStandardExamTheme(examTheme);
|
|
875
|
+
const layoutProps = {
|
|
876
|
+
headerProps: {
|
|
877
|
+
examName: examTitle,
|
|
878
|
+
examType: 'EXAM',
|
|
879
|
+
levelLabel,
|
|
880
|
+
totalQuestions,
|
|
881
|
+
answeredCount,
|
|
882
|
+
remainingSeconds: store.remainingSeconds,
|
|
883
|
+
isSubmitting: store.isSubmitting,
|
|
884
|
+
isCameraActive,
|
|
885
|
+
onSubmit: handleRequestSubmit,
|
|
886
|
+
},
|
|
887
|
+
onPrevious: handlePrevious,
|
|
888
|
+
onNext: handleNext,
|
|
889
|
+
onClose: handleClose,
|
|
890
|
+
isNavDisabled: store.isNavDisabled,
|
|
891
|
+
partName: currentPart?.name || (currentPart?.partNo ? `PART ${currentPart.partNo}` : undefined),
|
|
892
|
+
};
|
|
893
|
+
if (examTheme === 'CAMBRIDGE_YLE') {
|
|
894
|
+
const isOneQ = isOneQuestionPerScreen(activePartIndex);
|
|
895
|
+
layoutProps.isFirstQuestion = isOneQ ? currentQuestionGlobalIndex === 0 : activePartIndex === 0;
|
|
896
|
+
layoutProps.isLastQuestion = isOneQ ? currentQuestionGlobalIndex === sidebarQuestions.length - 1 : activePartIndex === parts.length - 1;
|
|
897
|
+
layoutProps.sidebarProps = {
|
|
898
|
+
parts: sidebarParts,
|
|
899
|
+
questions: sidebarQuestions,
|
|
900
|
+
currentQuestionGlobalIndex,
|
|
901
|
+
isNavDisabled: store.isNavDisabled,
|
|
902
|
+
onPartClick: (idx) => {
|
|
903
|
+
if (store.isNavDisabled)
|
|
904
|
+
return;
|
|
905
|
+
navDirectionRef.current = idx > activePartIndex ? 'forward' : 'backward';
|
|
906
|
+
setCurrentPartIndex(idx);
|
|
907
|
+
const targetQ = findSidebarQuestionForPart(sidebarQuestions, idx, navDirectionRef.current);
|
|
908
|
+
if (targetQ)
|
|
909
|
+
setCurrentQuestionGlobalIndex(targetQ.globalIndex);
|
|
910
|
+
scrollToTop();
|
|
911
|
+
},
|
|
912
|
+
onQuestionClick: (idx) => {
|
|
913
|
+
if (store.isNavDisabled)
|
|
914
|
+
return;
|
|
915
|
+
const question = sidebarQuestions[idx];
|
|
916
|
+
if (question && question.partIndex !== activePartIndex) {
|
|
917
|
+
navDirectionRef.current = question.partIndex > activePartIndex ? 'forward' : 'backward';
|
|
918
|
+
setCurrentPartIndex(question.partIndex);
|
|
919
|
+
}
|
|
920
|
+
setCurrentQuestionGlobalIndex(idx);
|
|
921
|
+
// Scroll to the specific question
|
|
922
|
+
if (question) {
|
|
923
|
+
setTimeout(() => {
|
|
924
|
+
const el = document.getElementById(`question-${question.questionId}`);
|
|
925
|
+
if (el) {
|
|
926
|
+
// Add a small offset if there's a sticky header, but block: 'center' or 'start' usually works
|
|
927
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
928
|
+
}
|
|
929
|
+
}, 100);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
layoutProps.isFirstQuestion = activePartIndex === 0;
|
|
936
|
+
layoutProps.isLastQuestion = activePartIndex === parts.length - 1;
|
|
937
|
+
}
|
|
938
|
+
// For speaking questions using the standard layout (like SPEAKING_DESCRIBE_IMAGE),
|
|
939
|
+
// we want to render them one-by-one according to the global index.
|
|
940
|
+
const isSpeakingUsingLayout = examTheme === 'CAMBRIDGE_YLE' && isCambridgeYleInlineSpeakingType(currentPart?.questionType);
|
|
941
|
+
const currentQuestionInSidebar = sidebarQuestions[currentQuestionGlobalIndex];
|
|
942
|
+
const currentQuestionIndexInPart = isSpeakingUsingLayout
|
|
943
|
+
? (currentQuestionInSidebar?.partIndex === activePartIndex ? currentQuestionInSidebar.indexInPart : undefined)
|
|
944
|
+
: undefined; // Force all questions to render for Reading/Writing
|
|
945
|
+
return (_jsxs(ExamLayout, { ...layoutProps, children: [_jsx(QuestionCard, { partName: currentPart?.name || (currentPart?.partNo ? `PART ${currentPart.partNo}` : undefined), children: _jsx(QuestionRenderer, { part: currentPart, answers: store.answers || {}, onAnswerChange: handleAnswerChange, isReviewMode: false, currentQuestionIndex: currentQuestionIndexInPart, onExit: handleClose, onPartComplete: activePartIndex < parts.length - 1 ? handleNext : handleRequestSubmit, onPartBack: activePartIndex > 0 ? handlePrevious : undefined, speakingTheme: examTheme === 'CAMBRIDGE_YLE' ? 'CAMBRIDGE_YLE' : speakingTheme, initialQuestionIndex: getPartInitialQuestionIndex(currentPart, navDirectionRef.current), skipTeacherVideo: store.playedIntroParts.has(activePartIndex) || examTemplateTitle !== achieversExamTitle, onTeacherIntroPlayed: () => handleTeacherIntroPlayed(activePartIndex) }) }), store.isSubmitting && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: _jsxs("div", { className: "rounded-xl bg-white p-8 text-center shadow-2xl", children: [_jsx(Loader2, { className: "mx-auto h-12 w-12 animate-spin text-green-500" }), _jsx("p", { className: "mt-4 text-lg font-semibold text-gray-700", children: "\u0110ang n\u1ED9p b\u00E0i..." }), _jsx("p", { className: "mt-2 text-sm text-gray-500", children: "Vui l\u00F2ng \u0111\u1EE3i trong gi\u00E2y l\u00E1t" })] }) }))] }));
|
|
946
|
+
})(), examTheme === 'CAMBRIDGE_YLE' ? (showSubmitConfirm && (_jsx(SummerSkySubmitModal, { onClose: () => setShowSubmitConfirm(false), onSubmit: handleSubmit, isSubmitting: store.isSubmitting }))) : (_jsx(AlertDialog, { open: showSubmitConfirm, onOpenChange: setShowSubmitConfirm, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "N\u1ED9p b\u00E0i thi" }), _jsx(AlertDialogDescription, { children: "B\u1EA1n c\u00F3 ch\u1EAFc ch\u1EAFn mu\u1ED1n n\u1ED9p b\u00E0i kh\u00F4ng? Sau khi n\u1ED9p, b\u1EA1n s\u1EBD kh\u00F4ng th\u1EC3 thay \u0111\u1ED5i c\u00E2u tr\u1EA3 l\u1EDDi." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: store.isSubmitting, children: "Quay l\u1EA1i" }), _jsx(AlertDialogAction, { onClick: handleSubmit, disabled: store.isSubmitting, children: store.isSubmitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "\u00C4\u0090ang n\u00E1\u00BB\u2122p..."] })) : ('Nộp bài') })] })] }) })), showExitConfirm && (_jsx(SummerSkyExitModal, { onClose: () => setShowExitConfirm(false), onConfirmExit: handleConfirmExit, isCancelling: isCancelling }))] }));
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* SDK container for the student exam-taking screen. Wraps the orchestration in an
|
|
950
|
+
* {@link ExamTakingApiProvider} so descendants (e.g. the speaking recorder) can
|
|
951
|
+
* reach the injected API without prop-drilling.
|
|
952
|
+
*/
|
|
953
|
+
export function ExamTakingPageContainer(props) {
|
|
954
|
+
return (_jsx(ExamTakingApiProvider, { api: props.api, children: _jsx(ExamTakingPageContainerInner, { ...props }) }));
|
|
955
|
+
}
|