@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.3
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/dist/api/exam-create.js +1 -179
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +1 -98
- package/dist/api/exam-questions.types.d.ts +45 -1
- package/dist/api/exam-questions.types.js +1 -1
- package/dist/api/exam-taking.js +1 -73
- package/dist/api/exam-taking.types.js +1 -1
- package/dist/api/index.js +1 -6
- package/dist/api/result-review.js +1 -50
- package/dist/api/result-review.types.js +1 -1
- package/dist/components/CueCardEditor.js +1 -58
- package/dist/components/common/ResolvedImage.js +1 -15
- package/dist/components/exams/CompletionDialog.d.ts +8 -0
- package/dist/components/exams/CompletionDialog.js +1 -0
- package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
- package/dist/components/exams/CreateExamInfoForm.js +1 -35
- package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
- package/dist/components/exams/CreateExamPageContainer.js +1 -103
- package/dist/components/exams/ExamCreator.d.ts +9 -3
- package/dist/components/exams/ExamCreator.js +1 -89
- package/dist/components/exams/ExamPartTabs.d.ts +1 -1
- package/dist/components/exams/ExamPartTabs.js +1 -40
- package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
- package/dist/components/exams/ExamPreviewDialog.js +1 -0
- package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
- package/dist/components/exams/ExamQuestionGrid.js +1 -18
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
- package/dist/components/exams/ExamViewDialog.d.ts +24 -0
- package/dist/components/exams/ExamViewDialog.js +1 -0
- package/dist/components/exams/PartAudioUploader.js +1 -9
- package/dist/components/exams/TemplateSelector.js +1 -66
- package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/exams/UnsavedChangesDialog.js +1 -0
- package/dist/components/exams/api.d.ts +1 -1
- package/dist/components/exams/api.js +1 -3
- package/dist/components/exams/create-exam.texts.d.ts +58 -0
- package/dist/components/exams/create-exam.texts.js +1 -0
- package/dist/components/exams/create-exam.validation.js +1 -18
- package/dist/components/exams/exam-question-status.d.ts +9 -0
- package/dist/components/exams/exam-question-status.js +1 -0
- package/dist/components/exams/exam-template.utils.js +1 -18
- package/dist/components/exams/index.d.ts +11 -1
- package/dist/components/exams/index.js +1 -10
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
- package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
- package/dist/components/exams/take/exam-taking.utils.js +1 -88
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
- package/dist/components/exams/take/index.js +1 -2
- package/dist/components/exams/take/types.js +1 -1
- package/dist/components/index.js +1 -42
- package/dist/components/questions/_shared/config/question-types.config.js +1 -333
- package/dist/components/questions/_shared/constants/color-palette.js +1 -21
- package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
- package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
- package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
- package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
- package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
- package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
- package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
- package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
- package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
- package/dist/components/questions/_shared/index.js +1 -7
- 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 +1 -3
- package/dist/components/questions/_shared/types/color-region.config.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.js +1 -12
- 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 +1 -3
- 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 +1 -3
- 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.js +1 -1
- package/dist/components/questions/_shared/types/question-group.type.js +1 -11
- 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.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.js +1 -1
- 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 +1 -3
- 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 +1 -79
- package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
- package/dist/components/questions/_shared/utils/id-generator.js +1 -3
- package/dist/components/questions/_shared/utils/question-validation.js +1 -21
- package/dist/components/questions/components/ColorNode.js +1 -85
- package/dist/components/questions/components/ColorRegionClient.js +1 -545
- package/dist/components/questions/components/ColorRegionCreator.js +1 -420
- package/dist/components/questions/components/ColorRegionNode.js +1 -59
- package/dist/components/questions/components/CreatorGuide.js +1 -103
- package/dist/components/questions/components/NameNode.js +1 -26
- package/dist/components/questions/components/QuestionBankOption.js +1 -46
- package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
- package/dist/components/questions/components/QuestionCommonFields.js +1 -33
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
- package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
- package/dist/components/questions/components/RegionNode.js +1 -94
- package/dist/components/questions/components/index.js +1 -14
- package/dist/components/questions/creator/QuestionCreator.js +1 -90
- package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
- package/dist/components/questions/creator/dedicated-component-router.js +1 -79
- package/dist/components/questions/creator/index.js +1 -3
- package/dist/components/questions/creator/question-type-registry.js +1 -80
- package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
- package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
- package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
- package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
- package/dist/components/questions/index.js +1 -10
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
- package/dist/components/questions/types/answer-the-question/register.js +1 -67
- package/dist/components/questions/types/answer-the-question/transform.js +1 -36
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
- package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
- package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
- package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
- package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
- package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
- package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
- package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
- package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
- package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
- package/dist/components/questions/types/fill-in-blank/index.js +1 -2
- package/dist/components/questions/types/fill-in-blank/register.js +1 -138
- package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
- package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
- package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
- package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
- package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
- package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
- package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
- package/dist/components/questions/types/image-object-matching/index.js +1 -2
- package/dist/components/questions/types/image-object-matching/register.js +1 -3
- package/dist/components/questions/types/image-object-matching/transform.js +1 -2
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
- package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
- package/dist/components/questions/types/label-the-picture/index.js +1 -2
- package/dist/components/questions/types/label-the-picture/register.js +1 -54
- package/dist/components/questions/types/label-the-picture/transform.js +1 -28
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
- package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
- package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
- package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
- package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
- package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
- package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
- package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
- package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
- package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
- package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
- package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
- package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
- package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
- package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
- package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
- package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
- 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 +1 -66
- package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
- package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
- package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
- package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
- package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
- package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
- package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
- package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
- package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
- package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
- package/dist/components/questions/types/read-displayed-content/register.js +1 -64
- package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
- package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
- package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
- package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
- package/dist/components/questions/types/speaking-conversation/register.js +1 -110
- package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
- package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
- package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
- package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
- package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
- package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
- package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
- package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
- package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
- package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
- package/dist/components/questions/types/true-false/index.js +1 -2
- package/dist/components/questions/types/true-false/register.js +1 -77
- package/dist/components/questions/types/true-false/transform.js +1 -32
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
- package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
- package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
- package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
- package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
- package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
- package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
- package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
- package/dist/components/questions/types/word-ordering/index.js +1 -2
- package/dist/components/questions/types/word-ordering/register.js +1 -46
- package/dist/components/questions/types/word-ordering/transform.js +1 -24
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
- package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
- package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
- package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
- package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
- package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
- package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
- package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
- package/dist/components/questions/types/write-sentences/index.js +1 -2
- package/dist/components/questions/types/write-sentences/register.js +1 -58
- package/dist/components/questions/types/write-sentences/transform.js +1 -28
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
- package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
- package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
- package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
- package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
- package/dist/components/questions/viewer/QuestionViewer.js +1 -446
- package/dist/components/questions/viewer/index.js +1 -2
- package/dist/components/results/ResultReviewPageContainer.js +1 -98
- package/dist/components/results/ReviewQuestionRenderer.js +1 -72
- package/dist/components/results/index.js +1 -2
- package/dist/components/results/result-review.utils.js +1 -6
- package/dist/components/results/review-data.js +1 -150
- package/dist/components/results/review-renderer.utils.js +1 -97
- package/dist/components/results/review-types.js +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
- package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
- package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
- package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
- package/dist/components/themes/cambridge-yle/index.js +1 -13
- package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
- package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
- package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
- package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
- package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
- package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
- package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
- package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
- package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
- package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
- package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
- package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
- package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
- package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
- package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
- package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
- package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
- package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
- package/dist/components/themes/english-certification/index.js +1 -21
- package/dist/components/themes/index.js +1 -6
- package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
- package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
- package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
- package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
- package/dist/components/themes/types.js +1 -1
- package/dist/components/ui/alert-dialog.js +1 -26
- package/dist/components/ui/alert.js +1 -22
- package/dist/components/ui/avatar.js +1 -12
- package/dist/components/ui/badge.js +1 -38
- package/dist/components/ui/button.js +1 -35
- package/dist/components/ui/calendar.js +1 -76
- package/dist/components/ui/card.js +1 -16
- package/dist/components/ui/carousel.js +1 -91
- package/dist/components/ui/checkbox.js +1 -9
- package/dist/components/ui/collapsible.js +1 -13
- package/dist/components/ui/command.js +1 -34
- package/dist/components/ui/confirm-dialog.js +1 -49
- package/dist/components/ui/dialog.js +1 -23
- package/dist/components/ui/drawer.js +1 -35
- package/dist/components/ui/dropdown-menu.js +1 -33
- package/dist/components/ui/file-upload.js +1 -155
- package/dist/components/ui/form.js +1 -62
- package/dist/components/ui/hover-card.js +1 -10
- package/dist/components/ui/image-preview.js +1 -29
- package/dist/components/ui/input.js +1 -8
- package/dist/components/ui/label.js +1 -9
- package/dist/components/ui/language-switcher.js +1 -15
- package/dist/components/ui/multi-select.js +1 -90
- package/dist/components/ui/points-input.js +1 -33
- package/dist/components/ui/popover.js +1 -26
- package/dist/components/ui/progress.js +1 -16
- package/dist/components/ui/radio-group.js +1 -12
- package/dist/components/ui/scroll-area.js +1 -11
- package/dist/components/ui/select.js +1 -38
- package/dist/components/ui/separator.js +1 -8
- package/dist/components/ui/sheet.js +1 -40
- package/dist/components/ui/single-select.js +1 -76
- package/dist/components/ui/skeleton.js +1 -6
- package/dist/components/ui/switch.js +1 -8
- package/dist/components/ui/table.js +1 -20
- package/dist/components/ui/tabs.js +1 -17
- package/dist/components/ui/textarea.js +1 -8
- package/dist/components/ui/tooltip.js +1 -11
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -19
- package/dist/mcp/config.js +1 -32
- package/dist/mcp/http-client.js +1 -48
- package/dist/mcp/index.js +1 -29
- package/dist/mcp/tools/_helpers.js +1 -25
- package/dist/mcp/tools/exams.js +1 -439
- package/dist/mcp/tools/index.js +1 -6
- package/dist/mcp/tools/results.js +1 -19
- package/dist/shared/constants/ApiConstant.js +1 -276
- package/dist/shared/constants/QueryKeyConstant.js +1 -112
- package/dist/shared/constants/exam-level.enum.js +1 -41
- package/dist/shared/constants/landings/placement-test.constants.js +1 -431
- package/dist/shared/constants/question-skills.js +1 -267
- package/dist/shared/constants/routes.js +1 -81
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +1 -7
- package/dist/shared/lib/hooks/index.js +1 -10
- package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
- package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
- package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
- package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
- package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
- package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
- package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
- package/dist/shared/lib/hooks/useFileUpload.js +1 -24
- package/dist/shared/lib/hooks/useImageUrl.js +1 -5
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
- package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
- package/dist/shared/lib/hooks/useToast.js +1 -62
- package/dist/shared/lib/i18n/index.js +1 -54
- package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/en/common.js +1 -140
- package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/en/student.js +1 -133
- package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/en.js +1 -15
- package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
- package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
- package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/vi.js +1 -15
- package/dist/shared/lib/i18n/types.js +1 -1
- package/dist/shared/lib/stores/examQuestionStore.js +1 -212
- package/dist/shared/lib/stores/localeStore.js +1 -10
- package/dist/shared/lib/utils/public-page.js +1 -44
- package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
- package/dist/shared/lib/utils/question-transform-validation.js +1 -0
- package/dist/shared/lib/utils/question-transform.d.ts +6 -2
- package/dist/shared/lib/utils/question-transform.js +1 -174
- package/dist/shared/lib/utils.js +1 -168
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
- package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
- package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
- package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
- package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
- package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
- package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
- package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
- package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
- package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
- package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
- package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
- package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
- package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
- package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
- package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
- package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
- package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
- package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
- package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
- package/dist/shared/types/branch.types.js +1 -6
- package/dist/shared/types/common.types.js +1 -50
- package/dist/shared/types/entities/exam-schedule.types.js +1 -5
- package/dist/shared/types/entities/exam.types.js +1 -1
- package/dist/shared/types/entities/question.types.js +1 -4
- package/dist/shared/types/entities/result.types.js +1 -4
- package/dist/shared/types/entities/user.types.js +1 -4
- package/dist/shared/types/exam-taking.types.js +1 -2
- package/dist/shared/types/history.types.js +1 -34
- package/dist/shared/types/index.js +1 -1
- package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
- package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
- package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
- package/dist/shared/types/questions/index.js +1 -40
- 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 +1 -3
- package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
- 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 +1 -3
- package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
- package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
- package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
- package/dist/shared/types/questions/read-displayed-content.js +1 -1
- package/dist/shared/types/questions/speaking-conversation.js +1 -7
- 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/write-correct-verb-form.js +1 -12
- package/dist/shared/types/role.types.js +1 -1
- package/dist/shared/types/structure-management.js +1 -28
- package/dist/shared/types/student.types.js +1 -54
- package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
- package/package.json +12 -3
|
@@ -1,156 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useEffect, useMemo, useCallback, memo } from 'react';
|
|
4
|
-
import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
5
|
-
import { cn } from '../../../shared/lib/utils';
|
|
6
|
-
import { transformWordFillStructuredForm } from '../../../components/exams/take/exam-taking.utils';
|
|
7
|
-
/**
|
|
8
|
-
* Replace `{key}` placeholders in raw HTML with inline `<input>` elements so the
|
|
9
|
-
* full HTML structure (tables, forms, …) is preserved and inputs render inside
|
|
10
|
-
* the correct cells — never breaking onto new lines or squeezing the layout.
|
|
11
|
-
*/
|
|
12
|
-
/** Convert a 0-based blank key into the 1-based number shown to the student. */
|
|
13
|
-
function toDisplayNumber(key) {
|
|
14
|
-
const numeric = Number(key);
|
|
15
|
-
return Number.isFinite(numeric) ? String(numeric + 1) : key;
|
|
16
|
-
}
|
|
17
|
-
function buildHtmlWithInputPlaceholders(html) {
|
|
18
|
-
return html.replace(/\{([\w\d]+)\}/g, (_match, key) => {
|
|
19
|
-
return `
|
|
20
|
-
<span class="ec-blank-wrapper" data-blank-wrapper="${key}">
|
|
21
|
-
<span class="ec-blank-number" data-blank-number="${key}">${toDisplayNumber(key)}</span>
|
|
22
|
-
<input type="text" data-blank-key="${key}" class="ec-blank-input" />
|
|
23
|
-
</span>
|
|
24
|
-
`;
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
function normalizeStructuredAnswer(value, caseSensitive) {
|
|
28
|
-
const normalized = value.trim();
|
|
29
|
-
return caseSensitive ? normalized : normalized.toLowerCase();
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Build the card label from the blank keys (which are the IELTS question numbers).
|
|
33
|
-
* Returns "Questions x–y" for a numeric range, a single "Question n", or falls
|
|
34
|
-
* back to `Question {fallback}` when keys are non-numeric/empty.
|
|
35
|
-
*/
|
|
36
|
-
function formatBlankRange(answers, fallback) {
|
|
37
|
-
const numericKeys = Object.keys(answers)
|
|
38
|
-
.map((key) => Number(key))
|
|
39
|
-
.filter((n) => Number.isFinite(n))
|
|
40
|
-
.sort((a, b) => a - b);
|
|
41
|
-
if (numericKeys.length === 0) {
|
|
42
|
-
return `Question ${fallback}`;
|
|
43
|
-
}
|
|
44
|
-
// Keys are 0-based; display numbers are 1-based.
|
|
45
|
-
const min = numericKeys[0] + 1;
|
|
46
|
-
const max = numericKeys[numericKeys.length - 1] + 1;
|
|
47
|
-
return min === max ? `Question ${min}` : `Questions ${min}–${max}`;
|
|
48
|
-
}
|
|
49
|
-
/** Stable empty object so React.memo children are not broken by new `{}`. */
|
|
50
|
-
const EMPTY_OBJECT = {};
|
|
51
|
-
/**
|
|
52
|
-
* Renders one structured-form question's HTML with inline branded inputs.
|
|
53
|
-
*
|
|
54
|
-
* CRITICAL: We set `innerHTML` imperatively via a ref exactly ONCE on mount and
|
|
55
|
-
* attach native input listeners, bypassing React reconciliation. Using
|
|
56
|
-
* `dangerouslySetInnerHTML` would re-apply HTML on every render, destroying the
|
|
57
|
-
* DOM inputs and losing focus while the student types.
|
|
58
|
-
*/
|
|
59
|
-
const StructuredFormBody = memo(function StructuredFormBody({ question, initialAnswers, onBlankChange, isReviewMode, }) {
|
|
60
|
-
const containerRef = useRef(null);
|
|
61
|
-
const onBlankChangeRef = useRef(onBlankChange);
|
|
62
|
-
onBlankChangeRef.current = onBlankChange;
|
|
63
|
-
const mountedRef = useRef(false);
|
|
64
|
-
const initialAnswersRef = useRef(initialAnswers);
|
|
65
|
-
const processedHtml = useMemo(() => buildHtmlWithInputPlaceholders(question.content), [question.content]);
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
const container = containerRef.current;
|
|
68
|
-
if (!container)
|
|
69
|
-
return;
|
|
70
|
-
if (!mountedRef.current) {
|
|
71
|
-
container.innerHTML = processedHtml;
|
|
72
|
-
mountedRef.current = true;
|
|
73
|
-
}
|
|
74
|
-
const inputs = container.querySelectorAll('input.ec-blank-input');
|
|
75
|
-
const handlers = [];
|
|
76
|
-
inputs.forEach((input) => {
|
|
77
|
-
const key = input.getAttribute('data-blank-key') || '';
|
|
78
|
-
const wrapper = container.querySelector(`[data-blank-wrapper="${key}"]`);
|
|
79
|
-
const userAnswer = initialAnswersRef.current[key] || '';
|
|
80
|
-
const correctAnswer = question.answers[key] || '';
|
|
81
|
-
const hasUserAnswer = userAnswer.trim().length > 0;
|
|
82
|
-
const isCorrect = hasUserAnswer &&
|
|
83
|
-
normalizeStructuredAnswer(userAnswer, question.caseSensitive) ===
|
|
84
|
-
normalizeStructuredAnswer(correctAnswer, question.caseSensitive);
|
|
85
|
-
const shouldShowReviewText = isReviewMode && !isCorrect && correctAnswer.trim().length > 0;
|
|
86
|
-
input.value = shouldShowReviewText
|
|
87
|
-
? `${hasUserAnswer ? userAnswer : '(empty)'} → ${correctAnswer}`
|
|
88
|
-
: userAnswer;
|
|
89
|
-
input.disabled = isReviewMode;
|
|
90
|
-
input.className = cn('ec-blank-input inline-block w-[110px] border-0 border-b-2 border-dashed bg-transparent px-1 py-0 text-base font-semibold leading-tight align-middle outline-none transition-colors', isReviewMode
|
|
91
|
-
? isCorrect
|
|
92
|
-
? 'border-[#22c55e] text-[#15803d]'
|
|
93
|
-
: hasUserAnswer
|
|
94
|
-
? 'border-[#ef4444] text-red-700'
|
|
95
|
-
: 'border-[#f97316] text-[#c2410c]'
|
|
96
|
-
: 'border-[#1167e8] text-[#0f1730] hover:border-[#0b57d0] focus:border-[#0b57d0]');
|
|
97
|
-
const numberBadge = container.querySelector(`[data-blank-number="${key}"]`);
|
|
98
|
-
if (numberBadge) {
|
|
99
|
-
// Always set 1-based display number here so it is correct even if
|
|
100
|
-
// the cached innerHTML was built with a different formatter.
|
|
101
|
-
numberBadge.textContent = toDisplayNumber(key);
|
|
102
|
-
numberBadge.className = cn('ec-blank-number mr-1 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full px-1 text-[11px] font-bold leading-none text-white align-middle', isReviewMode
|
|
103
|
-
? isCorrect
|
|
104
|
-
? 'bg-[#22c55e]'
|
|
105
|
-
: hasUserAnswer
|
|
106
|
-
? 'bg-[#ef4444]'
|
|
107
|
-
: 'bg-[#f97316]'
|
|
108
|
-
: 'bg-[#0b57d0]');
|
|
109
|
-
}
|
|
110
|
-
if (wrapper) {
|
|
111
|
-
wrapper.className = cn('ec-blank-wrapper inline-flex items-center whitespace-nowrap align-middle', isReviewMode ? 'mx-1' : 'mx-0.5');
|
|
112
|
-
}
|
|
113
|
-
const handler = (e) => {
|
|
114
|
-
const target = e.target;
|
|
115
|
-
onBlankChangeRef.current(key, target.value);
|
|
116
|
-
};
|
|
117
|
-
input.addEventListener('input', handler);
|
|
118
|
-
handlers.push({ el: input, handler });
|
|
119
|
-
});
|
|
120
|
-
return () => {
|
|
121
|
-
handlers.forEach(({ el, handler }) => {
|
|
122
|
-
el.removeEventListener('input', handler);
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
126
|
-
}, [processedHtml, isReviewMode]);
|
|
127
|
-
return (_jsxs("div", { className: "space-y-3", children: [question.title && (_jsx("h3", { className: "text-base font-semibold text-[#0f1730]", children: question.title })), question.imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "overflow-hidden rounded-lg border border-[#d9e4f2]", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: question.title || 'Question image', className: "max-h-56 w-auto object-contain" }) }) })), _jsx("div", { ref: containerRef, className: "ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold" }), isReviewMode && Object.keys(question.answers).length > 0 && (_jsxs("div", { className: "rounded-xl border border-[#d9e4f2] bg-white/70 p-3", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Correct answers" }), _jsx("div", { className: "mt-1 flex flex-wrap gap-1.5", children: Object.entries(question.answers).map(([key, val]) => (_jsxs("span", { className: "rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]", children: [key, ": ", val] }, key))) })] }))] }));
|
|
128
|
-
});
|
|
129
|
-
/**
|
|
130
|
-
* Branded English Certification renderer for `WORD_FILL_STRUCTURED_FORM`.
|
|
131
|
-
*
|
|
132
|
-
* Replaces the shared Movers two-column grid (which squeezed IELTS tables into
|
|
133
|
-
* narrow columns) with a full-width branded layout. Reuses
|
|
134
|
-
* {@link transformWordFillStructuredForm} so the answer shape
|
|
135
|
-
* (`{ [questionId]: { [blankKey]: value } }`) stays compatible with the autosave
|
|
136
|
-
* and grading store.
|
|
137
|
-
*/
|
|
138
|
-
export function EcWordFillStructuredForm({ part, answers, onAnswerChange, isReviewMode = false, }) {
|
|
139
|
-
const { questions, instruction, groupTitle, groupContent, groupImageUrl } = useMemo(() => transformWordFillStructuredForm(part.questions), [part.questions]);
|
|
140
|
-
const answersRef = useRef(answers);
|
|
141
|
-
answersRef.current = answers;
|
|
142
|
-
const onAnswerChangeRef = useRef(onAnswerChange);
|
|
143
|
-
onAnswerChangeRef.current = onAnswerChange;
|
|
144
|
-
const handleBlankChange = useCallback((questionId, key, value) => {
|
|
145
|
-
const currentAnswer = answersRef.current[questionId] || {};
|
|
146
|
-
onAnswerChangeRef.current(questionId, { ...currentAnswer, [key]: value });
|
|
147
|
-
}, []);
|
|
148
|
-
const blankChangeCallbacks = useMemo(() => {
|
|
149
|
-
const map = {};
|
|
150
|
-
questions.forEach((q) => {
|
|
151
|
-
map[q.id] = (key, value) => handleBlankChange(q.id, key, value);
|
|
152
|
-
});
|
|
153
|
-
return map;
|
|
154
|
-
}, [questions, handleBlankChange]);
|
|
155
|
-
return (_jsxs("div", { className: "flex flex-col gap-4", children: [instruction && (_jsx("div", { className: "rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]", children: instruction })), groupTitle && (_jsx("h2", { className: "text-center text-lg font-bold uppercase text-[#0b57d0]", children: groupTitle })), groupImageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: groupImageUrl, alt: groupTitle || 'Group image', className: "max-h-[300px] rounded-lg object-contain shadow-md" }) })), _jsx("div", { className: "flex flex-col gap-4", children: questions.map((q) => (_jsxs("div", { id: `question-${q.id}`, className: "rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)] backdrop-blur-md", children: [_jsx("div", { className: "mb-2 flex items-center gap-2", children: _jsx("span", { className: "inline-flex items-center rounded-full bg-gradient-to-b from-[#1f7cff] to-[#0b57d0] px-3 py-1 text-xs font-bold text-white shadow-sm", children: formatBlankRange(q.answers, q.questionNumber) }) }), _jsx(StructuredFormBody, { question: q, initialAnswers: answers[q.id] || EMPTY_OBJECT, onBlankChange: blankChangeCallbacks[q.id], isReviewMode: isReviewMode })] }, q.id))) })] }));
|
|
156
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useRef as r,useEffect as n,useMemo as s,useCallback as a,memo as i}from"react";import{ResolvedImage as o}from"../../../components/common/ResolvedImage";import{cn as l}from"../../../shared/lib/utils";import{transformWordFillStructuredForm as d}from"../../../components/exams/take/exam-taking.utils";function c(e){const t=Number(e);return Number.isFinite(t)?String(t+1):e}function b(e,t){const r=e.trim();return t?r:r.toLowerCase()}function u(e,t){const r=Object.keys(e).map(e=>Number(e)).filter(e=>Number.isFinite(e)).sort((e,t)=>e-t);if(0===r.length)return`Question ${t}`;const n=r[0]+1,s=r[r.length-1]+1;return n===s?`Question ${n}`:`Questions ${n}–${s}`}const m={},p=i(function({question:a,initialAnswers:i,onBlankChange:d,isReviewMode:u}){const m=r(null),p=r(d);p.current=d;const f=r(!1),x=r(i),h=s(()=>a.content.replace(/\{([\w\d]+)\}/g,(e,t)=>`\n <span class="ec-blank-wrapper" data-blank-wrapper="${t}">\n <span class="ec-blank-number" data-blank-number="${t}">${c(t)}</span>\n <input type="text" data-blank-key="${t}" class="ec-blank-input" />\n </span>\n `),[a.content]);return n(()=>{const e=m.current;if(!e)return;f.current||(e.innerHTML=h,f.current=!0);const t=e.querySelectorAll("input.ec-blank-input"),r=[];return t.forEach(t=>{const n=t.getAttribute("data-blank-key")||"",s=e.querySelector(`[data-blank-wrapper="${n}"]`),i=x.current[n]||"",o=a.answers[n]||"",d=i.trim().length>0,m=d&&b(i,a.caseSensitive)===b(o,a.caseSensitive),f=u&&!m&&o.trim().length>0;t.value=f?`${d?i:"(empty)"} → ${o}`:i,t.disabled=u,t.className=l("ec-blank-input inline-block w-[110px] border-0 border-b-2 border-dashed bg-transparent px-1 py-0 text-base font-semibold leading-tight align-middle outline-none transition-colors",u?m?"border-[#22c55e] text-[#15803d]":d?"border-[#ef4444] text-red-700":"border-[#f97316] text-[#c2410c]":"border-[#1167e8] text-[#0f1730] hover:border-[#0b57d0] focus:border-[#0b57d0]");const h=e.querySelector(`[data-blank-number="${n}"]`);h&&(h.textContent=c(n),h.className=l("ec-blank-number mr-1 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full px-1 text-[11px] font-bold leading-none text-white align-middle",u?m?"bg-[#22c55e]":d?"bg-[#ef4444]":"bg-[#f97316]":"bg-[#0b57d0]")),s&&(s.className=l("ec-blank-wrapper inline-flex items-center whitespace-nowrap align-middle",u?"mx-1":"mx-0.5"));const g=e=>{const t=e.target;p.current(n,t.value)};t.addEventListener("input",g),r.push({el:t,handler:g})}),()=>{r.forEach(({el:e,handler:t})=>{e.removeEventListener("input",t)})}},[h,u]),t("div",{className:"space-y-3",children:[a.title&&e("h3",{className:"text-base font-semibold text-[#0f1730]",children:a.title}),a.imageUrl&&e("div",{className:"flex justify-center",children:e("div",{className:"overflow-hidden rounded-lg border border-[#d9e4f2]",children:e(o,{src:a.imageUrl,alt:a.title||"Question image",className:"max-h-56 w-auto object-contain"})})}),e("div",{ref:m,className:"ec-structured-content w-full overflow-x-auto rounded-xl border border-[#d9e4f2] bg-[#f4f9ff] p-5 text-base leading-relaxed text-[#0f1730] [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#d9e4f2] [&_td]:px-4 [&_td]:py-3 [&_td]:align-top [&_th]:border [&_th]:border-[#d9e4f2] [&_th]:bg-[#e9f3ff] [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold"}),u&&Object.keys(a.answers).length>0&&t("div",{className:"rounded-xl border border-[#d9e4f2] bg-white/70 p-3",children:[e("span",{className:"text-[10px] font-semibold uppercase tracking-wider text-[#64748b]",children:"Correct answers"}),e("div",{className:"mt-1 flex flex-wrap gap-1.5",children:Object.entries(a.answers).map(([e,r])=>t("span",{className:"rounded border border-[#b9edc8] bg-[#ddf8e7] px-2 py-0.5 text-xs font-semibold text-[#15803d]",children:[e,": ",r]},e))})]})]})});export function EcWordFillStructuredForm({part:n,answers:i,onAnswerChange:l,isReviewMode:c=!1}){const{questions:b,instruction:f,groupTitle:x,groupContent:h,groupImageUrl:g}=s(()=>d(n.questions),[n.questions]),w=r(i);w.current=i;const N=r(l);N.current=l;const v=a((e,t,r)=>{const n=w.current[e]||{};N.current(e,{...n,[t]:r})},[]),k=s(()=>{const e={};return b.forEach(t=>{e[t.id]=(e,r)=>v(t.id,e,r)}),e},[b,v]);return t("div",{className:"flex flex-col gap-4",children:[f&&e("div",{className:"rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]",children:f}),x&&e("h2",{className:"text-center text-lg font-bold uppercase text-[#0b57d0]",children:x}),g&&e("div",{className:"flex items-center justify-center",children:e(o,{src:g,alt:x||"Group image",className:"max-h-[300px] rounded-lg object-contain shadow-md"})}),e("div",{className:"flex flex-col gap-4",children:b.map(r=>t("div",{id:`question-${r.id}`,className:"rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)] backdrop-blur-md",children:[e("div",{className:"mb-2 flex items-center gap-2",children:e("span",{className:"inline-flex items-center rounded-full bg-gradient-to-b from-[#1f7cff] to-[#0b57d0] px-3 py-1 text-xs font-bold text-white shadow-sm",children:u(r.answers,r.questionNumber)})}),e(p,{question:r,initialAnswers:i[r.id]||m,onBlankChange:k[r.id],isReviewMode:c})]},r.id))})]})}
|
|
@@ -1,34 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
|
-
import { ResolvedImage } from '../../../components/common/ResolvedImage';
|
|
5
|
-
import { cn } from '../../../shared/lib/utils';
|
|
6
|
-
import { transformWriteShortLetter } from '../../../components/exams/take/exam-taking.utils';
|
|
7
|
-
/** Count words in a free-text answer (collapses whitespace). */
|
|
8
|
-
function countWords(text) {
|
|
9
|
-
const trimmed = text.trim();
|
|
10
|
-
if (!trimmed)
|
|
11
|
-
return 0;
|
|
12
|
-
return trimmed.split(/\s+/).length;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Branded English Certification renderer for `WRITE_A_SHORT_LETTER`.
|
|
16
|
-
*
|
|
17
|
-
* Shows the prompt, optional requirements/guide images and a large branded
|
|
18
|
-
* textarea with a live word counter. The stored answer is a plain string at
|
|
19
|
-
* `answers[questionId]`, matching the shared writing renderer so the grading
|
|
20
|
-
* store stays compatible.
|
|
21
|
-
*/
|
|
22
|
-
export function EcWritingTask({ part, answers, onAnswerChange, isReviewMode = false, }) {
|
|
23
|
-
const firstQuestion = part.questions[0];
|
|
24
|
-
const data = useMemo(() => transformWriteShortLetter(firstQuestion), [firstQuestion]);
|
|
25
|
-
if (!firstQuestion) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
const { prompt, requirements, minWords, maxWords, guideImages } = data;
|
|
29
|
-
const answer = answers[firstQuestion.id] || '';
|
|
30
|
-
const wordCount = countWords(answer);
|
|
31
|
-
const meetsMin = minWords ? wordCount >= minWords : true;
|
|
32
|
-
const withinMax = maxWords ? wordCount <= maxWords : true;
|
|
33
|
-
return (_jsxs("div", { className: "flex flex-col gap-4", children: [part.instructions && (_jsx("div", { className: "rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]", children: part.instructions })), prompt && (_jsx("div", { className: "whitespace-pre-line rounded-2xl border border-[#d9e4f2] bg-white/90 p-5 text-base leading-relaxed text-[#0f1730] shadow-[0_14px_35px_rgba(13,71,161,0.10)]", children: prompt })), requirements && requirements.length > 0 && (_jsxs("div", { className: "rounded-2xl border border-[#d9e4f2] bg-white/70 p-4", children: [_jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-[#64748b]", children: "Y\u00EAu c\u1EA7u" }), _jsx("ul", { className: "mt-2 list-inside list-disc space-y-1 text-sm text-[#0f1730]", children: requirements.map((req, idx) => (_jsx("li", { children: req }, idx))) })] })), guideImages && guideImages.length > 0 && (_jsx("div", { className: "flex flex-col items-center justify-center gap-4 py-4", children: guideImages.map((img, idx) => (_jsx(ResolvedImage, { src: img, alt: `Guide ${idx + 1}`, className: "w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm" }, idx))) })), _jsxs("div", { className: "rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)]", children: [_jsx("textarea", { value: answer, disabled: isReviewMode, placeholder: "Vi\u1EBFt b\u00E0i c\u1EE7a b\u1EA1n \u1EDF \u0111\u00E2y\u2026", onChange: (e) => onAnswerChange(firstQuestion.id, e.target.value), rows: 12, className: cn('w-full resize-y rounded-xl border-2 border-[#9cc4f5] bg-[#f4f9ff] px-4 py-3 text-sm leading-relaxed text-[#0f1730] outline-none transition-all focus:border-[#1167e8]', isReviewMode && 'cursor-not-allowed opacity-80') }), _jsxs("div", { className: "mt-2 flex items-center justify-between text-xs", children: [_jsxs("span", { className: cn('font-semibold', !meetsMin || !withinMax ? 'text-[#ef4444]' : 'text-[#15803d]'), children: [wordCount, " t\u1EEB"] }), (minWords || maxWords) && (_jsxs("span", { className: "text-[#64748b]", children: [minWords ? `Tối thiểu ${minWords}` : '', minWords && maxWords ? ' · ' : '', maxWords ? `Tối đa ${maxWords}` : ''] }))] })] })] }));
|
|
34
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useMemo as r}from"react";import{ResolvedImage as s}from"../../../components/common/ResolvedImage";import{cn as i}from"../../../shared/lib/utils";import{transformWriteShortLetter as a}from"../../../components/exams/take/exam-taking.utils";export function EcWritingTask({part:n,answers:l,onAnswerChange:d,isReviewMode:o=!1}){const c=n.questions[0],m=r(()=>a(c),[c]);if(!c)return null;const{prompt:x,requirements:p,minWords:f,maxWords:u,guideImages:b}=m,h=l[c.id]||"",g=function(e){const t=e.trim();return t?t.split(/\s+/).length:0}(h),w=!f||g>=f,N=!u||g<=u;return t("div",{className:"flex flex-col gap-4",children:[n.instructions&&e("div",{className:"rounded-xl border border-[#d9e4f2] bg-[#e9f3ff] px-4 py-2.5 text-sm font-semibold uppercase tracking-wide text-[#0b57d0]",children:n.instructions}),x&&e("div",{className:"whitespace-pre-line rounded-2xl border border-[#d9e4f2] bg-white/90 p-5 text-base leading-relaxed text-[#0f1730] shadow-[0_14px_35px_rgba(13,71,161,0.10)]",children:x}),p&&p.length>0&&t("div",{className:"rounded-2xl border border-[#d9e4f2] bg-white/70 p-4",children:[e("span",{className:"text-[10px] font-semibold uppercase tracking-wider text-[#64748b]",children:"Yêu cầu"}),e("ul",{className:"mt-2 list-inside list-disc space-y-1 text-sm text-[#0f1730]",children:p.map((t,r)=>e("li",{children:t},r))})]}),b&&b.length>0&&e("div",{className:"flex flex-col items-center justify-center gap-4 py-4",children:b.map((t,r)=>e(s,{src:t,alt:`Guide ${r+1}`,className:"w-full max-w-3xl rounded-xl bg-white object-contain shadow-sm"},r))}),t("div",{className:"rounded-2xl border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_14px_35px_rgba(13,71,161,0.10)]",children:[e("textarea",{value:h,disabled:o,placeholder:"Viết bài của bạn ở đây…",onChange:e=>d(c.id,e.target.value),rows:12,className:i("w-full resize-y rounded-xl border-2 border-[#9cc4f5] bg-[#f4f9ff] px-4 py-3 text-sm leading-relaxed text-[#0f1730] outline-none transition-all focus:border-[#1167e8]",o&&"cursor-not-allowed opacity-80")}),t("div",{className:"mt-2 flex items-center justify-between text-xs",children:[t("span",{className:i("font-semibold",w&&N?"text-[#15803d]":"text-[#ef4444]"),children:[g," từ"]}),(f||u)&&t("span",{className:"text-[#64748b]",children:[f?`Tối thiểu ${f}`:"",f&&u?" · ":"",u?`Tối đa ${u}`:""]})]})]})]})}
|
|
@@ -1,114 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useState, useEffect } from 'react';
|
|
4
|
-
import { Play, Pause, RotateCcw, RefreshCw } from 'lucide-react';
|
|
5
|
-
import { usePresignedFileUrl } from '../../../shared/lib/hooks/usePresignedFileUrl';
|
|
6
|
-
import { cn } from '../../../shared/lib/utils';
|
|
7
|
-
/**
|
|
8
|
-
* Branded Ocean Edu audio bar for English Certification Listening sections.
|
|
9
|
-
*
|
|
10
|
-
* Handles play/pause/reset, progress and presigned-URL resolution while
|
|
11
|
-
* adopting the compact Ocean Edu audio-bar look.
|
|
12
|
-
*/
|
|
13
|
-
export function EnglishCertificationAudioPlayer({ audioUrl, partNumber = 0, isLoading = false, }) {
|
|
14
|
-
const audioRef = useRef(null);
|
|
15
|
-
// Resolve S3 storage keys to presigned URLs (HTTP URLs pass through unchanged)
|
|
16
|
-
const { previewUrl: resolvedAudioUrl } = usePresignedFileUrl(audioUrl || undefined);
|
|
17
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
18
|
-
const [currentTime, setCurrentTime] = useState(0);
|
|
19
|
-
const [duration, setDuration] = useState(0);
|
|
20
|
-
const [error, setError] = useState(null);
|
|
21
|
-
const togglePlayPause = () => {
|
|
22
|
-
if (!audioRef.current)
|
|
23
|
-
return;
|
|
24
|
-
if (isPlaying) {
|
|
25
|
-
audioRef.current.pause();
|
|
26
|
-
setIsPlaying(false);
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
audioRef.current.play().catch((err) => {
|
|
30
|
-
setError(`Failed to play audio: ${err.message}`);
|
|
31
|
-
});
|
|
32
|
-
setIsPlaying(true);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
const handleReset = () => {
|
|
36
|
-
if (!audioRef.current)
|
|
37
|
-
return;
|
|
38
|
-
audioRef.current.currentTime = 0;
|
|
39
|
-
audioRef.current.pause();
|
|
40
|
-
setIsPlaying(false);
|
|
41
|
-
setCurrentTime(0);
|
|
42
|
-
};
|
|
43
|
-
const handleRetry = () => {
|
|
44
|
-
const audio = audioRef.current;
|
|
45
|
-
if (!audio || !resolvedAudioUrl)
|
|
46
|
-
return;
|
|
47
|
-
setError(null);
|
|
48
|
-
setCurrentTime(0);
|
|
49
|
-
setIsPlaying(false);
|
|
50
|
-
audio.src = resolvedAudioUrl;
|
|
51
|
-
audio.load();
|
|
52
|
-
};
|
|
53
|
-
const handleProgressChange = (e) => {
|
|
54
|
-
const newTime = parseFloat(e.target.value);
|
|
55
|
-
if (!audioRef.current)
|
|
56
|
-
return;
|
|
57
|
-
audioRef.current.currentTime = newTime;
|
|
58
|
-
setCurrentTime(newTime);
|
|
59
|
-
};
|
|
60
|
-
const formatTime = (seconds) => {
|
|
61
|
-
if (!seconds || isNaN(seconds))
|
|
62
|
-
return '00:00';
|
|
63
|
-
const mins = Math.floor(seconds / 60);
|
|
64
|
-
const secs = Math.floor(seconds % 60);
|
|
65
|
-
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
66
|
-
};
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
const audio = audioRef.current;
|
|
69
|
-
if (!audio)
|
|
70
|
-
return;
|
|
71
|
-
const handleTimeUpdate = () => setCurrentTime(audio.currentTime);
|
|
72
|
-
const handleLoadedMetadata = () => {
|
|
73
|
-
setDuration(audio.duration);
|
|
74
|
-
setError(null);
|
|
75
|
-
};
|
|
76
|
-
const handleEnded = () => setIsPlaying(false);
|
|
77
|
-
const handleError = () => {
|
|
78
|
-
setError('Failed to load audio file');
|
|
79
|
-
setIsPlaying(false);
|
|
80
|
-
};
|
|
81
|
-
audio.addEventListener('timeupdate', handleTimeUpdate);
|
|
82
|
-
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
83
|
-
audio.addEventListener('ended', handleEnded);
|
|
84
|
-
audio.addEventListener('error', handleError);
|
|
85
|
-
return () => {
|
|
86
|
-
audio.removeEventListener('timeupdate', handleTimeUpdate);
|
|
87
|
-
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
88
|
-
audio.removeEventListener('ended', handleEnded);
|
|
89
|
-
audio.removeEventListener('error', handleError);
|
|
90
|
-
};
|
|
91
|
-
}, []);
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (audioRef.current && resolvedAudioUrl) {
|
|
94
|
-
audioRef.current.src = resolvedAudioUrl;
|
|
95
|
-
setCurrentTime(0);
|
|
96
|
-
setIsPlaying(false);
|
|
97
|
-
setError(null);
|
|
98
|
-
}
|
|
99
|
-
}, [resolvedAudioUrl]);
|
|
100
|
-
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
101
|
-
const statusLabel = error
|
|
102
|
-
? 'Audio unavailable'
|
|
103
|
-
: isLoading
|
|
104
|
-
? 'Loading...'
|
|
105
|
-
: isPlaying
|
|
106
|
-
? 'Playing...'
|
|
107
|
-
: currentTime > 0
|
|
108
|
-
? 'Paused'
|
|
109
|
-
: 'Ready to play';
|
|
110
|
-
return (_jsxs("div", { className: "grid w-full grid-cols-[64px_1fr] items-center gap-4 rounded-[10px] border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] sm:gap-[18px] sm:px-5", children: [_jsx("audio", { ref: audioRef }), _jsx("button", { type: "button", onClick: togglePlayPause, disabled: isLoading || !audioUrl || !!error, className: "grid h-14 w-14 place-items-center rounded-full text-2xl text-white shadow-[0_12px_22px_rgba(11,87,208,0.22)] transition-transform hover:scale-[1.03] disabled:cursor-not-allowed disabled:opacity-50", style: { background: 'linear-gradient(180deg, #1f7cff, #0b57d0)' }, "aria-label": isPlaying ? 'Pause audio' : 'Play audio', children: isPlaying ? _jsx(Pause, { size: 24 }) : _jsx(Play, { size: 24, className: "ml-0.5" }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-[18px] font-black text-[#0f1730]", children: ["Listening Audio", partNumber > 0 && ` · Part ${partNumber}`] }), _jsx("button", { type: "button", onClick: handleReset, disabled: isLoading || !audioUrl || !!error, className: "flex h-8 w-8 items-center justify-center rounded-full border border-[#d9e4f2] text-[#0b57d0] transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Replay from start", children: _jsx(RotateCcw, { size: 16 }) })] }), _jsxs("div", { className: "mt-1 flex items-center gap-2.5", children: [_jsx("span", { className: cn('text-sm font-bold', error ? 'text-red-500' : 'text-[#64748b]'), children: error ?? statusLabel }), error && (_jsxs("button", { type: "button", onClick: handleRetry, disabled: !audioUrl, className: "inline-flex items-center gap-1.5 rounded-full border border-red-200 bg-red-50 px-3 py-1 text-xs font-bold text-red-600 transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Retry loading audio", children: [_jsx(RefreshCw, { size: 13 }), "Retry"] }))] }), _jsxs("div", { className: "mt-2 grid grid-cols-[46px_1fr_46px] items-center gap-2.5 text-sm font-bold tabular-nums text-[#0f1730]", children: [_jsx("span", { children: formatTime(currentTime) }), _jsxs("div", { className: "relative", children: [_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-0 rounded-full", style: {
|
|
111
|
-
width: `${progressPercent}%`,
|
|
112
|
-
background: 'linear-gradient(90deg, #1f7cff, #0b57d0)',
|
|
113
|
-
} }), _jsx("input", { type: "range", min: "0", max: duration || 0, step: "0.1", value: currentTime, onChange: handleProgressChange, disabled: isLoading || !audioUrl || !!error, className: "relative z-10 h-2 w-full cursor-pointer appearance-none rounded-full bg-[#dfe9f6] accent-[#0b57d0] disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Audio progress" })] }), _jsx("span", { children: duration > 0 ? formatTime(duration) : '00:00' })] })] })] }));
|
|
114
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useRef as t,useState as a,useEffect as n}from"react";import{Play as i,Pause as d,RotateCcw as l,RefreshCw as s}from"lucide-react";import{usePresignedFileUrl as o}from"../../../shared/lib/hooks/usePresignedFileUrl";import{cn as c}from"../../../shared/lib/utils";export function EnglishCertificationAudioPlayer({audioUrl:u,partNumber:p=0,isLoading:m=!1}){const b=t(null),{previewUrl:f}=o(u||void 0),[g,x]=a(!1),[h,v]=a(0),[y,N]=a(0),[w,L]=a(null),E=e=>{if(!e||isNaN(e))return"00:00";const r=Math.floor(e/60),t=Math.floor(e%60);return`${r.toString().padStart(2,"0")}:${t.toString().padStart(2,"0")}`};n(()=>{const e=b.current;if(!e)return;const r=()=>v(e.currentTime),t=()=>{N(e.duration),L(null)},a=()=>x(!1),n=()=>{L("Failed to load audio file"),x(!1)};return e.addEventListener("timeupdate",r),e.addEventListener("loadedmetadata",t),e.addEventListener("ended",a),e.addEventListener("error",n),()=>{e.removeEventListener("timeupdate",r),e.removeEventListener("loadedmetadata",t),e.removeEventListener("ended",a),e.removeEventListener("error",n)}},[]),n(()=>{b.current&&f&&(b.current.src=f,v(0),x(!1),L(null))},[f]);const _=y>0?h/y*100:0,k=w?"Audio unavailable":m?"Loading...":g?"Playing...":h>0?"Paused":"Ready to play";return r("div",{className:"grid w-full grid-cols-[64px_1fr] items-center gap-4 rounded-[10px] border border-[#d9e4f2] bg-white/90 p-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] sm:gap-[18px] sm:px-5",children:[e("audio",{ref:b}),e("button",{type:"button",onClick:()=>{b.current&&(g?(b.current.pause(),x(!1)):(b.current.play().catch(e=>{L(`Failed to play audio: ${e.message}`)}),x(!0)))},disabled:m||!u||!!w,className:"grid h-14 w-14 place-items-center rounded-full text-2xl text-white shadow-[0_12px_22px_rgba(11,87,208,0.22)] transition-transform hover:scale-[1.03] disabled:cursor-not-allowed disabled:opacity-50",style:{background:"linear-gradient(180deg, #1f7cff, #0b57d0)"},"aria-label":g?"Pause audio":"Play audio",children:g?e(d,{size:24}):e(i,{size:24,className:"ml-0.5"})}),r("div",{className:"min-w-0",children:[r("div",{className:"flex items-center justify-between gap-3",children:[r("span",{className:"text-[18px] font-black text-[#0f1730]",children:["Listening Audio",p>0&&` · Part ${p}`]}),e("button",{type:"button",onClick:()=>{b.current&&(b.current.currentTime=0,b.current.pause(),x(!1),v(0))},disabled:m||!u||!!w,className:"flex h-8 w-8 items-center justify-center rounded-full border border-[#d9e4f2] text-[#0b57d0] transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50","aria-label":"Replay from start",children:e(l,{size:16})})]}),r("div",{className:"mt-1 flex items-center gap-2.5",children:[e("span",{className:c("text-sm font-bold",w?"text-red-500":"text-[#64748b]"),children:w??k}),w&&r("button",{type:"button",onClick:()=>{const e=b.current;e&&f&&(L(null),v(0),x(!1),e.src=f,e.load())},disabled:!u,className:"inline-flex items-center gap-1.5 rounded-full border border-red-200 bg-red-50 px-3 py-1 text-xs font-bold text-red-600 transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50","aria-label":"Retry loading audio",children:[e(s,{size:13}),"Retry"]})]}),r("div",{className:"mt-2 grid grid-cols-[46px_1fr_46px] items-center gap-2.5 text-sm font-bold tabular-nums text-[#0f1730]",children:[e("span",{children:E(h)}),r("div",{className:"relative",children:[e("div",{className:"pointer-events-none absolute inset-y-0 left-0 rounded-full",style:{width:`${_}%`,background:"linear-gradient(90deg, #1f7cff, #0b57d0)"}}),e("input",{type:"range",min:"0",max:y||0,step:"0.1",value:h,onChange:e=>{const r=parseFloat(e.target.value);b.current&&(b.current.currentTime=r,v(r))},disabled:m||!u||!!w,className:"relative z-10 h-2 w-full cursor-pointer appearance-none rounded-full bg-[#dfe9f6] accent-[#0b57d0] disabled:cursor-not-allowed disabled:opacity-50","aria-label":"Audio progress"})]}),e("span",{children:y>0?E(y):"00:00"})]})]})]})}
|
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import Image from 'next/image';
|
|
4
|
-
import { cn } from '../../../shared/lib/utils';
|
|
5
|
-
/**
|
|
6
|
-
* Ocean Edu brand lockup: horizontal logo image.
|
|
7
|
-
* Used in the English Certification header.
|
|
8
|
-
*/
|
|
9
|
-
export function EnglishCertificationBrand({ className }) {
|
|
10
|
-
return (_jsx("div", { className: cn('flex items-center', className), "aria-label": "Ocean Edu", children: _jsx(Image, { src: "/images/logo-ngang.png", alt: "Ocean Edu", width: 180, height: 40, priority: true, className: "h-10 w-auto object-contain" }) }));
|
|
11
|
-
}
|
|
1
|
+
"use client";import{jsx as e}from"react/jsx-runtime";import i from"next/image";import{cn as a}from"../../../shared/lib/utils";export function EnglishCertificationBrand({className:t}){return e("div",{className:a("flex items-center",t),"aria-label":"Ocean Edu",children:e(i,{src:"/images/logo-ngang.png",alt:"Ocean Edu",width:180,height:40,priority:!0,className:"h-10 w-auto object-contain"})})}
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { List } from 'lucide-react';
|
|
5
|
-
import { Sheet, SheetContent, SheetHeader, SheetTitle, } from '../../../components/ui/sheet';
|
|
6
|
-
import { EnglishCertificationHeader, } from './EnglishCertificationHeader';
|
|
7
|
-
import { EnglishCertificationNavigationPanel } from './EnglishCertificationNavigationPanel';
|
|
8
|
-
import { EnglishCertificationFooterBar, } from './EnglishCertificationFooterBar';
|
|
9
|
-
import { ENGLISH_CERT_APP_BACKGROUND, ENGLISH_CERT_PRIMARY_GRADIENT, } from './english-certification-theme';
|
|
10
|
-
/**
|
|
11
|
-
* Ocean Edu branded exam-taking shell for English Certification exams.
|
|
12
|
-
*
|
|
13
|
-
* Layout: branded sticky header · main content + collapsible "Answer Sheet"
|
|
14
|
-
* sidebar · branded wave footer with navigation actions. On mobile the sidebar
|
|
15
|
-
* is replaced by a bottom-sheet opened via a floating button.
|
|
16
|
-
*/
|
|
17
|
-
export function EnglishCertificationExamLayout({ children, headerProps, navigationProps, footerProps, }) {
|
|
18
|
-
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
|
19
|
-
const [isMobileSheetOpen, setIsMobileSheetOpen] = useState(false);
|
|
20
|
-
return (_jsxs("div", { className: "flex h-screen flex-col overflow-hidden text-[#0f1730]", style: { background: ENGLISH_CERT_APP_BACKGROUND }, children: [_jsx(EnglishCertificationHeader, { ...headerProps }), _jsxs("div", { className: "flex flex-1 gap-4 overflow-hidden px-4 py-4 lg:px-6", children: [_jsx("main", { id: "exam-main-scroll-area", className: "min-w-0 flex-1 overflow-y-auto", children: children }), isSidebarOpen && (_jsxs("aside", { className: "hidden w-[360px] shrink-0 flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md lg:flex", "aria-label": "Answer sheet navigation", children: [_jsxs("div", { className: "flex h-14 items-center justify-between px-5 text-white", style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, children: [_jsx("h3", { className: "text-[18px] font-black tracking-[0.1em]", children: "ANSWER SHEET" }), _jsx("button", { type: "button", onClick: () => setIsSidebarOpen(false), className: "text-sm font-bold text-white/90 hover:text-white", children: "Hide \u226A" })] }), _jsx("div", { className: "flex-1 overflow-hidden", children: _jsx(EnglishCertificationNavigationPanel, { ...navigationProps }) })] })), !isSidebarOpen && (_jsx("button", { type: "button", onClick: () => setIsSidebarOpen(true), className: "fixed right-0 top-24 z-30 hidden rounded-l-lg border border-r-0 border-[#d9e4f2] bg-white px-2 py-3 text-sm font-bold text-[#0b57d0] shadow-sm hover:bg-blue-50 lg:block", "aria-label": "Show answer sheet", children: "\u226B" }))] }), footerProps && _jsx(EnglishCertificationFooterBar, { ...footerProps }), _jsx("button", { type: "button", onClick: () => setIsMobileSheetOpen(true), className: "fixed bottom-24 right-4 z-30 flex h-12 w-12 items-center justify-center rounded-full text-white shadow-lg lg:hidden", style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, "aria-label": "Open answer sheet", children: _jsx(List, { className: "h-5 w-5" }) }), _jsx(Sheet, { open: isMobileSheetOpen, onOpenChange: setIsMobileSheetOpen, children: _jsxs(SheetContent, { side: "bottom", className: "h-[78vh] p-0", children: [_jsx(SheetHeader, { className: "flex h-14 flex-row items-center px-5 text-white", style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, children: _jsx(SheetTitle, { className: "text-[18px] font-black tracking-[0.1em] text-white", children: "ANSWER SHEET" }) }), _jsx("div", { className: "h-[calc(78vh-3.5rem)] overflow-hidden", children: _jsx(EnglishCertificationNavigationPanel, { ...navigationProps }) })] }) })] }));
|
|
21
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useState as i}from"react";import{List as r}from"lucide-react";import{Sheet as o,SheetContent as l,SheetHeader as n,SheetTitle as a}from"../../../components/ui/sheet";import{EnglishCertificationHeader as s}from"./EnglishCertificationHeader";import{EnglishCertificationNavigationPanel as d}from"./EnglishCertificationNavigationPanel";import{EnglishCertificationFooterBar as c}from"./EnglishCertificationFooterBar";import{ENGLISH_CERT_APP_BACKGROUND as h,ENGLISH_CERT_PRIMARY_GRADIENT as m}from"./english-certification-theme";export function EnglishCertificationExamLayout({children:f,headerProps:x,navigationProps:p,footerProps:b}){const[g,w]=i(!0),[u,v]=i(!1);return t("div",{className:"flex h-screen flex-col overflow-hidden text-[#0f1730]",style:{background:h},children:[e(s,{...x}),t("div",{className:"flex flex-1 gap-4 overflow-hidden px-4 py-4 lg:px-6",children:[e("main",{id:"exam-main-scroll-area",className:"min-w-0 flex-1 overflow-y-auto",children:f}),g&&t("aside",{className:"hidden w-[360px] shrink-0 flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md lg:flex","aria-label":"Answer sheet navigation",children:[t("div",{className:"flex h-14 items-center justify-between px-5 text-white",style:{background:m},children:[e("h3",{className:"text-[18px] font-black tracking-[0.1em]",children:"ANSWER SHEET"}),e("button",{type:"button",onClick:()=>w(!1),className:"text-sm font-bold text-white/90 hover:text-white",children:"Hide ≪"})]}),e("div",{className:"flex-1 overflow-hidden",children:e(d,{...p})})]}),!g&&e("button",{type:"button",onClick:()=>w(!0),className:"fixed right-0 top-24 z-30 hidden rounded-l-lg border border-r-0 border-[#d9e4f2] bg-white px-2 py-3 text-sm font-bold text-[#0b57d0] shadow-sm hover:bg-blue-50 lg:block","aria-label":"Show answer sheet",children:"≫"})]}),b&&e(c,{...b}),e("button",{type:"button",onClick:()=>v(!0),className:"fixed bottom-24 right-4 z-30 flex h-12 w-12 items-center justify-center rounded-full text-white shadow-lg lg:hidden",style:{background:m},"aria-label":"Open answer sheet",children:e(r,{className:"h-5 w-5"})}),e(o,{open:u,onOpenChange:v,children:t(l,{side:"bottom",className:"h-[78vh] p-0",children:[e(n,{className:"flex h-14 flex-row items-center px-5 text-white",style:{background:m},children:e(a,{className:"text-[18px] font-black tracking-[0.1em] text-white",children:"ANSWER SHEET"})}),e("div",{className:"h-[calc(78vh-3.5rem)] overflow-hidden",children:e(d,{...p})})]})})]})}
|
|
@@ -1,15 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { cn } from '../../../shared/lib/utils';
|
|
4
|
-
/**
|
|
5
|
-
* Shared Ocean Edu "Mark for Review" control.
|
|
6
|
-
*
|
|
7
|
-
* Single source of truth for the mark/flag affordance so the bookmark flag
|
|
8
|
-
* looks identical on single-question and grouped-reading screens.
|
|
9
|
-
*/
|
|
10
|
-
export function EnglishCertificationFlagButton({ isFlagged, onToggle, size = 'default', className, }) {
|
|
11
|
-
const isCompact = size === 'sm';
|
|
12
|
-
return (_jsxs("button", { type: "button", onClick: onToggle, className: cn('inline-flex items-center gap-1.5 rounded-md border font-bold transition-colors', isCompact ? 'px-2 py-1 text-xs' : 'px-3 py-1.5 text-sm', isFlagged
|
|
13
|
-
? 'border-[#ff8a00] bg-[#fff1db] text-[#ff8a00]'
|
|
14
|
-
: 'border-[#d9e4f2] bg-white text-[#64748b] hover:text-[#ff8a00]', className), "aria-label": isFlagged ? 'Unmark for review' : 'Mark for review', "aria-pressed": isFlagged, children: [_jsx("span", { className: "h-[14px] w-2 shrink-0 bg-current", style: { clipPath: 'polygon(0 0, 100% 0, 100% 100%, 50% 70%, 0 100%)' }, "aria-hidden": "true" }), !isCompact && _jsx("span", { children: isFlagged ? 'Marked' : 'Mark' })] }));
|
|
15
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{cn as t}from"../../../shared/lib/utils";export function EnglishCertificationFlagButton({isFlagged:a,onToggle:s,size:i="default",className:n}){const o="sm"===i;return r("button",{type:"button",onClick:s,className:t("inline-flex items-center gap-1.5 rounded-md border font-bold transition-colors",o?"px-2 py-1 text-xs":"px-3 py-1.5 text-sm",a?"border-[#ff8a00] bg-[#fff1db] text-[#ff8a00]":"border-[#d9e4f2] bg-white text-[#64748b] hover:text-[#ff8a00]",n),"aria-label":a?"Unmark for review":"Mark for review","aria-pressed":a,children:[e("span",{className:"h-[14px] w-2 shrink-0 bg-current",style:{clipPath:"polygon(0 0, 100% 0, 100% 100%, 50% 70%, 0 100%)"},"aria-hidden":"true"}),!o&&e("span",{children:a?"Marked":"Mark"})]})}
|
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { cn } from '../../../shared/lib/utils';
|
|
4
|
-
import { ENGLISH_CERT_FOOTER_NOTE, ENGLISH_CERT_FOOTER_WAVE, ENGLISH_CERT_PRIMARY_GRADIENT, } from './english-certification-theme';
|
|
5
|
-
/**
|
|
6
|
-
* Branded Ocean Edu footer action bar for English Certification exam taking.
|
|
7
|
-
*
|
|
8
|
-
* Provides Previous / Clear Answer / Mark for Review / Next actions plus the
|
|
9
|
-
* decorative wave, brand lockup and encouragement note.
|
|
10
|
-
*/
|
|
11
|
-
export function EnglishCertificationFooterBar({ onPrevious, onClear, onToggleMark, onNext, isMarked = false, isPreviousDisabled = false, isNextDisabled = false, showSecondaryActions = true, }) {
|
|
12
|
-
return (_jsxs("footer", { className: "sticky bottom-0 z-40 flex flex-col gap-2 px-4 pt-3 lg:px-7", style: {
|
|
13
|
-
background: `linear-gradient(180deg, rgba(255,255,255,0.94) 0 calc(100% - 64px), transparent calc(100% - 64px)), ${ENGLISH_CERT_FOOTER_WAVE} bottom/100% 64px no-repeat`,
|
|
14
|
-
}, children: [_jsxs("div", { className: cn('grid grid-cols-2 items-center gap-3 lg:gap-12', showSecondaryActions
|
|
15
|
-
? 'lg:grid-cols-[240px_190px_1fr_200px]'
|
|
16
|
-
: 'mx-auto w-full max-w-[520px] lg:grid-cols-2'), children: [_jsx("button", { type: "button", onClick: onPrevious, disabled: isPreviousDisabled, className: cn('h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50', !showSecondaryActions && 'lg:w-[200px] lg:justify-self-start'), children: "\u2190 \u00A0 Previous" }), showSecondaryActions && onClear && (_jsx("button", { type: "button", onClick: onClear, className: "h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)]", children: "\u25C6 \u00A0 Clear Answer" })), showSecondaryActions && onToggleMark && (_jsxs("button", { type: "button", onClick: onToggleMark, className: cn('h-[42px] rounded-md border bg-white text-[15px] font-black shadow-[0_5px_16px_rgba(13,71,161,0.08)] lg:w-[220px] lg:justify-self-center', isMarked
|
|
17
|
-
? 'border-[#ff8a00] text-[#ff8a00]'
|
|
18
|
-
: 'border-[#d9e4f2] text-[#0a3d9a]'), children: ["\u25AE \u00A0 ", isMarked ? 'Unmark Review' : 'Mark for Review'] })), _jsx("button", { type: "button", onClick: onNext, disabled: isNextDisabled, className: cn('h-[42px] rounded-md border border-[#0b57d0] text-[15px] font-black text-white shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50', !showSecondaryActions && 'lg:w-[200px] lg:justify-self-end'), style: { background: ENGLISH_CERT_PRIMARY_GRADIENT }, children: "Next \u00A0 \u2192" })] }), _jsxs("div", { className: "pointer-events-none hidden h-9 items-center gap-4 text-white lg:flex", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-2 font-black tracking-wide", children: [_jsx("span", { className: "grid h-[25px] w-[25px] place-items-center rounded-full border-2 border-white text-xs", children: "\u2726" }), "OCEAN EDU"] }), _jsxs("span", { className: "min-w-0 flex-1 text-center text-[13px] font-bold", children: ["\u3030 \u00A0 ", ENGLISH_CERT_FOOTER_NOTE] })] })] }));
|
|
19
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{cn as r}from"../../../shared/lib/utils";import{ENGLISH_CERT_FOOTER_NOTE as a,ENGLISH_CERT_FOOTER_WAVE as o,ENGLISH_CERT_PRIMARY_GRADIENT as l}from"./english-certification-theme";export function EnglishCertificationFooterBar({onPrevious:d,onClear:i,onToggleMark:s,onNext:n,isMarked:c=!1,isPreviousDisabled:x=!1,isNextDisabled:p=!1,showSecondaryActions:b=!0}){return t("footer",{className:"sticky bottom-0 z-40 flex flex-col gap-2 px-4 pt-3 lg:px-7",style:{background:`linear-gradient(180deg, rgba(255,255,255,0.94) 0 calc(100% - 64px), transparent calc(100% - 64px)), ${o} bottom/100% 64px no-repeat`},children:[t("div",{className:r("grid grid-cols-2 items-center gap-3 lg:gap-12",b?"lg:grid-cols-[240px_190px_1fr_200px]":"mx-auto w-full max-w-[520px] lg:grid-cols-2"),children:[e("button",{type:"button",onClick:d,disabled:x,className:r("h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50",!b&&"lg:w-[200px] lg:justify-self-start"),children:"← Previous"}),b&&i&&e("button",{type:"button",onClick:i,className:"h-[42px] rounded-md border border-[#d9e4f2] bg-white text-[15px] font-black text-[#0a3d9a] shadow-[0_5px_16px_rgba(13,71,161,0.08)]",children:"◆ Clear Answer"}),b&&s&&t("button",{type:"button",onClick:s,className:r("h-[42px] rounded-md border bg-white text-[15px] font-black shadow-[0_5px_16px_rgba(13,71,161,0.08)] lg:w-[220px] lg:justify-self-center",c?"border-[#ff8a00] text-[#ff8a00]":"border-[#d9e4f2] text-[#0a3d9a]"),children:["▮ ",c?"Unmark Review":"Mark for Review"]}),e("button",{type:"button",onClick:n,disabled:p,className:r("h-[42px] rounded-md border border-[#0b57d0] text-[15px] font-black text-white shadow-[0_5px_16px_rgba(13,71,161,0.08)] disabled:cursor-not-allowed disabled:opacity-50",!b&&"lg:w-[200px] lg:justify-self-end"),style:{background:l},children:"Next →"})]}),t("div",{className:"pointer-events-none hidden h-9 items-center gap-4 text-white lg:flex",children:[t("div",{className:"flex shrink-0 items-center gap-2 font-black tracking-wide",children:[e("span",{className:"grid h-[25px] w-[25px] place-items-center rounded-full border-2 border-white text-xs",children:"✦"}),"OCEAN EDU"]}),t("span",{className:"min-w-0 flex-1 text-center text-[13px] font-bold",children:["〰 ",a]})]})]})}
|
|
@@ -1,148 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { FileText, Headphones, Info } from 'lucide-react';
|
|
4
|
-
import { EnglishCertificationQuestionCard } from './EnglishCertificationQuestionCard';
|
|
5
|
-
import { EnglishCertificationPassagePanel } from './EnglishCertificationPassagePanel';
|
|
6
|
-
import { EnglishCertificationReadingSection } from './EnglishCertificationReadingSection';
|
|
7
|
-
import { EnglishCertificationAudioPlayer } from './EnglishCertificationAudioPlayer';
|
|
8
|
-
import { ENGLISH_CERT_CONTENT_WIDTH_CLASS } from './english-certification-theme';
|
|
9
|
-
import { isHtmlContent, stripInstruction } from './grouped-question-utils';
|
|
10
|
-
/** Fixed split layout: equal-width passage / questions columns. */
|
|
11
|
-
const SPLIT_VIEW_GRID = 'minmax(0, 1fr) minmax(0, 1fr)';
|
|
12
|
-
/**
|
|
13
|
-
* Splits a grouped reading part into its individual document groups while
|
|
14
|
-
* preserving the original question order. Each document keeps its own passage
|
|
15
|
-
* payload so the layout can stack them vertically on a single screen.
|
|
16
|
-
*/
|
|
17
|
-
function buildDocumentGroups(part, sharedInstruction) {
|
|
18
|
-
const order = [];
|
|
19
|
-
const byGroup = new Map();
|
|
20
|
-
part.questions.forEach((question) => {
|
|
21
|
-
const groupId = question.group_id ?? question.id;
|
|
22
|
-
if (!byGroup.has(groupId)) {
|
|
23
|
-
byGroup.set(groupId, []);
|
|
24
|
-
order.push(groupId);
|
|
25
|
-
}
|
|
26
|
-
byGroup.get(groupId).push(question);
|
|
27
|
-
});
|
|
28
|
-
return order.map((groupId) => {
|
|
29
|
-
const questions = byGroup.get(groupId);
|
|
30
|
-
const payload = questions[0]?.questionGroup?.payload ?? {};
|
|
31
|
-
const rawPassage = payload.content ?? payload.passage ?? '';
|
|
32
|
-
const passage = stripInstruction(rawPassage, sharedInstruction);
|
|
33
|
-
const passageImageUrl = payload.imageUrl ?? payload.passageImageUrl ?? '';
|
|
34
|
-
return {
|
|
35
|
-
groupId,
|
|
36
|
-
questions,
|
|
37
|
-
passage,
|
|
38
|
-
// API exposes the group heading as `title`; older Reading payloads use
|
|
39
|
-
// `passageTitle`. Fall back to `title` so grouped MCQ headings show.
|
|
40
|
-
passageTitle: payload.passageTitle ?? payload.title ?? '',
|
|
41
|
-
passageSubtitle: payload.subtitle ?? '',
|
|
42
|
-
passageImageUrl,
|
|
43
|
-
hasPassageContent: Boolean(passage.trim()) || Boolean(passageImageUrl),
|
|
44
|
-
};
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Resolves the shared part instruction.
|
|
49
|
-
*
|
|
50
|
-
* Priority:
|
|
51
|
-
* 1. `questionGroup.payload.instructions` — used by some grouped question types.
|
|
52
|
-
* 2. `partInstructions` — the template-level instruction from the QuestionPart
|
|
53
|
-
* (e.g. Part 6 TOEIC where the instruction lives on the template part).
|
|
54
|
-
* 3. `content.question` of the first question — legacy fallback.
|
|
55
|
-
*/
|
|
56
|
-
function resolveSharedInstruction(part, partInstructions) {
|
|
57
|
-
const first = part.questions[0];
|
|
58
|
-
return (first?.questionGroup?.payload?.instructions ??
|
|
59
|
-
(partInstructions?.trim() || undefined) ??
|
|
60
|
-
first?.content?.question ??
|
|
61
|
-
'').trim();
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Ocean Edu branded split-view renderer for grouped reading parts.
|
|
65
|
-
*
|
|
66
|
-
* Renders every document of the part stacked vertically. Each document shows a
|
|
67
|
-
* reading passage (title, optional image, body, source bar) on the left and its
|
|
68
|
-
* MCQ questions on the right in a balanced 50/50 split.
|
|
69
|
-
*/
|
|
70
|
-
export function EnglishCertificationGroupedQuestion({ part, partConfig, answers, flaggedQuestions, onAnswerChange, onToggleFlag, isReviewMode = false, isListeningPart = false, partAudioUrl, }) {
|
|
71
|
-
const startIndex = partConfig.startIndex ?? 0;
|
|
72
|
-
// Part-level instruction (e.g. "Read the texts that follow…") is shared by
|
|
73
|
-
// every document. Render it once at the top and strip it from each passage
|
|
74
|
-
// (the API prepends it to every document's content) to avoid duplication.
|
|
75
|
-
// Prefer part.instructions (set by groupQuestionsByPart from the template)
|
|
76
|
-
// which covers TOEIC Part 6 where the instruction lives on the template part
|
|
77
|
-
// rather than inside the question group payload.
|
|
78
|
-
const sharedInstruction = resolveSharedInstruction(part, part.instructions ?? partConfig.instructions);
|
|
79
|
-
// When the part carries template sections, render every question in a single
|
|
80
|
-
// document so the section loop in renderQuestions runs once and stacks each
|
|
81
|
-
// sub-section. Splitting by group_id here would duplicate the section headings
|
|
82
|
-
// and break per-section dispatch.
|
|
83
|
-
//
|
|
84
|
-
// The document still carries the shared passage when present: IELTS Reading
|
|
85
|
-
// parts ship a DOCUMENT group whose passage lives on each question's
|
|
86
|
-
// `questionGroup.payload` (content/title/subtitle). Surfacing it flips
|
|
87
|
-
// `hasPassageContent` to true so the split branch shows passage-left /
|
|
88
|
-
// questions-right (TOEIC Part 7 style). Mixed Listening parts (SECTION 2:
|
|
89
|
-
// CHOOSE + MATCH + WORD_FILL) carry no passage, so this stays false and the
|
|
90
|
-
// full-width no-passage branch is preserved.
|
|
91
|
-
const buildSectionedDocument = () => {
|
|
92
|
-
const payload = (part.questions[0]?.questionGroup?.payload ?? {});
|
|
93
|
-
const rawPassage = (payload.content ?? payload.passage ?? '');
|
|
94
|
-
const passage = stripInstruction(rawPassage, sharedInstruction);
|
|
95
|
-
const passageImageUrl = (payload.imageUrl ?? payload.passageImageUrl ?? '');
|
|
96
|
-
return {
|
|
97
|
-
groupId: part.partId,
|
|
98
|
-
questions: part.questions,
|
|
99
|
-
passage,
|
|
100
|
-
passageTitle: (payload.passageTitle ?? payload.title ?? ''),
|
|
101
|
-
passageSubtitle: (payload.subtitle ?? ''),
|
|
102
|
-
passageImageUrl,
|
|
103
|
-
hasPassageContent: Boolean(passage.trim()) || Boolean(passageImageUrl),
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
const documents = part.sections?.length
|
|
107
|
-
? [buildSectionedDocument()]
|
|
108
|
-
: buildDocumentGroups(part, sharedInstruction);
|
|
109
|
-
if (!documents.length) {
|
|
110
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center rounded-xl border border-red-200 bg-red-50 p-8 text-red-600", children: "No questions found for this part" }));
|
|
111
|
-
}
|
|
112
|
-
const showAudio = isListeningPart && Boolean(partAudioUrl);
|
|
113
|
-
/** Renders a document's question list (used by both split and full-width layouts). */
|
|
114
|
-
const renderQuestions = (document) => {
|
|
115
|
-
if (part.sections?.length) {
|
|
116
|
-
return (_jsx("div", { className: "flex-1 space-y-6 px-4 py-4", children: part.sections.map((section, sectionIdx) => {
|
|
117
|
-
const sectionQuestions = document.questions.filter((question) => {
|
|
118
|
-
const pos = part.questions.indexOf(question);
|
|
119
|
-
return (pos >= section.startIndex - startIndex &&
|
|
120
|
-
pos <= section.endIndex - startIndex);
|
|
121
|
-
});
|
|
122
|
-
if (!sectionQuestions.length)
|
|
123
|
-
return null;
|
|
124
|
-
return (_jsx(EnglishCertificationReadingSection, { part: part, section: section, questions: sectionQuestions, startIndex: startIndex, answers: answers, flaggedQuestions: flaggedQuestions, onAnswerChange: onAnswerChange, onToggleFlag: onToggleFlag, isReviewMode: isReviewMode }, `${document.groupId}-section-${sectionIdx}`));
|
|
125
|
-
}) }));
|
|
126
|
-
}
|
|
127
|
-
return (_jsx("div", { className: "flex-1 space-y-3 px-4 py-4", children: document.questions.map((question) => {
|
|
128
|
-
const questionNumber = part.questions.indexOf(question) + 1;
|
|
129
|
-
const globalQuestionNumber = startIndex + questionNumber;
|
|
130
|
-
return (_jsx(EnglishCertificationQuestionCard, { question: question, globalQuestionNumber: globalQuestionNumber, answer: answers[question.id], isFlagged: flaggedQuestions.has(question.id), sharedInstruction: sharedInstruction, onAnswerChange: onAnswerChange, onToggleFlag: onToggleFlag, isReviewMode: isReviewMode }, question.id));
|
|
131
|
-
}) }));
|
|
132
|
-
};
|
|
133
|
-
return (_jsxs("div", { className: "flex h-full flex-col gap-6 overflow-y-auto pb-4", children: [showAudio && (_jsx("div", { className: `${ENGLISH_CERT_CONTENT_WIDTH_CLASS} w-full self-center`, children: _jsx(EnglishCertificationAudioPlayer, { audioUrl: partAudioUrl, partNumber: part.partNo }) })), sharedInstruction && (_jsx("div", { className: `${ENGLISH_CERT_CONTENT_WIDTH_CLASS} w-full self-center`, children: _jsxs("div", { className: "flex items-start gap-3 rounded-xl border border-[#bfd8f8] bg-gradient-to-r from-[#eef5ff] to-[#f7fbff] px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md", children: [_jsx("span", { className: "mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full bg-[#1167e8] text-white", children: isListeningPart ? (_jsx(Headphones, { className: "h-3.5 w-3.5" })) : (_jsx(Info, { className: "h-3.5 w-3.5" })) }), _jsxs("div", { className: "min-w-0", children: [part.name && (_jsx("div", { className: "mb-1 text-xs font-black uppercase tracking-wide text-[#0b57d0]", children: part.name })), _jsx("p", { className: "text-base font-semibold leading-relaxed text-[#0f1730]", children: sharedInstruction })] })] }) })), documents.map((document) => {
|
|
134
|
-
const firstNumber = part.questions.indexOf(document.questions[0]) + startIndex + 1;
|
|
135
|
-
const lastNumber = part.questions.indexOf(document.questions[document.questions.length - 1]) +
|
|
136
|
-
startIndex +
|
|
137
|
-
1;
|
|
138
|
-
const passageIsHtml = isHtmlContent(document.passage);
|
|
139
|
-
// No-passage group (e.g. Listening grouped MCQ that only carries a
|
|
140
|
-
// heading like "Choose the correct letter, A, B or C"): hide the empty
|
|
141
|
-
// passage panel and render the group title as a banner above the
|
|
142
|
-
// full-width questions.
|
|
143
|
-
if (!document.hasPassageContent) {
|
|
144
|
-
return (_jsxs("div", { className: `${ENGLISH_CERT_CONTENT_WIDTH_CLASS} flex w-full flex-col gap-3 self-center`, children: [_jsxs("div", { className: "flex items-center gap-4 rounded-xl border border-[#d9e4f2] bg-white/90 px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md", children: [_jsx("span", { className: "grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-[#eaf2ff] text-[#1167e8]", children: _jsx(FileText, { className: "h-6 w-6" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-md border border-[#bfd8f8] bg-[#eaf2ff] px-2.5 py-1 text-xs font-bold text-[#0b57d0]", children: ["Questions ", firstNumber, "\u2013", lastNumber] }), document.passageTitle && (_jsx("p", { className: "mt-2 text-lg font-bold leading-tight text-[#0f1730]", children: document.passageTitle })), document.passageSubtitle && (_jsx("p", { className: "mt-1 text-[15px] font-medium text-[#475569]", children: document.passageSubtitle }))] }), document.passageImageUrl && (_jsx("img", { src: document.passageImageUrl, alt: document.passageTitle || 'Illustration', className: "h-20 w-32 shrink-0 rounded-lg object-contain" }))] }), _jsx("section", { className: "flex flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md", children: renderQuestions(document) })] }, document.groupId));
|
|
145
|
-
}
|
|
146
|
-
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "flex flex-wrap items-center gap-3 rounded-xl border border-[#d9e4f2] bg-white/85 px-4 py-3 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md", children: _jsxs("div", { className: "flex items-center gap-2.5 rounded-md border border-[#bfd8f8] bg-white/70 px-3 py-1.5 text-base font-bold text-[#0b57d0]", children: [_jsx("span", { className: "grid h-[18px] w-[18px] place-items-center rounded-full bg-[#1167e8] text-white", children: _jsx(Info, { className: "h-3 w-3" }) }), "Questions ", firstNumber, "\u2013", lastNumber, " refer to the following passage."] }) }), _jsxs("div", { className: "grid gap-4", style: { gridTemplateColumns: SPLIT_VIEW_GRID }, children: [_jsx("div", { className: "max-h-[calc(100vh-220px)] min-h-0 overflow-y-auto", children: _jsx(EnglishCertificationPassagePanel, { passage: document.passage, passageTitle: document.passageTitle, passageSubtitle: document.passageSubtitle, passageImageUrl: document.passageImageUrl, passageIsHtml: passageIsHtml, partName: part.name ?? '' }) }), _jsx("section", { className: "flex max-h-[calc(100vh-220px)] min-h-0 flex-col overflow-y-auto rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md", children: renderQuestions(document) })] })] }, document.groupId));
|
|
147
|
-
})] }));
|
|
148
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as s}from"react/jsx-runtime";import{FileText as a,Headphones as t,Info as r}from"lucide-react";import{EnglishCertificationQuestionCard as i}from"./EnglishCertificationQuestionCard";import{EnglishCertificationPassagePanel as n}from"./EnglishCertificationPassagePanel";import{EnglishCertificationReadingSection as l}from"./EnglishCertificationReadingSection";import{EnglishCertificationAudioPlayer as o}from"./EnglishCertificationAudioPlayer";import{ENGLISH_CERT_CONTENT_WIDTH_CLASS as d}from"./english-certification-theme";import{isHtmlContent as c,stripInstruction as p}from"./grouped-question-utils";export function EnglishCertificationGroupedQuestion({part:g,partConfig:u,answers:m,flaggedQuestions:f,onAnswerChange:h,onToggleFlag:x,isReviewMode:b=!1,isListeningPart:w=!1,partAudioUrl:N}){const v=u.startIndex??0,q=function(e,s){const a=e.questions[0];return(a?.questionGroup?.payload?.instructions??(s?.trim()||void 0)??a?.content?.question??"").trim()}(g,g.instructions??u.instructions),I=g.sections?.length?[(()=>{const e=g.questions[0]?.questionGroup?.payload??{},s=e.content??e.passage??"",a=p(s,q),t=e.imageUrl??e.passageImageUrl??"";return{groupId:g.partId,questions:g.questions,passage:a,passageTitle:e.passageTitle??e.title??"",passageSubtitle:e.subtitle??"",passageImageUrl:t,hasPassageContent:Boolean(a.trim())||Boolean(t)}})()]:function(e,s){const a=[],t=new Map;return e.questions.forEach(e=>{const s=e.group_id??e.id;t.has(s)||(t.set(s,[]),a.push(s)),t.get(s).push(e)}),a.map(e=>{const a=t.get(e),r=a[0]?.questionGroup?.payload??{},i=r.content??r.passage??"",n=p(i,s),l=r.imageUrl??r.passageImageUrl??"";return{groupId:e,questions:a,passage:n,passageTitle:r.passageTitle??r.title??"",passageSubtitle:r.subtitle??"",passageImageUrl:l,hasPassageContent:Boolean(n.trim())||Boolean(l)}})}(g,q);if(!I.length)return e("div",{className:"flex h-full items-center justify-center rounded-xl border border-red-200 bg-red-50 p-8 text-red-600",children:"No questions found for this part"});const y=w&&Boolean(N),_=s=>g.sections?.length?e("div",{className:"flex-1 space-y-6 px-4 py-4",children:g.sections.map((a,t)=>{const r=s.questions.filter(e=>{const s=g.questions.indexOf(e);return s>=a.startIndex-v&&s<=a.endIndex-v});return r.length?e(l,{part:g,section:a,questions:r,startIndex:v,answers:m,flaggedQuestions:f,onAnswerChange:h,onToggleFlag:x,isReviewMode:b},`${s.groupId}-section-${t}`):null})}):e("div",{className:"flex-1 space-y-3 px-4 py-4",children:s.questions.map(s=>{const a=g.questions.indexOf(s)+1;return e(i,{question:s,globalQuestionNumber:v+a,answer:m[s.id],isFlagged:f.has(s.id),sharedInstruction:q,onAnswerChange:h,onToggleFlag:x,isReviewMode:b},s.id)})});return s("div",{className:"flex h-full flex-col gap-6 overflow-y-auto pb-4",children:[y&&e("div",{className:`${d} w-full self-center`,children:e(o,{audioUrl:N,partNumber:g.partNo})}),q&&e("div",{className:`${d} w-full self-center`,children:s("div",{className:"flex items-start gap-3 rounded-xl border border-[#bfd8f8] bg-gradient-to-r from-[#eef5ff] to-[#f7fbff] px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md",children:[e("span",{className:"mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full bg-[#1167e8] text-white",children:e(w?t:r,{className:"h-3.5 w-3.5"})}),s("div",{className:"min-w-0",children:[g.name&&e("div",{className:"mb-1 text-xs font-black uppercase tracking-wide text-[#0b57d0]",children:g.name}),e("p",{className:"text-base font-semibold leading-relaxed text-[#0f1730]",children:q})]})]})}),I.map(t=>{const i=g.questions.indexOf(t.questions[0])+v+1,l=g.questions.indexOf(t.questions[t.questions.length-1])+v+1,o=c(t.passage);return t.hasPassageContent?s("div",{className:"flex flex-col gap-3",children:[e("div",{className:"flex flex-wrap items-center gap-3 rounded-xl border border-[#d9e4f2] bg-white/85 px-4 py-3 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md",children:s("div",{className:"flex items-center gap-2.5 rounded-md border border-[#bfd8f8] bg-white/70 px-3 py-1.5 text-base font-bold text-[#0b57d0]",children:[e("span",{className:"grid h-[18px] w-[18px] place-items-center rounded-full bg-[#1167e8] text-white",children:e(r,{className:"h-3 w-3"})}),"Questions ",i,"–",l," refer to the following passage."]})}),s("div",{className:"grid gap-4",style:{gridTemplateColumns:"minmax(0, 1fr) minmax(0, 1fr)"},children:[e("div",{className:"max-h-[calc(100vh-220px)] min-h-0 overflow-y-auto",children:e(n,{passage:t.passage,passageTitle:t.passageTitle,passageSubtitle:t.passageSubtitle,passageImageUrl:t.passageImageUrl,passageIsHtml:o,partName:g.name??""})}),e("section",{className:"flex max-h-[calc(100vh-220px)] min-h-0 flex-col overflow-y-auto rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md",children:_(t)})]})]},t.groupId):s("div",{className:`${d} flex w-full flex-col gap-3 self-center`,children:[s("div",{className:"flex items-center gap-4 rounded-xl border border-[#d9e4f2] bg-white/90 px-5 py-4 shadow-[0_5px_16px_rgba(13,71,161,0.08)] backdrop-blur-md",children:[e("span",{className:"grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-[#eaf2ff] text-[#1167e8]",children:e(a,{className:"h-6 w-6"})}),s("div",{className:"min-w-0 flex-1",children:[s("span",{className:"inline-flex items-center gap-1.5 rounded-md border border-[#bfd8f8] bg-[#eaf2ff] px-2.5 py-1 text-xs font-bold text-[#0b57d0]",children:["Questions ",i,"–",l]}),t.passageTitle&&e("p",{className:"mt-2 text-lg font-bold leading-tight text-[#0f1730]",children:t.passageTitle}),t.passageSubtitle&&e("p",{className:"mt-1 text-[15px] font-medium text-[#475569]",children:t.passageSubtitle})]}),t.passageImageUrl&&e("img",{src:t.passageImageUrl,alt:t.passageTitle||"Illustration",className:"h-20 w-32 shrink-0 rounded-lg object-contain"})]}),e("section",{className:"flex flex-col overflow-hidden rounded-xl border border-[#d9e4f2] bg-white/90 shadow-[0_14px_35px_rgba(13,71,161,0.12)] backdrop-blur-md",children:_(t)})]},t.groupId)})]})}
|