@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,90 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Suspense, useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { ArrowLeft, Hash, Loader2, Save } from 'lucide-react';
|
|
5
|
-
import { Badge } from '../../components/ui/badge';
|
|
6
|
-
import { Button } from '../../components/ui/button';
|
|
7
|
-
import { useToast } from '../../shared/lib/hooks/useToast';
|
|
8
|
-
import { useT } from '../../shared/lib/i18n';
|
|
9
|
-
import { ExamCreator } from './ExamCreator';
|
|
10
|
-
function ExamQuestionsContent({ api, examId, listPath = '/manage/exams', onNavigate }) {
|
|
11
|
-
const t = useT();
|
|
12
|
-
const { error: errorToast, success } = useToast();
|
|
13
|
-
const [exam, setExam] = useState(null);
|
|
14
|
-
const [questions, setQuestions] = useState([]);
|
|
15
|
-
const [partAudioOverrides, setPartAudioOverrides] = useState({});
|
|
16
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
17
|
-
const navigate = (href) => {
|
|
18
|
-
if (onNavigate)
|
|
19
|
-
onNavigate(href);
|
|
20
|
-
else if (typeof window !== 'undefined')
|
|
21
|
-
window.location.href = href;
|
|
22
|
-
};
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
let ignore = false;
|
|
25
|
-
setIsLoading(true);
|
|
26
|
-
Promise.all([api.getExam(examId), api.getQuestions?.(examId)])
|
|
27
|
-
.then(([examResult, questionResult]) => {
|
|
28
|
-
if (ignore)
|
|
29
|
-
return;
|
|
30
|
-
setExam(unwrap(examResult));
|
|
31
|
-
setQuestions(unwrapOptional(questionResult) ?? []);
|
|
32
|
-
})
|
|
33
|
-
.catch((err) => errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : t('admin.exams.questions.loadError') }))
|
|
34
|
-
.finally(() => !ignore && setIsLoading(false));
|
|
35
|
-
return () => { ignore = true; };
|
|
36
|
-
}, [api, examId, errorToast, t]);
|
|
37
|
-
const template = useMemo(() => exam?.template ? normalizeTemplate(exam.template) : null, [exam?.template]);
|
|
38
|
-
const handleSaveExam = async (examData) => {
|
|
39
|
-
if (!template)
|
|
40
|
-
return;
|
|
41
|
-
await api.saveExam?.({ examId, questions: examData.questions, template, examData });
|
|
42
|
-
navigate(listPath);
|
|
43
|
-
};
|
|
44
|
-
const handlePartAudioChange = async (partId, audioUrl) => {
|
|
45
|
-
if (!template)
|
|
46
|
-
return;
|
|
47
|
-
const previous = partAudioOverrides[partId];
|
|
48
|
-
setPartAudioOverrides((prev) => ({ ...prev, [partId]: audioUrl }));
|
|
49
|
-
try {
|
|
50
|
-
await api.updatePartAudio?.({ examId, partId, audioUrl, template });
|
|
51
|
-
success({ message: t('admin.exams.partEditor.audioFile') });
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
setPartAudioOverrides((prev) => {
|
|
55
|
-
const next = { ...prev };
|
|
56
|
-
if (previous === undefined)
|
|
57
|
-
delete next[partId];
|
|
58
|
-
else
|
|
59
|
-
next[partId] = previous;
|
|
60
|
-
return next;
|
|
61
|
-
});
|
|
62
|
-
errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : undefined });
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
if (isLoading)
|
|
66
|
-
return _jsx(Loading, { label: t('admin.exams.questions.loading') });
|
|
67
|
-
if (!exam || !template)
|
|
68
|
-
return _jsx(Empty, { onBack: () => navigate(listPath), label: t('admin.exams.questions.noTemplate') });
|
|
69
|
-
return (_jsxs("div", { className: "-m-4 flex h-[calc(100vh-4rem)] flex-col overflow-hidden md:-m-6 md:-mb-10", children: [_jsxs("div", { className: "flex flex-shrink-0 items-center gap-4 border-b border-gray-200/60 bg-white px-4 py-3", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => navigate(listPath), className: "h-8 w-8", children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsxs("h1", { className: "truncate text-lg font-semibold tracking-tight text-gray-900", children: [exam.name, exam.code && _jsxs(Badge, { variant: "outline", className: "ml-2", children: [_jsx(Hash, { className: "mr-0.5 h-3 w-3" }), exam.code] })] }) }), _jsxs(Button, { onClick: () => navigate(listPath), className: "gap-2 bg-blue-600 hover:bg-blue-700", size: "sm", children: [_jsx(Save, { className: "h-4 w-4" }), "Tho\u00E1t"] })] }), _jsx(ExamCreator, { template: template, examId: examId, examDifficulty: exam.difficulty, examLevel: exam.examLevel ?? undefined, subjectId: exam.subject?.id, initialQuestions: questions, onSaveQuestion: api.saveQuestion, onSave: handleSaveExam, onCancel: () => navigate(listPath), onPartAudioChange: api.updatePartAudio ? handlePartAudioChange : undefined, partAudioOverrides: partAudioOverrides })] }));
|
|
70
|
-
}
|
|
71
|
-
export function ExamQuestionsPageContainer(props) {
|
|
72
|
-
return _jsx(Suspense, { fallback: _jsx(Loading, { label: "Loading..." }), children: _jsx(ExamQuestionsContent, { ...props }) });
|
|
73
|
-
}
|
|
74
|
-
function Loading({ label }) {
|
|
75
|
-
return _jsx("div", { className: "flex h-screen items-center justify-center", children: _jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx(Loader2, { className: "h-8 w-8 animate-spin text-blue-600" }), _jsx("p", { className: "text-sm text-gray-600", children: label })] }) });
|
|
76
|
-
}
|
|
77
|
-
function Empty({ label, onBack }) {
|
|
78
|
-
return _jsx("div", { className: "flex h-screen items-center justify-center", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-lg text-gray-900", children: label }), _jsx(Button, { onClick: onBack, className: "mt-4", children: "Quay l\u1EA1i" })] }) });
|
|
79
|
-
}
|
|
80
|
-
function unwrap(value) {
|
|
81
|
-
return typeof value === 'object' && value !== null && 'data' in value ? value.data : value;
|
|
82
|
-
}
|
|
83
|
-
function unwrapOptional(value) {
|
|
84
|
-
return value === undefined ? undefined : unwrap(value);
|
|
85
|
-
}
|
|
86
|
-
function normalizeTemplate(template) {
|
|
87
|
-
if ('name' in template)
|
|
88
|
-
return template;
|
|
89
|
-
return { ...template, name: template.title, description: template.description || template.subTitle || '', level: template.examLevel, parts: template.parts.map((part) => ({ ...part, name: part.name || part.title || '' })) };
|
|
90
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{Suspense as a,useEffect as i,useMemo as n,useRef as s,useState as r}from"react";import{ArrowLeft as o,Hash as l,Loader2 as m,Save as c}from"lucide-react";import{Badge as u}from"../../components/ui/badge";import{Button as d}from"../../components/ui/button";import{useToast as f}from"../../shared/lib/hooks/useToast";import{useT as h}from"../../shared/lib/i18n";import{transformApiQuestionsToFrontend as p}from"../../shared/lib/utils/question-reverse-transform";import{ExamCreator as x}from"./ExamCreator";import{initializeQuestionStatusesFromAPI as b}from"./exam-question-status";function g({api:a,examId:m,listPath:g="/manage/exams",onNavigate:w,texts:q}){const Q=h(),{error:k,success:E}=f(),L=s(null),[j,C]=r(null),[I,P]=r([]),[T,A]=r({}),[z,B]=r(!0),D=e=>{w?w(e):"undefined"!=typeof window&&(window.location.href=e)};i(()=>{let e=!1;return B(!0),Promise.all([a.getExam(m),a.getQuestions?.(m)]).then(([t,a])=>{if(e)return;const i=y(t),n=function(e){return 0===e.length?[]:e.some(e=>"question_type"in e||"questionType"in e)?p(e):e}((void 0===(s=a)?void 0:y(s))??[]);var s;C(i),P(n);const r=function(e){return e?.totalQuestions??0}(i.template);r>0&&b(m,n.map(e=>e.questionNumber??0),r)}).catch(e=>k({message:Q("admin.exams.questions.error"),description:e instanceof Error?e.message:Q("admin.exams.questions.loadError")})).finally(()=>!e&&B(!1)),()=>{e=!0}},[a,m,k,Q]);const S=n(()=>j?.template?function(e){return"name"in e?e:{...e,name:e.title,description:e.description||e.subTitle||"",level:e.examLevel,parts:e.parts.map(e=>({...e,name:e.name||e.title||""}))}}(j.template):null,[j?.template]);return z?e(v,{label:q?.questionsLoadingLabel??Q("admin.exams.questions.loading")}):j&&S?t("div",{className:"-m-4 flex h-[calc(100vh-4rem)] flex-col overflow-hidden md:-m-6 md:-mb-10",children:[t("div",{className:"flex flex-shrink-0 items-center gap-4 border-b border-gray-200/60 bg-white px-4 py-3",children:[e(d,{variant:"ghost",size:"icon",onClick:()=>D(g),className:"h-8 w-8",children:e(o,{className:"h-4 w-4"})}),e("div",{className:"min-w-0 flex-1",children:t("h1",{className:"truncate text-lg font-semibold tracking-tight text-gray-900",children:[j.name,j.code&&t(u,{variant:"outline",className:"ml-2 align-middle border-blue-200 bg-blue-50 text-blue-700 text-xs font-normal",children:[e(l,{className:"mr-0.5 h-3 w-3"}),j.code]})]})}),t(d,{onClick:()=>L.current?.(),className:"gap-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 shadow-sm",size:"sm",children:[e(c,{className:"h-4 w-4"}),q?.saveQuestionsLabel??"Lưu đề thi"]})]}),e(x,{template:S,examId:m,examDifficulty:j.difficulty,examLevel:j.examLevel??void 0,subjectId:j.subject?.id,initialQuestions:I,onSaveQuestion:a.saveQuestion,onSave:async e=>{S&&(await(a.saveExam?.({examId:m,questions:e.questions,template:S,examData:e})),D(g))},onCancel:()=>D(g),onPartAudioChange:a.updatePartAudio?async(e,t)=>{if(!S)return;const i=T[e];A(a=>({...a,[e]:t}));try{await(a.updatePartAudio?.({examId:m,partId:e,audioUrl:t,template:S})),E({message:Q("admin.exams.partEditor.audioFile"),description:"Đã lưu file âm thanh cho part."})}catch(t){A(t=>{const a={...t};return void 0===i?delete a[e]:a[e]=i,a}),k({message:Q("admin.exams.questions.error"),description:t instanceof Error?t.message:void 0})}}:void 0,partAudioOverrides:T,searchQuestions:a.searchQuestions,saveExamRef:L})]}):e(N,{onBack:()=>D(g),label:q?.questionsNoTemplateLabel??Q("admin.exams.questions.noTemplate")})}export function ExamQuestionsPageContainer(t){return e(a,{fallback:e(v,{label:"Loading..."}),children:e(g,{...t})})}function v({label:a}){return e("div",{className:"flex h-screen items-center justify-center",children:t("div",{className:"flex flex-col items-center gap-3",children:[e(m,{className:"h-8 w-8 animate-spin text-blue-600"}),e("p",{className:"text-sm text-gray-600",children:a})]})})}function N({label:a,onBack:i}){return e("div",{className:"flex h-screen items-center justify-center",children:t("div",{className:"text-center",children:[e("p",{className:"text-lg text-gray-900",children:a}),e(d,{onClick:i,className:"mt-4",children:"Quay lại"})]})})}function y(e){return"object"==typeof e&&null!==e&&"data"in e?e.data:e}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const EXAM_DIFFICULTY: {
|
|
2
|
+
readonly EASY: "EASY";
|
|
3
|
+
readonly MEDIUM: "MEDIUM";
|
|
4
|
+
readonly HARD: "HARD";
|
|
5
|
+
};
|
|
6
|
+
export type ExamDifficulty = (typeof EXAM_DIFFICULTY)[keyof typeof EXAM_DIFFICULTY];
|
|
7
|
+
export interface ExamViewDialogExam {
|
|
8
|
+
id: string;
|
|
9
|
+
code?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
subject: string;
|
|
12
|
+
level: string;
|
|
13
|
+
duration: number;
|
|
14
|
+
questionCount: number;
|
|
15
|
+
totalPoints: number;
|
|
16
|
+
difficulty: ExamDifficulty;
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ExamViewDialogProps {
|
|
20
|
+
open: boolean;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
exam: ExamViewDialogExam | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function ExamViewDialog({ open, onOpenChange, exam }: ExamViewDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{Dialog as n,DialogContent as i,DialogDescription as r,DialogFooter as s,DialogHeader as l,DialogTitle as t}from"../../components/ui/dialog";import{Button as o}from"../../components/ui/button";import{Badge as c}from"../../components/ui/badge";import{BookOpen as m,Clock as d,FileText as g,GraduationCap as b,Hash as u,Star as x,Target as h,X as N}from"lucide-react";import{useT as p}from"../../shared/lib/i18n";export const EXAM_DIFFICULTY={EASY:"EASY",MEDIUM:"MEDIUM",HARD:"HARD"};function v({exam:n}){return e("div",{className:"bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-5 text-white",children:e(l,{className:"space-y-2",children:e("div",{className:"flex items-start justify-between",children:a("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-12 w-12 items-center justify-center rounded-xl bg-white/20 backdrop-blur-sm",children:e(g,{className:"h-6 w-6 text-white"})}),a("div",{children:[e(t,{className:"text-xl font-bold text-white",children:n.name}),e(r,{className:"text-blue-100 mt-1",children:a("span",{className:"inline-flex items-center gap-1.5",children:[e(u,{className:"h-3.5 w-3.5"}),n.code||n.id]})})]})]})})})})}function C({label:n,value:i,icon:r,iconContainerClassName:s,iconClassName:l,hoverClassName:t}){return e("div",{className:`rounded-xl border border-gray-200 bg-gray-50/50 p-4 transition-all ${t}`,children:a("div",{className:"flex items-center gap-3",children:[e("div",{className:`flex h-10 w-10 items-center justify-center rounded-lg ${s}`,children:e(r,{className:`h-5 w-5 ${l}`})}),a("div",{children:[e("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:n}),e("p",{className:"text-sm font-semibold text-gray-900 mt-0.5",children:i})]})]})})}function f({label:n,config:i}){const r=i.icon;return e("div",{className:"rounded-xl border border-gray-200 bg-gray-50/50 p-4 transition-all hover:border-gray-300",children:a("div",{className:"flex items-center gap-3",children:[e("div",{className:`flex h-10 w-10 items-center justify-center rounded-lg ${i.iconContainerClassName}`,children:e(r,{className:`h-5 w-5 ${i.iconClassName}`})}),a("div",{children:[e("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:n}),e(c,{className:`mt-1 ${i.badgeClassName} border`,children:i.label})]})]})})}function y({description:n,title:i}){return a("div",{className:"rounded-xl border border-gray-200 bg-gray-50/50 p-4",children:[a("h4",{className:"text-sm font-semibold text-gray-900 mb-2 flex items-center gap-2",children:[e(g,{className:"h-4 w-4 text-gray-500"}),i]}),e("p",{className:"text-sm text-gray-600 leading-relaxed",children:n})]})}function w({closeLabel:n,onClose:i}){return e(s,{className:"border-t border-gray-200 bg-gray-50/50 px-6 py-4",children:a(o,{variant:"outline",onClick:i,className:"gap-2",children:[e(N,{className:"h-4 w-4"}),n]})})}export function ExamViewDialog({open:r,onOpenChange:s,exam:l}){const t=p();if(!l)return null;const o=function(e,a){return{[EXAM_DIFFICULTY.EASY]:{label:a("admin.exams.difficulty.easy"),badgeClassName:"bg-green-50 text-green-700 border-green-200",iconContainerClassName:"bg-green-100",iconClassName:"text-green-600",icon:x},[EXAM_DIFFICULTY.MEDIUM]:{label:a("admin.exams.difficulty.medium"),badgeClassName:"bg-yellow-50 text-yellow-700 border-yellow-200",iconContainerClassName:"bg-yellow-100",iconClassName:"text-yellow-600",icon:x},[EXAM_DIFFICULTY.HARD]:{label:a("admin.exams.difficulty.hard"),badgeClassName:"bg-red-50 text-red-700 border-red-200",iconContainerClassName:"bg-red-100",iconClassName:"text-red-600",icon:x}}[e]}(l.difficulty,t);return e(n,{open:r,onOpenChange:s,children:a(i,{className:"max-w-2xl p-0 overflow-hidden",children:[e(v,{exam:l}),a("div",{className:"px-6 py-5 space-y-6",children:[a("div",{className:"grid grid-cols-2 md:grid-cols-3 gap-4",children:[e(C,{label:t("admin.exams.view.subject"),value:l.subject,icon:m,iconContainerClassName:"bg-blue-100",iconClassName:"text-blue-600",hoverClassName:"hover:border-blue-200 hover:bg-blue-50/30"}),e(C,{label:t("admin.exams.view.level"),value:l.level,icon:b,iconContainerClassName:"bg-purple-100",iconClassName:"text-purple-600",hoverClassName:"hover:border-purple-200 hover:bg-purple-50/30"}),e(C,{label:t("admin.exams.view.duration"),value:t("admin.exams.view.minutes",{count:String(l.duration)}),icon:d,iconContainerClassName:"bg-orange-100",iconClassName:"text-orange-600",hoverClassName:"hover:border-orange-200 hover:bg-orange-50/30"}),e(C,{label:t("admin.exams.view.questionCount"),value:t("admin.exams.view.questions",{count:String(l.questionCount)}),icon:g,iconContainerClassName:"bg-cyan-100",iconClassName:"text-cyan-600",hoverClassName:"hover:border-cyan-200 hover:bg-cyan-50/30"}),e(C,{label:t("admin.exams.view.totalPoints"),value:t("admin.exams.view.points",{count:String(l.totalPoints)}),icon:h,iconContainerClassName:"bg-green-100",iconClassName:"text-green-600",hoverClassName:"hover:border-green-200 hover:bg-green-50/30"}),e(f,{label:t("admin.exams.view.difficulty"),config:o})]}),l.description&&e(y,{title:t("admin.exams.view.description"),description:l.description})]}),e(w,{closeLabel:t("admin.exams.view.close"),onClose:()=>s(!1)})]})})}
|
|
@@ -1,9 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Headphones, Music } from 'lucide-react';
|
|
4
|
-
import { Card, CardContent } from '../../components/ui/card';
|
|
5
|
-
import { FileUpload } from '../../components/ui/file-upload';
|
|
6
|
-
import { Label } from '../../components/ui/label';
|
|
7
|
-
export function PartAudioUploader({ partId, partName, audioUrl, onAudioChange, examId, isReadOnly = false }) {
|
|
8
|
-
return (_jsxs(Card, { className: "mb-4 overflow-hidden border-0 bg-white shadow-lg shadow-sky-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-sky-500 via-cyan-500 to-teal-500" }), _jsx(CardContent, { className: "px-4 py-4", children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 to-cyan-600 shadow-md", children: _jsx(Headphones, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsxs(Label, { className: "text-sm font-bold text-gray-900", children: ["File \u00E2m thanh cho ", partName] }), _jsx("p", { className: "text-xs text-gray-500", children: "Audio d\u00F9ng chung cho t\u1EA5t c\u1EA3 c\u00E2u h\u1ECFi trong ph\u1EA7n n\u00E0y." })] })] }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `part-audio-${partId}`, label: "", accept: "audio/*", value: audioUrl || '', onChange: (url) => !isReadOnly && onAudioChange(url), placeholder: "Ch\u1ECDn file audio cho part n\u00E0y...", icon: _jsx(Music, { className: "h-4 w-4" }), maxSize: 100, autoUpload: true, examId: examId, partId: partId, showPlayButton: true, disabled: isReadOnly }) })] }) })] }));
|
|
9
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{Headphones as t,Music as o}from"lucide-react";import{Card as i,CardContent as r}from"../../components/ui/card";import{FileUpload as l}from"../../components/ui/file-upload";import{Label as s}from"../../components/ui/label";export function PartAudioUploader({partId:d,partName:n,audioUrl:c,onAudioChange:m,examId:h,isReadOnly:p=!1}){return a(i,{className:"mb-4 overflow-hidden border-0 bg-white shadow-lg shadow-sky-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-sky-500 via-cyan-500 to-teal-500"}),e(r,{className:"px-4 py-4",children:a("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4",children:[a("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 to-cyan-600 shadow-md",children:e(t,{className:"h-5 w-5 text-white"})}),a("div",{children:[a(s,{className:"text-sm font-bold text-gray-900",children:["File âm thanh cho ",n]}),e("p",{className:"text-xs text-gray-500",children:"Audio dùng chung cho tất cả câu hỏi trong phần này."})]})]}),e("div",{className:"flex-1",children:e(l,{id:`part-audio-${d}`,label:"",accept:"audio/*",value:c||"",onChange:e=>!p&&m(e),placeholder:"Chọn file audio cho part này...",icon:e(o,{className:"h-4 w-4"}),maxSize:100,autoUpload:!0,examId:h,partId:d,showPlayButton:!0,disabled:p})})]})})]})}
|
|
@@ -1,66 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { ChevronLeft, ChevronRight, Loader2, Search } from 'lucide-react';
|
|
5
|
-
import { Button } from '../../components/ui/button';
|
|
6
|
-
import { Card, CardContent } from '../../components/ui/card';
|
|
7
|
-
import { Input } from '../../components/ui/input';
|
|
8
|
-
import { useT } from '../../shared/lib/i18n';
|
|
9
|
-
import { mapTemplateDetailToExamTemplate } from '../../api/exam-create';
|
|
10
|
-
export function TemplateSelector({ api, onTemplateSelect, onCancel }) {
|
|
11
|
-
const t = useT();
|
|
12
|
-
const [search, setSearch] = useState('');
|
|
13
|
-
const [page, setPage] = useState(1);
|
|
14
|
-
const [templates, setTemplates] = useState([]);
|
|
15
|
-
const [totalPages, setTotalPages] = useState(1);
|
|
16
|
-
const [totalItems, setTotalItems] = useState(0);
|
|
17
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
-
const [error, setError] = useState();
|
|
19
|
-
const [loadingId, setLoadingId] = useState();
|
|
20
|
-
// Reset to the first page whenever the search term changes so results stay in sync.
|
|
21
|
-
useEffect(() => { setPage(1); }, [search]);
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
let ignore = false;
|
|
24
|
-
setIsLoading(true);
|
|
25
|
-
api.getExamTemplates({ page, search: search || undefined })
|
|
26
|
-
.then((result) => {
|
|
27
|
-
if (ignore)
|
|
28
|
-
return;
|
|
29
|
-
setTemplates(result.items);
|
|
30
|
-
setTotalPages(result.totalPages);
|
|
31
|
-
setTotalItems(result.totalItems);
|
|
32
|
-
})
|
|
33
|
-
.catch((err) => { if (!ignore)
|
|
34
|
-
setError(err instanceof Error ? err.message : t('admin.exams.questions.loadError')); })
|
|
35
|
-
.finally(() => { if (!ignore)
|
|
36
|
-
setIsLoading(false); });
|
|
37
|
-
return () => { ignore = true; };
|
|
38
|
-
}, [api, page, search, t]);
|
|
39
|
-
// The list endpoint returns lightweight summaries (no `parts`). Fetch the full template detail by
|
|
40
|
-
// id and map it to an `ExamTemplate` before emitting, so downstream (TemplateSection /
|
|
41
|
-
// buildExamTemplatePayload) gets a complete entity. Ported from oe-exam-fe TemplateSelector.
|
|
42
|
-
const handleSelect = async (summary) => {
|
|
43
|
-
setError(undefined);
|
|
44
|
-
setLoadingId(summary.id);
|
|
45
|
-
try {
|
|
46
|
-
const detail = unwrapDetail(await api.getExamTemplateDetail(summary.id));
|
|
47
|
-
onTemplateSelect(mapTemplateDetailToExamTemplate(detail));
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
setError(err instanceof Error ? err.message : t('admin.exams.questions.loadError'));
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
setLoadingId(undefined);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
const canPrev = page > 1 && !isLoading;
|
|
57
|
-
const canNext = page < totalPages && !isLoading;
|
|
58
|
-
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" }), _jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), placeholder: t('admin.templates.searchPlaceholder'), className: "pl-9" })] }), isLoading ? _jsx("div", { className: "flex flex-1 items-center justify-center", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-blue-600" }) }) : null, !isLoading && error ? _jsx("p", { className: "text-sm text-red-600", children: error }) : null, !isLoading && !error ? _jsx("div", { className: "grid flex-1 content-start gap-3 overflow-y-auto md:grid-cols-2 lg:grid-cols-3", children: templates.map((template) => _jsx(TemplateCard, { template: template, onSelect: handleSelect, isLoading: loadingId === template.id, disabled: !!loadingId }, template.id)) }) : null, !isLoading && !error && templates.length === 0 ? _jsx("p", { className: "flex-1 text-center text-sm text-gray-500", children: t('admin.templates.table.empty') }) : null, !isLoading && !error && totalItems > 0 ? (_jsxs("div", { className: "flex shrink-0 items-center justify-between border-t border-gray-100 pt-3", children: [_jsx("span", { className: "text-sm text-gray-500", children: t('admin.templates.table.showing', { count: templates.length, total: totalItems }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", disabled: !canPrev, onClick: () => setPage((value) => Math.max(1, value - 1)), children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), t('common.pagination.previous')] }), _jsx("span", { className: "text-sm text-gray-600", children: t('admin.schedules.list.page', { current: page, total: totalPages }) }), _jsxs(Button, { variant: "outline", size: "sm", disabled: !canNext, onClick: () => setPage((value) => value + 1), children: [t('common.pagination.next'), _jsx(ChevronRight, { className: "h-4 w-4" })] })] })] })) : null, onCancel && _jsx("div", { className: "flex shrink-0 justify-end", children: _jsx(Button, { variant: "outline", onClick: onCancel, children: t('common.cancel') }) })] }));
|
|
59
|
-
}
|
|
60
|
-
function TemplateCard({ template, onSelect, isLoading, disabled }) {
|
|
61
|
-
return (_jsx(Card, { className: `relative cursor-pointer transition hover:border-blue-300 hover:shadow-sm ${disabled ? 'pointer-events-none opacity-60' : ''}`, onClick: () => onSelect(template), children: _jsxs(CardContent, { className: "space-y-2 p-4", children: [_jsx("h3", { className: "line-clamp-2 font-medium text-gray-900", children: template.title }), _jsx("p", { className: "line-clamp-2 text-sm text-gray-500", children: template.subTitle }), _jsxs("p", { className: "text-xs text-gray-500", children: [template.totalQuestions, " c\u00E2u h\u1ECFi \u2022 ", template.totalParts, " ph\u1EA7n"] }), isLoading ? _jsx("div", { className: "absolute inset-0 flex items-center justify-center rounded-xl bg-white/60", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-blue-600" }) }) : null] }) }));
|
|
62
|
-
}
|
|
63
|
-
/** Unwrap the optional `{ data }` envelope returned by the detail endpoint. */
|
|
64
|
-
function unwrapDetail(result) {
|
|
65
|
-
return 'data' in result ? result.data : result;
|
|
66
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{useEffect as t,useState as l}from"react";import{BookOpen as s,ChevronLeft as n,ChevronRight as i,Clock as r,Loader2 as c,Search as o,Users as m}from"lucide-react";import{Button as d}from"../../components/ui/button";import{Card as p,CardContent as g,CardDescription as h,CardHeader as x,CardTitle as u}from"../../components/ui/card";import{Input as N}from"../../components/ui/input";import{useT as f}from"../../shared/lib/i18n";import{mapTemplateDetailToExamTemplate as v}from"../../api/exam-create";export function TemplateSelector({api:s,onTemplateSelect:r,onCancel:m}){const p=f(),[g,h]=l(""),[x,u]=l(1),[b,y]=l([]),[k,j]=l(1),[C,E]=l(0),[T,L]=l(!0),[S,D]=l(),[P,q]=l();t(()=>{u(1)},[g]),t(()=>{let e=!1;return L(!0),s.getExamTemplates({page:x,search:g||void 0}).then(a=>{e||(y(a.items),j(a.totalPages),E(a.totalItems))}).catch(a=>{e||D(a instanceof Error?a.message:p("admin.exams.questions.loadError"))}).finally(()=>{e||L(!1)}),()=>{e=!0}},[s,x,g,p]);const z=async e=>{D(void 0),q(e.id);try{const t="data"in(a=await s.getExamTemplateDetail(e.id))?a.data:a;r(v(t))}catch(e){D(e instanceof Error?e.message:p("admin.exams.questions.loadError"))}finally{q(void 0)}var a},A=x>1&&!T,I=x<k&&!T;return a("div",{className:"flex min-h-0 flex-1 flex-col gap-4",children:[a("div",{className:"relative",children:[e(o,{className:"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),e(N,{value:g,onChange:e=>h(e.target.value),placeholder:p("admin.templates.searchPlaceholder"),className:"pl-9"})]}),T?e("div",{className:"flex flex-1 items-center justify-center",children:e(c,{className:"h-6 w-6 animate-spin text-blue-600"})}):null,!T&&S?e("p",{className:"text-sm text-red-600",children:S}):null,T||S?null:e("div",{className:"grid flex-1 content-start gap-3 overflow-y-auto md:grid-cols-2 lg:grid-cols-3",children:b.map((a,t)=>e(w,{template:a,index:t,onSelect:z,isLoading:P===a.id,disabled:!!P},a.id))}),T||S||0!==b.length?null:e("p",{className:"flex-1 text-center text-sm text-gray-500",children:p("admin.templates.table.empty")}),!T&&!S&&C>0?a("div",{className:"flex shrink-0 items-center justify-between border-t border-gray-100 pt-3",children:[e("span",{className:"text-sm text-gray-500",children:p("admin.templates.table.showing",{count:b.length,total:C})}),a("div",{className:"flex items-center gap-2",children:[a(d,{variant:"outline",size:"sm",disabled:!A,onClick:()=>u(e=>Math.max(1,e-1)),children:[e(n,{className:"h-4 w-4"}),p("common.pagination.previous")]}),e("span",{className:"text-sm text-gray-600",children:p("admin.schedules.list.page",{current:x,total:k})}),a(d,{variant:"outline",size:"sm",disabled:!I,onClick:()=>u(e=>e+1),children:[p("common.pagination.next"),e(i,{className:"h-4 w-4"})]})]})]}):null,m&&e("div",{className:"flex shrink-0 justify-end",children:e(d,{variant:"outline",onClick:m,children:p("common.cancel")})})]})}const b=["blue","green","purple","orange","pink","indigo"],y={blue:"bg-blue-100 text-blue-600",green:"bg-green-100 text-green-600",purple:"bg-purple-100 text-purple-600",orange:"bg-orange-100 text-orange-600",pink:"bg-pink-100 text-pink-600",indigo:"bg-indigo-100 text-indigo-600"};function w({template:t,index:l,onSelect:n,isLoading:i,disabled:o}){const d=b[l%b.length];return a(p,{className:"relative cursor-pointer border-2 border-gray-200 bg-white transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 hover:shadow-md "+(o?"pointer-events-none opacity-60":""),onClick:()=>n(t),children:[e(x,{className:"px-4 py-3",children:a("div",{className:"flex items-center gap-2",children:[e("div",{className:`rounded-md p-1.5 ${y[d]}`,children:e(s,{className:"h-4 w-4"})}),a("div",{className:"min-w-0 flex-1",children:[e(u,{className:"truncate text-sm font-semibold",children:t.title}),t.subTitle&&e(h,{className:"truncate text-[11px]",children:t.subTitle})]})]})}),e(g,{className:"space-y-2 px-4 pb-3 pt-0",children:a("div",{className:"grid grid-cols-2 gap-x-3 gap-y-1 text-xs",children:[a("div",{className:"flex items-center gap-1.5",children:[e(s,{className:"h-3 w-3 text-gray-500"}),a("span",{className:"text-gray-600",children:[t.totalQuestions," câu hỏi"]})]}),a("div",{className:"flex items-center gap-1.5",children:[e("div",{className:"h-3 w-3 rounded-full bg-gray-300"}),a("span",{className:"text-gray-600",children:[t.totalParts," phần"]})]}),t.examLevel&&a("div",{className:"flex items-center gap-1.5",children:[e(m,{className:"h-3 w-3 text-gray-500"}),a("span",{className:"text-gray-600",children:["Cấp độ ",t.examLevel]})]}),t.createdAt&&a("div",{className:"flex items-center gap-1.5",children:[e(r,{className:"h-3 w-3 text-gray-500"}),e("span",{className:"text-gray-600",children:new Date(t.createdAt).toLocaleDateString("vi-VN")})]})]})}),i?e("div",{className:"absolute inset-0 flex items-center justify-center rounded-xl bg-white/60",children:e(c,{className:"h-5 w-5 animate-spin text-blue-600"})}):null]})}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface UnsavedChangesDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
onConfirm: () => void;
|
|
5
|
+
onCancel: () => void;
|
|
6
|
+
questionNumber?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function UnsavedChangesDialog({ open, onOpenChange, onConfirm, onCancel, questionNumber, }: UnsavedChangesDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{AlertDialog as n,AlertDialogAction as c,AlertDialogCancel as r,AlertDialogContent as l,AlertDialogDescription as s,AlertDialogFooter as t,AlertDialogHeader as i,AlertDialogTitle as h}from"../../components/ui/alert-dialog";import{AlertTriangle as m,Save as o,X as d}from"lucide-react";export function UnsavedChangesDialog({open:g,onOpenChange:x,onConfirm:p,onCancel:u,questionNumber:b}){return e(n,{open:g,onOpenChange:x,children:a(l,{className:"max-w-md",children:[a("div",{className:"flex flex-col items-center pt-2 pb-4",children:[e("div",{className:"flex h-16 w-16 items-center justify-center rounded-full bg-amber-100 mb-4",children:e(m,{className:"h-8 w-8 text-amber-600"})}),a(i,{className:"text-center space-y-2",children:[e(h,{className:"text-xl font-semibold text-gray-900",children:"Xác nhận chuyển câu hỏi"}),e(s,{className:"text-center text-gray-600",children:b?a("span",{children:["Câu hỏi ",e("strong",{className:"text-gray-800",children:b})," có những thay đổi chưa được lưu."]}):e("span",{children:"Câu hỏi hiện tại có những thay đổi chưa được lưu."})})]})]}),e("div",{className:"rounded-lg border border-amber-200 bg-amber-50 p-4 mb-4",children:e("p",{className:"text-sm text-amber-800",children:"Nếu bạn tiếp tục mà không lưu, các thay đổi sẽ bị mất và không thể khôi phục."})}),a(t,{className:"flex flex-col-reverse sm:flex-row gap-2 sm:gap-3",children:[a(r,{onClick:u,className:"flex-1 gap-2 border-gray-300 hover:bg-gray-50",children:[e(o,{className:"h-4 w-4"}),"Quay lại chỉnh sửa"]}),a(c,{onClick:p,className:"flex-1 gap-2 bg-red-500 hover:bg-red-600 text-white",children:[e(d,{className:"h-4 w-4"}),"Hủy thay đổi"]})]})]})})}
|
|
@@ -2,4 +2,4 @@ export { buildExamTemplatePayload, createExamCreateApi, createMockCreateExamApi
|
|
|
2
2
|
export type { CreateExamFetchApiOptions, CreateExamPayload, CreateExamSdkApi, ExamFormData, ExamLevelOption, ExamResponse, ExamTemplatePayload, UpdateExamTemplateInput } from '../../api/exam-create';
|
|
3
3
|
export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from '../../api/exam-questions';
|
|
4
4
|
export { createExamTakingApi, createMockExamTakingApi } from '../../api/exam-taking';
|
|
5
|
-
export type { ExamQuestionsSdkApi, ExamWithTemplate, FetchApiOptions, SaveExamInput, SaveQuestionInput, UpdatePartAudioInput } from '../../api/exam-questions';
|
|
5
|
+
export type { ExamQuestionsSdkApi, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, QuestionSearchItem, SaveExamInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, UpdatePartAudioInput } from '../../api/exam-questions';
|
|
@@ -1,3 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from '../../api/exam-questions';
|
|
3
|
-
export { createExamTakingApi, createMockExamTakingApi } from '../../api/exam-taking';
|
|
1
|
+
export{buildExamTemplatePayload,createExamCreateApi,createMockCreateExamApi}from"../../api/exam-create";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"../../api/exam-questions";export{createExamTakingApi,createMockExamTakingApi}from"../../api/exam-taking";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional label overrides for the exam-creation flow. Lets a consumer reuse
|
|
3
|
+
* these containers in a non-exam context (e.g. authoring homework) by passing
|
|
4
|
+
* its own wording. Every field is optional; when omitted the component falls
|
|
5
|
+
* back to its default i18n string, so existing usages are unaffected.
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateExamTexts {
|
|
8
|
+
/** Page title — default 'Tạo đề thi mới'. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Step subtitle — default 'Bước 1: Nhập thông tin cơ bản của đề thi'. */
|
|
11
|
+
step1?: string;
|
|
12
|
+
/** Basic-info card title — default 'Thông tin cơ bản'. */
|
|
13
|
+
basicInfo?: string;
|
|
14
|
+
/** Required-fields hint shown under the basic-info card title. */
|
|
15
|
+
requiredHint?: string;
|
|
16
|
+
/** Name field label — default 'Tên đề thi'. */
|
|
17
|
+
nameLabel?: string;
|
|
18
|
+
/** Name field placeholder — default 'Nhập tên đề thi...'. */
|
|
19
|
+
namePlaceholder?: string;
|
|
20
|
+
/** Code field label — default 'Mã đề thi'. */
|
|
21
|
+
codeLabel?: string;
|
|
22
|
+
/** Code field placeholder — default 'Nhập mã đề thi...'. */
|
|
23
|
+
codePlaceholder?: string;
|
|
24
|
+
/** Duration field label — default 'Thời gian thi (phút)'. */
|
|
25
|
+
durationLabel?: string;
|
|
26
|
+
/** Description field label — default 'Mô tả'. */
|
|
27
|
+
descriptionLabel?: string;
|
|
28
|
+
/** Description field placeholder — default exam description placeholder. */
|
|
29
|
+
descriptionPlaceholder?: string;
|
|
30
|
+
/** Template section title — default 'Chọn Template'. */
|
|
31
|
+
templateSectionTitle?: string;
|
|
32
|
+
/** Empty template hint — default 'Chưa chọn template đề thi'. */
|
|
33
|
+
noTemplateText?: string;
|
|
34
|
+
/** Template picker dialog title — default 'Chọn template đề thi'. */
|
|
35
|
+
templateDialogTitle?: string;
|
|
36
|
+
/** Template picker dialog description — default 'Chọn loại đề thi bạn muốn tạo.'. */
|
|
37
|
+
templateDialogDescription?: string;
|
|
38
|
+
/** Missing-template warning description. */
|
|
39
|
+
missingTemplateDescription?: string;
|
|
40
|
+
/** Button text for picking a template. */
|
|
41
|
+
selectTemplateLabel?: string;
|
|
42
|
+
/** Button text for changing a selected template. */
|
|
43
|
+
changeTemplateLabel?: string;
|
|
44
|
+
/** Cancel button label. */
|
|
45
|
+
cancelLabel?: string;
|
|
46
|
+
/** Saving button label. */
|
|
47
|
+
savingLabel?: string;
|
|
48
|
+
/** Submit button label. */
|
|
49
|
+
saveAndContinueLabel?: string;
|
|
50
|
+
/** Success toast description. Use {name} as placeholder for the created name. */
|
|
51
|
+
successDescription?: string;
|
|
52
|
+
/** Loading label for question-authoring screen. */
|
|
53
|
+
questionsLoadingLabel?: string;
|
|
54
|
+
/** Empty-template label for question-authoring screen. */
|
|
55
|
+
questionsNoTemplateLabel?: string;
|
|
56
|
+
/** Save button label for question-authoring screen. */
|
|
57
|
+
saveQuestionsLabel?: string;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -1,18 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const examFormSchema = z.object({
|
|
3
|
-
code: z.string().min(1, 'Mã đề thi không được để trống').max(100, 'Mã đề thi không được vượt quá 100 ký tự').regex(/^[a-zA-Z0-9_-]+$/, 'Mã đề thi chỉ được chứa chữ cái, số, dấu gạch ngang (-) và gạch dưới (_)').trim(),
|
|
4
|
-
name: z.string().min(3, 'Tên đề thi phải có ít nhất 3 ký tự').max(255, 'Tên đề thi không được vượt quá 255 ký tự').trim(),
|
|
5
|
-
level: z.string(),
|
|
6
|
-
difficulty: z.string(),
|
|
7
|
-
duration: z.string().refine((value) => {
|
|
8
|
-
if (!value)
|
|
9
|
-
return true;
|
|
10
|
-
const minutes = Number.parseInt(value, 10);
|
|
11
|
-
return !Number.isNaN(minutes) && minutes >= 5 && minutes <= 300;
|
|
12
|
-
}, { message: 'Thời gian thi phải từ 5 đến 300 phút' }),
|
|
13
|
-
totalPoints: z.string().min(1, 'Vui lòng nhập tổng điểm').refine((value) => /^[1-9]\d*$/.test(value.trim()), { message: 'Tổng điểm phải là số nguyên dương' }).refine((value) => {
|
|
14
|
-
const points = Number.parseInt(value, 10);
|
|
15
|
-
return points >= 1 && points <= 1000;
|
|
16
|
-
}, { message: 'Tổng điểm phải từ 1 đến 1000' }),
|
|
17
|
-
description: z.string().max(2000, 'Mô tả không được vượt quá 2000 ký tự').trim(),
|
|
18
|
-
});
|
|
1
|
+
import{z as t}from"zod";export const examFormSchema=t.object({code:t.string().min(1,"Mã đề kiểm tra không được để trống").max(100,"Mã đề kiểm tra không được vượt quá 100 ký tự").regex(/^[a-zA-Z0-9_-]+$/,"Mã đề kiểm tra chỉ được chứa chữ cái, số, dấu gạch ngang (-) và gạch dưới (_)").trim(),name:t.string().min(3,"Tên đề kiểm tra phải có ít nhất 3 ký tự").max(255,"Tên đề kiểm tra không được vượt quá 255 ký tự").trim(),level:t.string(),difficulty:t.string(),duration:t.string().refine(t=>{if(!t)return!0;const n=Number.parseInt(t,10);return!Number.isNaN(n)&&n>=5&&n<=300},{message:"Thời gian kiểm tra phải từ 5 đến 300 phút"}),totalPoints:t.string().min(1,"Vui lòng nhập tổng điểm").refine(t=>/^[1-9]\d*$/.test(t.trim()),{message:"Tổng điểm phải là số nguyên dương"}).refine(t=>{const n=Number.parseInt(t,10);return n>=1&&n<=1e3},{message:"Tổng điểm phải từ 1 đến 1000"}),description:t.string().max(2e3,"Mô tả không được vượt quá 2000 ký tự").trim()});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare enum QuestionStatus {
|
|
2
|
+
NEW = "new",
|
|
3
|
+
SAVED = "saved",
|
|
4
|
+
UNSAVED = "unsaved"
|
|
5
|
+
}
|
|
6
|
+
export declare function getQuestionStatus(examId: string, questionNumber: number): QuestionStatus;
|
|
7
|
+
export declare function setQuestionStatus(examId: string, questionNumber: number, status: QuestionStatus): void;
|
|
8
|
+
export declare function clearQuestionStatuses(examId: string): void;
|
|
9
|
+
export declare function initializeQuestionStatusesFromAPI(examId: string, savedQuestionNumbers: number[], totalQuestions: number): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export var QuestionStatus;!function(t){t.NEW="new",t.SAVED="saved",t.UNSAVED="unsaved"}(QuestionStatus||(QuestionStatus={}));const t=t=>`oe-exam-question-status:${t}`;function e(e){if("undefined"==typeof window)return{};const n=window.localStorage.getItem(t(e));if(!n)return{};try{return JSON.parse(n)}catch{return{}}}function n(e,n){"undefined"!=typeof window&&window.localStorage.setItem(t(e),JSON.stringify(n))}export function getQuestionStatus(t,n){return e(t)[n]??QuestionStatus.NEW}export function setQuestionStatus(t,o,i){n(t,{...e(t),[o]:i})}export function clearQuestionStatuses(e){"undefined"!=typeof window&&window.localStorage.removeItem(t(e))}export function initializeQuestionStatusesFromAPI(t,e,o){const i=new Set(e.filter(t=>Number.isFinite(t))),u={};for(let t=1;t<=o;t+=1)u[t]=i.has(t)?QuestionStatus.SAVED:QuestionStatus.NEW;n(t,u)}
|
|
@@ -1,18 +1 @@
|
|
|
1
|
-
export function normalizeTemplateGroupConfig(config) {
|
|
2
|
-
return {
|
|
3
|
-
...config,
|
|
4
|
-
groups: config.groups ?? config.group?.items,
|
|
5
|
-
groupConfig: config.groupConfig ?? config.group?.config,
|
|
6
|
-
groupType: config.groupType ?? config.group?.type,
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export function getPartByQuestionIndex(template, questionIndex) {
|
|
10
|
-
return template.parts.find((part) => questionIndex >= part.startIndex && questionIndex <= part.endIndex);
|
|
11
|
-
}
|
|
12
|
-
export function getSectionForQuestionIndex(part, questionIndex) {
|
|
13
|
-
return part.sections?.find((section) => questionIndex >= section.startIndex && questionIndex <= section.endIndex);
|
|
14
|
-
}
|
|
15
|
-
export function getEffectiveQuestionConfig(part, questionIndex) {
|
|
16
|
-
const section = getSectionForQuestionIndex(part, questionIndex);
|
|
17
|
-
return section ? { ...normalizeTemplateGroupConfig(section), sectionName: section.name } : normalizeTemplateGroupConfig(part);
|
|
18
|
-
}
|
|
1
|
+
export function normalizeTemplateGroupConfig(e){return{...e,groups:e.groups??e.group?.items,groupConfig:e.groupConfig??e.group?.config,groupType:e.groupType??e.group?.type}}export function getPartByQuestionIndex(e,n){return e.parts.find(e=>n>=e.startIndex&&n<=e.endIndex)}export function getSectionForQuestionIndex(e,n){return e.sections?.find(e=>n>=e.startIndex&&n<=e.endIndex)}export function getEffectiveQuestionConfig(e,n){const o=getSectionForQuestionIndex(e,n);return o?{...normalizeTemplateGroupConfig(o),sectionName:o.name}:normalizeTemplateGroupConfig(e)}
|
|
@@ -3,6 +3,7 @@ export { CreateExamPageContainer } from './CreateExamPageContainer';
|
|
|
3
3
|
export type { CreateExamPageContainerProps } from './CreateExamPageContainer';
|
|
4
4
|
export { CreateExamInfoForm } from './CreateExamInfoForm';
|
|
5
5
|
export type { CreateExamInfoFormProps } from './CreateExamInfoForm';
|
|
6
|
+
export type { CreateExamTexts } from './create-exam.texts';
|
|
6
7
|
export { TemplateSelector } from './TemplateSelector';
|
|
7
8
|
export type { TemplateSelectorProps } from './TemplateSelector';
|
|
8
9
|
export { ExamCreator } from './ExamCreator';
|
|
@@ -12,6 +13,15 @@ export { ExamQuestionGrid } from './ExamQuestionGrid';
|
|
|
12
13
|
export { PartAudioUploader } from './PartAudioUploader';
|
|
13
14
|
export type { PartAudioUploaderProps } from './PartAudioUploader';
|
|
14
15
|
export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from './api';
|
|
15
|
-
export type { ExamQuestionsSdkApi, ExamWithTemplate, FetchApiOptions, SaveExamInput, SaveQuestionInput, UpdatePartAudioInput } from './api';
|
|
16
|
+
export type { ExamQuestionsSdkApi, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, QuestionSearchItem, SaveExamInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, UpdatePartAudioInput } from './api';
|
|
16
17
|
export { ExamQuestionsPageContainer } from './ExamQuestionsPageContainer';
|
|
17
18
|
export type { ExamQuestionsPageContainerProps } from './ExamQuestionsPageContainer';
|
|
19
|
+
export { UnsavedChangesDialog } from './UnsavedChangesDialog';
|
|
20
|
+
export type { UnsavedChangesDialogProps } from './UnsavedChangesDialog';
|
|
21
|
+
export { CompletionDialog } from './CompletionDialog';
|
|
22
|
+
export type { CompletionDialogProps } from './CompletionDialog';
|
|
23
|
+
export { EXAM_DIFFICULTY, ExamViewDialog } from './ExamViewDialog';
|
|
24
|
+
export type { ExamDifficulty, ExamViewDialogExam, ExamViewDialogProps } from './ExamViewDialog';
|
|
25
|
+
export { ExamPreviewDialog } from './ExamPreviewDialog';
|
|
26
|
+
export type { ExamPreviewDialogProps } from './ExamPreviewDialog';
|
|
27
|
+
export { QuestionStatus, clearQuestionStatuses, getQuestionStatus, initializeQuestionStatusesFromAPI, setQuestionStatus } from './exam-question-status';
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export { CreateExamPageContainer } from './CreateExamPageContainer';
|
|
3
|
-
export { CreateExamInfoForm } from './CreateExamInfoForm';
|
|
4
|
-
export { TemplateSelector } from './TemplateSelector';
|
|
5
|
-
export { ExamCreator } from './ExamCreator';
|
|
6
|
-
export { ExamPartTabs } from './ExamPartTabs';
|
|
7
|
-
export { ExamQuestionGrid } from './ExamQuestionGrid';
|
|
8
|
-
export { PartAudioUploader } from './PartAudioUploader';
|
|
9
|
-
export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from './api';
|
|
10
|
-
export { ExamQuestionsPageContainer } from './ExamQuestionsPageContainer';
|
|
1
|
+
export*from"./take";export{CreateExamPageContainer}from"./CreateExamPageContainer";export{CreateExamInfoForm}from"./CreateExamInfoForm";export{TemplateSelector}from"./TemplateSelector";export{ExamCreator}from"./ExamCreator";export{ExamPartTabs}from"./ExamPartTabs";export{ExamQuestionGrid}from"./ExamQuestionGrid";export{PartAudioUploader}from"./PartAudioUploader";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"./api";export{ExamQuestionsPageContainer}from"./ExamQuestionsPageContainer";export{UnsavedChangesDialog}from"./UnsavedChangesDialog";export{CompletionDialog}from"./CompletionDialog";export{EXAM_DIFFICULTY,ExamViewDialog}from"./ExamViewDialog";export{ExamPreviewDialog}from"./ExamPreviewDialog";export{QuestionStatus,clearQuestionStatuses,getQuestionStatus,initializeQuestionStatusesFromAPI,setQuestionStatus}from"./exam-question-status";
|
|
@@ -1,153 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { ArrowLeft, ArrowRight, Loader2, Send } from 'lucide-react';
|
|
5
|
-
import { Button } from '../../../components/ui/button';
|
|
6
|
-
import { CambridgeYleExamLayout, EnglishCertificationExamLayout, SummerSkyLayout } from '../../../components/themes';
|
|
7
|
-
import { useToast } from '../../../shared/lib/hooks/useToast';
|
|
8
|
-
import { GenericQuestionRenderer } from './GenericQuestionRenderer';
|
|
9
|
-
import { groupQuestions, unwrap, unwrapAttempt } from './exam-taking.utils';
|
|
10
|
-
export function ExamTakingPageContainer({ examId, api, exitPath = '/student/exams', onNavigate, onSubmitted }) {
|
|
11
|
-
const { error: errorToast, success } = useToast();
|
|
12
|
-
const [attempt, setAttempt] = useState(null);
|
|
13
|
-
const [parts, setParts] = useState([]);
|
|
14
|
-
const [questionsResult, setQuestionsResult] = useState(null);
|
|
15
|
-
const [partIndex, setPartIndex] = useState(0);
|
|
16
|
-
const [answers, setAnswers] = useState({});
|
|
17
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
let ignore = false;
|
|
21
|
-
setIsLoading(true);
|
|
22
|
-
loadAttempt(api, examId)
|
|
23
|
-
.then(async (loadedAttempt) => {
|
|
24
|
-
const questions = unwrap(await api.getQuestions(loadedAttempt.id));
|
|
25
|
-
if (ignore)
|
|
26
|
-
return;
|
|
27
|
-
setAttempt(loadedAttempt);
|
|
28
|
-
setQuestionsResult(questions);
|
|
29
|
-
setParts(groupQuestions(questions));
|
|
30
|
-
})
|
|
31
|
-
.catch((err) => errorToast({ message: 'Không tải được bài thi', description: err instanceof Error ? err.message : undefined }))
|
|
32
|
-
.finally(() => { if (!ignore)
|
|
33
|
-
setIsLoading(false); });
|
|
34
|
-
return () => { ignore = true; };
|
|
35
|
-
}, [api, examId, errorToast]);
|
|
36
|
-
const currentPart = parts[partIndex];
|
|
37
|
-
const flatQuestions = useMemo(() => parts.flatMap((part) => part.questions), [parts]);
|
|
38
|
-
const currentQuestionIndex = useMemo(() => flatQuestions.findIndex((question) => currentPart?.questions[0]?.id === question.id), [currentPart?.questions, flatQuestions]);
|
|
39
|
-
const answeredCount = useMemo(() => Object.values(answers).filter((value) => value !== '' && value !== null && value !== undefined).length, [answers]);
|
|
40
|
-
const totalQuestions = flatQuestions.length;
|
|
41
|
-
const theme = normalizeTheme(questionsResult?.examTemplate?.theme);
|
|
42
|
-
const examName = questionsResult?.examTemplate?.title ?? 'Làm bài thi';
|
|
43
|
-
const remainingSeconds = getRemainingSeconds(attempt);
|
|
44
|
-
const navigate = (href) => {
|
|
45
|
-
if (onNavigate)
|
|
46
|
-
onNavigate(href);
|
|
47
|
-
else if (typeof window !== 'undefined')
|
|
48
|
-
window.location.href = href;
|
|
49
|
-
};
|
|
50
|
-
const handleAnswerChange = async (questionId, answer) => {
|
|
51
|
-
setAnswers((prev) => ({ ...prev, [questionId]: answer }));
|
|
52
|
-
if (attempt && api.saveAnswers)
|
|
53
|
-
await api.saveAnswers({ attemptId: attempt.id, answers: [{ questionId, answer }] });
|
|
54
|
-
};
|
|
55
|
-
const handleSubmit = async () => {
|
|
56
|
-
if (!attempt)
|
|
57
|
-
return;
|
|
58
|
-
setIsSubmitting(true);
|
|
59
|
-
try {
|
|
60
|
-
await api.submitAttempt(attempt.id);
|
|
61
|
-
success({ message: 'Đã nộp bài' });
|
|
62
|
-
onSubmitted?.(attempt.id);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
errorToast({ message: 'Không nộp được bài', description: err instanceof Error ? err.message : undefined });
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
setIsSubmitting(false);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
if (isLoading)
|
|
72
|
-
return _jsx(Loading, {});
|
|
73
|
-
if (!attempt || !currentPart)
|
|
74
|
-
return _jsx(Empty, { onExit: () => navigate(exitPath) });
|
|
75
|
-
const content = _jsx(QuestionList, { part: currentPart, answers: answers, onAnswerChange: handleAnswerChange });
|
|
76
|
-
const goPrev = () => setPartIndex((prev) => Math.max(0, prev - 1));
|
|
77
|
-
const goNext = () => setPartIndex((prev) => Math.min(parts.length - 1, prev + 1));
|
|
78
|
-
const isFirstPart = partIndex === 0;
|
|
79
|
-
const isLastPart = partIndex >= parts.length - 1;
|
|
80
|
-
if (theme === 'ENGLISH_CERTIFICATION') {
|
|
81
|
-
return (_jsx(EnglishCertificationExamLayout, { headerProps: { examName, remainingSeconds, totalQuestions, answeredCount, isSubmitting, onSubmit: handleSubmit, onClose: () => navigate(exitPath), lastSavedAt: null, isSaving: false, partLabel: currentPart.name, questionRange: getQuestionRange(currentPart) }, navigationProps: { parts: toStoreParts(parts), questions: toStudentQuestions(flatQuestions), answers, flaggedQuestions: new Set(), currentIndex: Math.max(0, currentQuestionIndex), onQuestionClick: (index) => setPartIndex(findPartIndex(parts, index)), onPartClick: setPartIndex }, footerProps: { onPrevious: goPrev, onNext: goNext, onClear: () => undefined, onToggleMark: () => undefined, isPreviousDisabled: isFirstPart, isNextDisabled: isLastPart, isMarked: false }, children: content }));
|
|
82
|
-
}
|
|
83
|
-
if (theme === 'CAMBRIDGE_YLE') {
|
|
84
|
-
return (_jsx(CambridgeYleExamLayout, { onPrevious: goPrev, onNext: goNext, onClose: () => navigate(exitPath), isFirstQuestion: isFirstPart, isLastQuestion: isLastPart, headerProps: { examName, examType: 'Exam', totalQuestions, answeredCount, remainingSeconds, isSubmitting, onSubmit: handleSubmit }, sidebarProps: toCambridgeSidebar(parts, answers, Math.max(0, currentQuestionIndex), setPartIndex), children: content }));
|
|
85
|
-
}
|
|
86
|
-
if (theme === 'SUMMER_SKY') {
|
|
87
|
-
return (_jsx(SummerSkyLayout, { onExit: () => navigate(exitPath), partName: currentPart.name, instruction: currentPart.instructions ?? '', onPrev: goPrev, onNext: goNext, canGoPrev: !isFirstPart, canGoNext: !isLastPart, audioUrl: currentPart.audioUrl ?? '', questionNumber: partIndex + 1, recorder: null, children: content }));
|
|
88
|
-
}
|
|
89
|
-
return _jsx(DefaultLayout, { title: examName, answeredCount: answeredCount, totalQuestions: totalQuestions, onExit: () => navigate(exitPath), onPrev: goPrev, onNext: goNext, isFirst: isFirstPart, isLast: isLastPart, currentPart: currentPart, onSubmit: handleSubmit, isSubmitting: isSubmitting, children: content });
|
|
90
|
-
}
|
|
91
|
-
async function loadAttempt(api, examId) {
|
|
92
|
-
const activeAttempt = unwrap(await api.getActiveAttempt(examId));
|
|
93
|
-
if (activeAttempt)
|
|
94
|
-
return activeAttempt;
|
|
95
|
-
return unwrapAttempt(unwrap(await api.startAttempt(examId)));
|
|
96
|
-
}
|
|
97
|
-
function QuestionList({ part, answers, onAnswerChange }) {
|
|
98
|
-
return _jsx("div", { className: "space-y-4", children: part.questions.map((question) => _jsx(GenericQuestionRenderer, { question: question, value: answers[question.id], onChange: onAnswerChange }, question.id)) });
|
|
99
|
-
}
|
|
100
|
-
function DefaultLayout({ title, answeredCount, totalQuestions, onExit, onPrev, onNext, isFirst, isLast, currentPart, onSubmit, isSubmitting, children }) {
|
|
101
|
-
return (_jsxs("div", { className: "min-h-screen bg-slate-50", children: [_jsx("header", { className: "sticky top-0 z-10 border-b bg-white px-4 py-3 shadow-sm", children: _jsxs("div", { className: "mx-auto flex max-w-5xl items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "font-semibold text-gray-900", children: title }), _jsxs("p", { className: "text-sm text-gray-500", children: ["\u0110\u00E3 tr\u1EA3 l\u1EDDi ", answeredCount, "/", totalQuestions] })] }), _jsx(Button, { variant: "outline", onClick: onExit, children: "Tho\u00E1t" })] }) }), _jsxs("main", { className: "mx-auto max-w-5xl space-y-4 p-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-white p-4 shadow-sm", children: [_jsxs(Button, { variant: "outline", disabled: isFirst, onClick: onPrev, children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Ph\u1EA7n tr\u01B0\u1EDBc"] }), _jsx("div", { className: "text-center", children: _jsx("h2", { className: "font-medium", children: currentPart.name }) }), _jsxs(Button, { variant: "outline", disabled: isLast, onClick: onNext, children: ["Ph\u1EA7n sau", _jsx(ArrowRight, { className: "ml-2 h-4 w-4" })] })] }), children, _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { onClick: onSubmit, disabled: isSubmitting, className: "bg-blue-600 hover:bg-blue-700", children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(Send, { className: "mr-2 h-4 w-4" }), "N\u1ED9p b\u00E0i"] }) })] })] }));
|
|
102
|
-
}
|
|
103
|
-
function normalizeTheme(theme) {
|
|
104
|
-
if (theme === 'ENGLISH_CERTIFICATION')
|
|
105
|
-
return theme;
|
|
106
|
-
if (theme === 'CAMBRIDGE_YLE')
|
|
107
|
-
return theme;
|
|
108
|
-
if (theme === 'SUMMER_SKY')
|
|
109
|
-
return theme;
|
|
110
|
-
return 'DEFAULT';
|
|
111
|
-
}
|
|
112
|
-
function getRemainingSeconds(attempt) {
|
|
113
|
-
if (!attempt)
|
|
114
|
-
return 0;
|
|
115
|
-
const end = new Date(attempt.startedAt).getTime() + attempt.durationMinutes * 60000;
|
|
116
|
-
return Math.max(0, Math.floor((end - Date.now()) / 1000));
|
|
117
|
-
}
|
|
118
|
-
function getQuestionRange(part) {
|
|
119
|
-
const numbers = part.questions.map((question) => question.question_number).filter((value) => Number.isFinite(value));
|
|
120
|
-
if (numbers.length === 0)
|
|
121
|
-
return '';
|
|
122
|
-
return `${Math.min(...numbers)}–${Math.max(...numbers)}`;
|
|
123
|
-
}
|
|
124
|
-
function toStoreParts(parts) {
|
|
125
|
-
let index = 0;
|
|
126
|
-
return parts.map((part) => {
|
|
127
|
-
const startIndex = index;
|
|
128
|
-
index += part.questions.length;
|
|
129
|
-
return { id: part.id, name: part.name, category: part.template?.category, startIndex, endIndex: index - 1, questionCount: part.questions.length, audioUrl: part.audioUrl, isGroup: part.isGroup };
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
function toStudentQuestions(questions) {
|
|
133
|
-
return questions.map((question) => ({ id: question.id, question_type: question.question_type, content: question.content ?? {}, is_example: Boolean(question.is_example), total_points: String(question.total_points ?? 0), part_id: question.part_id ?? '', part_no: question.part_no ?? 0, question_number: question.question_number }));
|
|
134
|
-
}
|
|
135
|
-
function findPartIndex(parts, questionIndex) {
|
|
136
|
-
let cursor = 0;
|
|
137
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
138
|
-
cursor += parts[index].questions.length;
|
|
139
|
-
if (questionIndex < cursor)
|
|
140
|
-
return index;
|
|
141
|
-
}
|
|
142
|
-
return 0;
|
|
143
|
-
}
|
|
144
|
-
function toCambridgeSidebar(parts, answers, currentQuestionGlobalIndex, onPartClick) {
|
|
145
|
-
const questions = parts.flatMap((part, partIndex) => part.questions.map((question, index) => ({ globalIndex: parts.slice(0, partIndex).reduce((sum, item) => sum + item.questions.length, 0) + index, partIndex, questionNumber: question.question_number, isAnswered: answers[question.id] !== undefined && answers[question.id] !== '' })));
|
|
146
|
-
return { parts: parts.map((part, partIndex) => ({ partIndex, partNo: part.partNo ?? partIndex + 1, name: part.name, category: part.template?.category, totalQuestions: part.questions.length, answeredCount: part.questions.filter((question) => answers[question.id] !== undefined && answers[question.id] !== '').length })), questions, currentQuestionGlobalIndex, onPartClick, onQuestionClick: (index) => onPartClick(findPartIndex(parts, index)) };
|
|
147
|
-
}
|
|
148
|
-
function Loading() {
|
|
149
|
-
return _jsx("div", { className: "flex h-screen items-center justify-center", children: _jsx(Loader2, { className: "h-8 w-8 animate-spin text-blue-600" }) });
|
|
150
|
-
}
|
|
151
|
-
function Empty({ onExit }) {
|
|
152
|
-
return _jsxs("div", { className: "flex h-screen flex-col items-center justify-center gap-4", children: [_jsx("p", { children: "Kh\u00F4ng c\u00F3 c\u00E2u h\u1ECFi." }), _jsx(Button, { onClick: onExit, children: "Quay l\u1EA1i" })] });
|
|
153
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useEffect as n,useMemo as i,useState as s}from"react";import{ArrowLeft as a,ArrowRight as r,Loader2 as o,Send as l}from"lucide-react";import{Button as u}from"../../../components/ui/button";import{CambridgeYleExamLayout as c,EnglishCertificationExamLayout as m,SummerSkyLayout as d}from"../../../components/themes";import{useToast as h}from"../../../shared/lib/hooks/useToast";import{GenericQuestionRenderer as p}from"./GenericQuestionRenderer";import{groupQuestions as x,unwrap as g,unwrapAttempt as f}from"./exam-taking.utils";export function ExamTakingPageContainer({examId:t,api:a,exitPath:r="/student/exams",onNavigate:o,onSubmitted:l}){const{error:u,success:p}=h(),[S,E]=s(null),[P,_]=s([]),[M,A]=s(null),[k,Q]=s(0),[T,G]=s({}),[L,j]=s(!0),[R,D]=s(!1);n(()=>{let e=!1;return j(!0),async function(e,t){const n=g(await e.getActiveAttempt(t));return n||f(g(await e.startAttempt(t)))}(a,t).then(async t=>{const n=g(await a.getQuestions(t.id));e||(E(t),A(n),_(x(n)))}).catch(e=>u({message:"Không tải được bài thi",description:e instanceof Error?e.message:void 0})).finally(()=>{e||j(!1)}),()=>{e=!0}},[a,t,u]);const F=P[k],U=i(()=>P.flatMap(e=>e.questions),[P]),K=i(()=>U.findIndex(e=>F?.questions[0]?.id===e.id),[F?.questions,U]),Y=i(()=>Object.values(T).filter(e=>""!==e&&null!=e).length,[T]),B=U.length,O=function(e){return"ENGLISH_CERTIFICATION"===e||"CAMBRIDGE_YLE"===e||"SUMMER_SKY"===e?e:"DEFAULT"}(M?.examTemplate?.theme),H=M?.examTemplate?.title??"Làm bài thi",$=function(e){if(!e)return 0;const t=new Date(e.startedAt).getTime()+6e4*e.durationMinutes;return Math.max(0,Math.floor((t-Date.now())/1e3))}(S),z=e=>{o?o(e):"undefined"!=typeof window&&(window.location.href=e)},J=async()=>{if(S){D(!0);try{await a.submitAttempt(S.id),p({message:"Đã nộp bài"}),l?.(S.id)}catch(e){u({message:"Không nộp được bài",description:e instanceof Error?e.message:void 0})}finally{D(!1)}}};if(L)return e(q,{});if(!S||!F)return e(I,{onExit:()=>z(r)});const V=e(b,{part:F,answers:T,onAnswerChange:async(e,t)=>{G(n=>({...n,[e]:t})),S&&a.saveAnswers&&await a.saveAnswers({attemptId:S.id,answers:[{questionId:e,answer:t}]})}}),W=()=>Q(e=>Math.max(0,e-1)),X=()=>Q(e=>Math.min(P.length-1,e+1)),Z=0===k,ee=k>=P.length-1;return"ENGLISH_CERTIFICATION"===O?e(m,{headerProps:{examName:H,remainingSeconds:$,totalQuestions:B,answeredCount:Y,isSubmitting:R,onSubmit:J,onClose:()=>z(r),lastSavedAt:null,isSaving:!1,partLabel:F.name,questionRange:N(F)},navigationProps:{parts:v(P),questions:(te=U,te.map(e=>({id:e.id,question_type:e.question_type,content:e.content??{},is_example:Boolean(e.is_example),total_points:String(e.total_points??0),part_id:e.part_id??"",part_no:e.part_no??0,question_number:e.question_number}))),answers:T,flaggedQuestions:new Set,currentIndex:Math.max(0,K),onQuestionClick:e=>Q(C(P,e)),onPartClick:Q},footerProps:{onPrevious:W,onNext:X,onClear:()=>{},onToggleMark:()=>{},isPreviousDisabled:Z,isNextDisabled:ee,isMarked:!1},children:V}):"CAMBRIDGE_YLE"===O?e(c,{onPrevious:W,onNext:X,onClose:()=>z(r),isFirstQuestion:Z,isLastQuestion:ee,headerProps:{examName:H,examType:"Exam",totalQuestions:B,answeredCount:Y,remainingSeconds:$,isSubmitting:R,onSubmit:J},sidebarProps:y(P,T,Math.max(0,K),Q),children:V}):"SUMMER_SKY"===O?e(d,{onExit:()=>z(r),partName:F.name,instruction:F.instructions??"",onPrev:W,onNext:X,canGoPrev:!Z,canGoNext:!ee,audioUrl:F.audioUrl??"",questionNumber:k+1,recorder:null,children:V}):e(w,{title:H,answeredCount:Y,totalQuestions:B,onExit:()=>z(r),onPrev:W,onNext:X,isFirst:Z,isLast:ee,currentPart:F,onSubmit:J,isSubmitting:R,children:V});var te}function b({part:t,answers:n,onAnswerChange:i}){return e("div",{className:"space-y-4",children:t.questions.map(t=>e(p,{question:t,value:n[t.id],onChange:i},t.id))})}function w({title:n,answeredCount:i,totalQuestions:s,onExit:c,onPrev:m,onNext:d,isFirst:h,isLast:p,currentPart:x,onSubmit:g,isSubmitting:f,children:b}){return t("div",{className:"min-h-screen bg-slate-50",children:[e("header",{className:"sticky top-0 z-10 border-b bg-white px-4 py-3 shadow-sm",children:t("div",{className:"mx-auto flex max-w-5xl items-center justify-between gap-4",children:[t("div",{children:[e("h1",{className:"font-semibold text-gray-900",children:n}),t("p",{className:"text-sm text-gray-500",children:["Đã trả lời ",i,"/",s]})]}),e(u,{variant:"outline",onClick:c,children:"Thoát"})]})}),t("main",{className:"mx-auto max-w-5xl space-y-4 p-4",children:[t("div",{className:"flex items-center justify-between rounded-lg bg-white p-4 shadow-sm",children:[t(u,{variant:"outline",disabled:h,onClick:m,children:[e(a,{className:"mr-2 h-4 w-4"}),"Phần trước"]}),e("div",{className:"text-center",children:e("h2",{className:"font-medium",children:x.name})}),t(u,{variant:"outline",disabled:p,onClick:d,children:["Phần sau",e(r,{className:"ml-2 h-4 w-4"})]})]}),b,e("div",{className:"flex justify-end",children:t(u,{onClick:g,disabled:f,className:"bg-blue-600 hover:bg-blue-700",children:[f?e(o,{className:"mr-2 h-4 w-4 animate-spin"}):e(l,{className:"mr-2 h-4 w-4"}),"Nộp bài"]})})]})]})}function N(e){const t=e.questions.map(e=>e.question_number).filter(e=>Number.isFinite(e));return 0===t.length?"":`${Math.min(...t)}–${Math.max(...t)}`}function v(e){let t=0;return e.map(e=>{const n=t;return t+=e.questions.length,{id:e.id,name:e.name,category:e.template?.category,startIndex:n,endIndex:t-1,questionCount:e.questions.length,audioUrl:e.audioUrl,isGroup:e.isGroup}})}function C(e,t){let n=0;for(let i=0;i<e.length;i+=1)if(n+=e[i].questions.length,t<n)return i;return 0}function y(e,t,n,i){const s=e.flatMap((n,i)=>n.questions.map((n,s)=>({globalIndex:e.slice(0,i).reduce((e,t)=>e+t.questions.length,0)+s,partIndex:i,questionNumber:n.question_number,isAnswered:void 0!==t[n.id]&&""!==t[n.id]})));return{parts:e.map((e,n)=>({partIndex:n,partNo:e.partNo??n+1,name:e.name,category:e.template?.category,totalQuestions:e.questions.length,answeredCount:e.questions.filter(e=>void 0!==t[e.id]&&""!==t[e.id]).length})),questions:s,currentQuestionGlobalIndex:n,onPartClick:i,onQuestionClick:t=>i(C(e,t))}}function q(){return e("div",{className:"flex h-screen items-center justify-center",children:e(o,{className:"h-8 w-8 animate-spin text-blue-600"})})}function I({onExit:n}){return t("div",{className:"flex h-screen flex-col items-center justify-center gap-4",children:[e("p",{children:"Không có câu hỏi."}),e(u,{onClick:n,children:"Quay lại"})]})}
|