@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,84 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export const listenAndSpeakAnswerRegistration = {
|
|
3
|
-
component: ListenAndSpeakAnswerCreator,
|
|
4
|
-
transformInitialData: (initialData) => {
|
|
5
|
-
let creatorData = initialData?.answer;
|
|
6
|
-
const content = initialData?.content;
|
|
7
|
-
const answer = initialData?.answer || initialData?.correctAnswer || initialData?.correct_answer;
|
|
8
|
-
const points = initialData?.total_points
|
|
9
|
-
? parseFloat(initialData.total_points)
|
|
10
|
-
: (initialData?.points || content?.points || answer?.points || 1);
|
|
11
|
-
const explanation = initialData?.explanation || content?.explanation || answer?.explanation || '';
|
|
12
|
-
if (content && typeof content === 'object' && content.audioUrl !== undefined) {
|
|
13
|
-
const expectedAnswers = answer?.expectedAnswers || [];
|
|
14
|
-
creatorData = {
|
|
15
|
-
audioUrl: content.audioUrl || '',
|
|
16
|
-
expectedAnswers: expectedAnswers,
|
|
17
|
-
arrowNode: content.arrowNode,
|
|
18
|
-
backgroundImageUrl: answer?.backgroundImageUrl || content.backgroundImageUrl || '',
|
|
19
|
-
groupNumber: answer?.groupNumber || content.groupNumber || 1,
|
|
20
|
-
explanation,
|
|
21
|
-
points,
|
|
22
|
-
isBackup: content.isBackup || false,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
else if (answer && typeof answer === 'object' && answer.expectedAnswers !== undefined) {
|
|
26
|
-
creatorData = {
|
|
27
|
-
...answer,
|
|
28
|
-
audioUrl: answer.audioUrl || content?.audioUrl || '',
|
|
29
|
-
arrowNode: answer.arrowNode || content?.arrowNode,
|
|
30
|
-
backgroundImageUrl: answer.backgroundImageUrl || content?.backgroundImageUrl || '',
|
|
31
|
-
groupNumber: answer.groupNumber || content?.groupNumber || 1,
|
|
32
|
-
explanation,
|
|
33
|
-
points,
|
|
34
|
-
isBackup: content?.isBackup || false,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
return creatorData;
|
|
38
|
-
},
|
|
39
|
-
getExtraProps: (ctx) => ({
|
|
40
|
-
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
41
|
-
validationRef: ctx.validationRef,
|
|
42
|
-
groups: ctx.groups,
|
|
43
|
-
questionIndexInPart: ctx.questionIndexInPart,
|
|
44
|
-
partId: ctx.partId,
|
|
45
|
-
}),
|
|
46
|
-
wrapOnSave: (data, ctx) => ({
|
|
47
|
-
type: ctx.questionType,
|
|
48
|
-
content: {
|
|
49
|
-
...(typeof ctx.state.content === 'object' && ctx.state.content !== null ? ctx.state.content : {}),
|
|
50
|
-
audioUrl: data.audioUrl,
|
|
51
|
-
arrowNode: data.arrowNode,
|
|
52
|
-
backgroundImageUrl: data.backgroundImageUrl,
|
|
53
|
-
groupNumber: data.groupNumber,
|
|
54
|
-
isBackup: data.isBackup || false,
|
|
55
|
-
},
|
|
56
|
-
points: data?.points ?? ctx.state.points ?? 1,
|
|
57
|
-
level: ctx.state.level,
|
|
58
|
-
difficulty: ctx.state.difficulty,
|
|
59
|
-
skill: ctx.state.skill,
|
|
60
|
-
answer: {
|
|
61
|
-
expectedAnswers: data.expectedAnswers,
|
|
62
|
-
},
|
|
63
|
-
explanation: data?.explanation || '',
|
|
64
|
-
}),
|
|
65
|
-
wrapOnChange: (data, ctx) => ({
|
|
66
|
-
type: ctx.questionType,
|
|
67
|
-
content: {
|
|
68
|
-
...(typeof ctx.state.content === 'object' && ctx.state.content !== null ? ctx.state.content : {}),
|
|
69
|
-
audioUrl: data.audioUrl,
|
|
70
|
-
arrowNode: data.arrowNode,
|
|
71
|
-
backgroundImageUrl: data.backgroundImageUrl,
|
|
72
|
-
groupNumber: data.groupNumber,
|
|
73
|
-
isBackup: data.isBackup || false,
|
|
74
|
-
},
|
|
75
|
-
points: data?.points ?? ctx.state.points ?? 1,
|
|
76
|
-
level: ctx.state.level,
|
|
77
|
-
difficulty: ctx.state.difficulty,
|
|
78
|
-
skill: ctx.state.skill,
|
|
79
|
-
answer: {
|
|
80
|
-
expectedAnswers: data.expectedAnswers,
|
|
81
|
-
},
|
|
82
|
-
explanation: data?.explanation || '',
|
|
83
|
-
}),
|
|
84
|
-
};
|
|
1
|
+
import{ListenAndSpeakAnswerCreator as e}from"./ListenAndSpeakAnswerCreator";export const listenAndSpeakAnswerRegistration={component:e,transformInitialData:e=>{let t=e?.answer;const o=e?.content,a=e?.answer||e?.correctAnswer||e?.correct_answer,n=e?.total_points?parseFloat(e.total_points):e?.points||o?.points||a?.points||1,r=e?.explanation||o?.explanation||a?.explanation||"";if(o&&"object"==typeof o&&void 0!==o.audioUrl){const e=a?.expectedAnswers||[];t={audioUrl:o.audioUrl||"",expectedAnswers:e,arrowNode:o.arrowNode,backgroundImageUrl:a?.backgroundImageUrl||o.backgroundImageUrl||"",groupNumber:a?.groupNumber||o.groupNumber||1,explanation:r,points:n,isBackup:o.isBackup||!1}}else a&&"object"==typeof a&&void 0!==a.expectedAnswers&&(t={...a,audioUrl:a.audioUrl||o?.audioUrl||"",arrowNode:a.arrowNode||o?.arrowNode,backgroundImageUrl:a.backgroundImageUrl||o?.backgroundImageUrl||"",groupNumber:a.groupNumber||o?.groupNumber||1,explanation:r,points:n,isBackup:o?.isBackup||!1});return t},getExtraProps:e=>({onUnsavedChangesChange:e.onUnsavedChangesChange,validationRef:e.validationRef,groups:e.groups,questionIndexInPart:e.questionIndexInPart,partId:e.partId}),wrapOnSave:(e,t)=>({type:t.questionType,content:{..."object"==typeof t.state.content&&null!==t.state.content?t.state.content:{},audioUrl:e.audioUrl,arrowNode:e.arrowNode,backgroundImageUrl:e.backgroundImageUrl,groupNumber:e.groupNumber,isBackup:e.isBackup||!1},points:e?.points??t.state.points??1,level:t.state.level,difficulty:t.state.difficulty,skill:t.state.skill,answer:{expectedAnswers:e.expectedAnswers},explanation:e?.explanation||""}),wrapOnChange:(e,t)=>({type:t.questionType,content:{..."object"==typeof t.state.content&&null!==t.state.content?t.state.content:{},audioUrl:e.audioUrl,arrowNode:e.arrowNode,backgroundImageUrl:e.backgroundImageUrl,groupNumber:e.groupNumber,isBackup:e.isBackup||!1},points:e?.points??t.state.points??1,level:t.state.level,difficulty:t.state.difficulty,skill:t.state.skill,answer:{expectedAnswers:e.expectedAnswers},explanation:e?.explanation||""})};
|
|
@@ -1,27 +1 @@
|
|
|
1
|
-
export const transformListenAndSpeakAnswer = (
|
|
2
|
-
const contentData = question.content;
|
|
3
|
-
const answerData = question.answer;
|
|
4
|
-
const questionGroup = question.questionGroup;
|
|
5
|
-
// No strict early return - let validation handle missing expectedAnswers
|
|
6
|
-
const expectedAnswers = answerData?.expectedAnswers || [];
|
|
7
|
-
const apiContent = {
|
|
8
|
-
audioUrl: contentData?.audioUrl || answerData?.audioUrl || '',
|
|
9
|
-
arrowNode: contentData?.arrowNode || answerData?.arrowNode || { x: 40, y: 30, width: 10, height: 20 },
|
|
10
|
-
isBackup: contentData?.isBackup || answerData?.isBackup || false,
|
|
11
|
-
};
|
|
12
|
-
const result = {
|
|
13
|
-
apiContent,
|
|
14
|
-
apiCorrectAnswer: {
|
|
15
|
-
expectedAnswers: expectedAnswers,
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
if (contentData?.backgroundImageUrl) {
|
|
19
|
-
result.sceneData = { backgroundImageUrl: contentData.backgroundImageUrl };
|
|
20
|
-
result.groupNumber = contentData.groupNumber || questionGroup?.groupNumber || 1;
|
|
21
|
-
}
|
|
22
|
-
else if (questionGroup?.payload) {
|
|
23
|
-
result.sceneData = questionGroup.payload;
|
|
24
|
-
result.groupNumber = questionGroup.groupNumber;
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
27
|
-
};
|
|
1
|
+
export const transformListenAndSpeakAnswer=r=>{const e=r.content,a=r.answer,o=r.questionGroup,u=a?.expectedAnswers||[],n={apiContent:{audioUrl:e?.audioUrl||a?.audioUrl||"",arrowNode:e?.arrowNode||a?.arrowNode||{x:40,y:30,width:10,height:20},isBackup:e?.isBackup||a?.isBackup||!1},apiCorrectAnswer:{expectedAnswers:u}};return e?.backgroundImageUrl?(n.sceneData={backgroundImageUrl:e.backgroundImageUrl},n.groupNumber=e.groupNumber||o?.groupNumber||1):o?.payload&&(n.sceneData=o.payload,n.groupNumber=o.groupNumber),n};
|
|
@@ -1,32 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Mic, Volume2, CheckCircle2, Play, Pause, ImageIcon, Info, GitCompareArrows } from 'lucide-react';
|
|
4
|
-
import { Button } from '../../../../components/ui/button';
|
|
5
|
-
import { useCompareImagesGroup } from '../../_shared/hooks/useCompareImagesGroup';
|
|
6
|
-
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
7
|
-
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
8
|
-
function QuestionAudioPlayer({ audioUrl }) {
|
|
9
|
-
const { previewUrl } = usePresignedFileUrl(audioUrl);
|
|
10
|
-
const { isPlaying, currentTime, duration, progressPercentage, handlePlayPause, handleProgressClick, formatTime, } = useAudioPreview(previewUrl);
|
|
11
|
-
return (_jsxs("div", { className: "flex items-center gap-3 rounded-lg bg-gradient-to-r from-indigo-50 to-purple-50 border border-indigo-200 px-4 py-2.5", children: [_jsx(Button, { variant: "outline", size: "icon", onClick: handlePlayPause, disabled: !previewUrl, className: "h-8 w-8 border-indigo-300 text-indigo-600 hover:bg-indigo-100", children: isPlaying ? _jsx(Pause, { className: "h-3.5 w-3.5" }) : _jsx(Play, { className: "h-3.5 w-3.5" }) }), _jsxs("div", { className: "text-muted-foreground min-w-[70px] text-xs", children: [formatTime(currentTime), " / ", formatTime(duration)] }), _jsx("div", { className: "bg-indigo-200 relative h-1.5 flex-1 cursor-pointer rounded-full", onClick: handleProgressClick, children: _jsx("div", { className: "bg-indigo-500 absolute top-0 left-0 h-full rounded-full transition-all", style: { width: `${progressPercentage}%` } }) }), _jsx(Volume2, { className: "h-4 w-4 text-indigo-400" })] }));
|
|
12
|
-
}
|
|
13
|
-
// ─── Sub-component: Presigned image preview ──────────────────────────────────
|
|
14
|
-
function CompareImagePreview({ imageUrl, label }) {
|
|
15
|
-
const { previewUrl } = usePresignedFileUrl(imageUrl);
|
|
16
|
-
if (!previewUrl) {
|
|
17
|
-
return (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-gray-100 text-gray-400", children: _jsx(ImageIcon, { className: "h-8 w-8" }) }));
|
|
18
|
-
}
|
|
19
|
-
return (_jsx("img", { src: previewUrl, alt: label, className: "object-contain w-full h-full" }));
|
|
20
|
-
}
|
|
21
|
-
// ─── Main Client Component ───────────────────────────────────────────────────
|
|
22
|
-
export function ListenAndSpeakCompareImagesClient({ questionData, isReviewMode = false, }) {
|
|
23
|
-
const { audioUrl, expectedAnswers = [], explanation } = questionData;
|
|
24
|
-
// Retrieve shared group data which should have been populated by the store when the exam loaded
|
|
25
|
-
const { candidateImageUrl: storeCandidateImageUrl, examinerImageUrl: storeExaminerImageUrl } = useCompareImagesGroup(undefined, // partId not needed for read-only client
|
|
26
|
-
questionData.groupNumber);
|
|
27
|
-
// Use questionData images as primary source (Creator preview passes them directly),
|
|
28
|
-
// fall back to store values (exam-taking mode populates the store)
|
|
29
|
-
const candidateImageUrl = questionData.candidateImageUrl || storeCandidateImageUrl;
|
|
30
|
-
const examinerImageUrl = questionData.examinerImageUrl || storeExaminerImageUrl;
|
|
31
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-indigo-50 to-blue-50 px-5 py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-white shadow-sm", children: _jsx(GitCompareArrows, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Nghe v\u00E0 n\u00F3i (So s\u00E1nh h\u00ECnh \u1EA3nh)" })] }) }), _jsxs("div", { className: "p-5 space-y-5", children: [audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), (candidateImageUrl || examinerImageUrl) && (_jsxs("div", { className: "space-y-4 bg-gradient-to-br from-amber-100 to-orange-100 p-5 rounded-2xl border border-amber-200 shadow-sm", children: [candidateImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-amber-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-amber-300/50", children: "Candidate's Copy" }) }), _jsx("div", { className: "w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200", children: _jsx(CompareImagePreview, { imageUrl: candidateImageUrl, label: "Candidate's Copy" }) })] })), examinerImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-orange-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-orange-300/50", children: "Examiner's Copy" }) }), _jsx("div", { className: "w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200", children: _jsx(CompareImagePreview, { imageUrl: examinerImageUrl, label: "Examiner's Copy" }) })] }))] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh s\u1EBD nh\u1EA5n n\u00FAt ghi \u00E2m v\u00E0 n\u00F3i c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y" })] }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: explanation })] }))] })] })] }));
|
|
32
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{Mic as i,Volume2 as a,CheckCircle2 as d,Play as n,Pause as s,ImageIcon as l,Info as t,GitCompareArrows as o}from"lucide-react";import{Button as c}from"../../../../components/ui/button";import{useCompareImagesGroup as m}from"../../_shared/hooks/useCompareImagesGroup";import{usePresignedFileUrl as g,useAudioPreview as h}from"../../../../shared/lib/hooks";function b({audioUrl:i}){const{previewUrl:d}=g(i),{isPlaying:l,currentTime:t,duration:o,progressPercentage:m,handlePlayPause:b,handleProgressClick:x,formatTime:p}=h(d);return r("div",{className:"flex items-center gap-3 rounded-lg bg-gradient-to-r from-indigo-50 to-purple-50 border border-indigo-200 px-4 py-2.5",children:[e(c,{variant:"outline",size:"icon",onClick:b,disabled:!d,className:"h-8 w-8 border-indigo-300 text-indigo-600 hover:bg-indigo-100",children:e(l?s:n,{className:"h-3.5 w-3.5"})}),r("div",{className:"text-muted-foreground min-w-[70px] text-xs",children:[p(t)," / ",p(o)]}),e("div",{className:"bg-indigo-200 relative h-1.5 flex-1 cursor-pointer rounded-full",onClick:x,children:e("div",{className:"bg-indigo-500 absolute top-0 left-0 h-full rounded-full transition-all",style:{width:`${m}%`}})}),e(a,{className:"h-4 w-4 text-indigo-400"})]})}function x({imageUrl:r,label:i}){const{previewUrl:a}=g(r);return a?e("img",{src:a,alt:i,className:"object-contain w-full h-full"}):e("div",{className:"flex h-full w-full items-center justify-center bg-gray-100 text-gray-400",children:e(l,{className:"h-8 w-8"})})}export function ListenAndSpeakCompareImagesClient({questionData:n,isReviewMode:s=!1}){const{audioUrl:l,expectedAnswers:c=[],explanation:g}=n,{candidateImageUrl:h,examinerImageUrl:p}=m(void 0,n.groupNumber),u=n.candidateImageUrl||h,f=n.examinerImageUrl||p;return r("div",{className:"space-y-4",children:[n.isBackup&&r("div",{className:"inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm",children:[e("span",{children:"🔄"})," Câu hỏi dự phòng"]}),r("div",{className:"overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm",children:[e("div",{className:"flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-indigo-50 to-blue-50 px-5 py-3",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-white shadow-sm",children:e(o,{className:"h-4 w-4"})}),e("span",{className:"text-sm font-medium text-gray-700",children:"Nghe và nói (So sánh hình ảnh)"})]})}),r("div",{className:"p-5 space-y-5",children:[l?e(b,{audioUrl:l}):r("div",{className:"flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3",children:[e(a,{className:"h-4 w-4 text-gray-400"}),e("span",{className:"text-xs text-gray-400",children:"Chưa có audio câu hỏi"})]}),(u||f)&&r("div",{className:"space-y-4 bg-gradient-to-br from-amber-100 to-orange-100 p-5 rounded-2xl border border-amber-200 shadow-sm",children:[u&&r("div",{className:"space-y-2",children:[e("div",{className:"flex justify-center",children:e("span",{className:"inline-block rounded-lg border-2 border-amber-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-amber-300/50",children:"Candidate's Copy"})}),e("div",{className:"w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200",children:e(x,{imageUrl:u,label:"Candidate's Copy"})})]}),f&&r("div",{className:"space-y-2",children:[e("div",{className:"flex justify-center",children:e("span",{className:"inline-block rounded-lg border-2 border-orange-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-orange-300/50",children:"Examiner's Copy"})}),e("div",{className:"w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200",children:e(x,{imageUrl:f,label:"Examiner's Copy"})})]})]}),r("div",{className:"flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50",children:e(i,{className:"w-4 h-4 text-indigo-400"})}),e("p",{className:"text-xs text-gray-400",children:"Học sinh sẽ nhấn nút ghi âm và nói câu trả lời tại đây"})]}),s&&c.length>0&&r("div",{className:"rounded-xl border border-green-200 bg-green-50/80 p-4",children:[r("div",{className:"flex items-center gap-2 mb-3",children:[e(d,{className:"h-4 w-4 text-green-600"}),e("h4",{className:"text-sm font-semibold text-green-800",children:"Các đáp án mong muốn"})]}),e("div",{className:"space-y-2",children:c.map((i,a)=>r("div",{className:"flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2",children:[e("span",{className:"flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold",children:a+1}),e("span",{className:"text-sm text-green-800",children:i})]},a))})]}),s&&g&&r("div",{className:"rounded-xl border border-indigo-200 bg-indigo-50 p-4",children:[r("div",{className:"flex items-center gap-2 mb-2",children:[e(t,{className:"h-4 w-4 text-indigo-600"}),e("p",{className:"text-sm font-semibold text-indigo-800",children:"Giải thích"})]}),e("p",{className:"text-sm text-indigo-700 leading-relaxed",children:g})]})]})]})]})}
|
|
@@ -1,295 +1 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
5
|
-
import { Label } from '../../../../components/ui/label';
|
|
6
|
-
import { Button } from '../../../../components/ui/button';
|
|
7
|
-
import { Input } from '../../../../components/ui/input';
|
|
8
|
-
import { Textarea } from '../../../../components/ui/textarea';
|
|
9
|
-
import { Form, FormControl, FormField, FormItem, FormMessage } from '../../../../components/ui/form';
|
|
10
|
-
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
11
|
-
import { PointsInput } from '../../../../components/ui/points-input';
|
|
12
|
-
// Stores & hooks
|
|
13
|
-
import { useCompareImagesGroup } from '../../_shared/hooks/useCompareImagesGroup';
|
|
14
|
-
import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
|
|
15
|
-
import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
16
|
-
import { listenAndSpeakCompareImagesSchema } from '../../../../shared/lib/validation/listen-and-speak-compare-images.validation';
|
|
17
|
-
// Icons
|
|
18
|
-
import { Plus, Trash2, Mic, Volume2, ImageIcon, MessageSquareText, ListChecks, BookOpen, Eye, Pencil, Info } from 'lucide-react';
|
|
19
|
-
import { CreatorGuide } from '../../components/CreatorGuide';
|
|
20
|
-
import { ListenAndSpeakCompareImagesClient } from './ListenAndSpeakCompareImagesClient';
|
|
21
|
-
// ─── Sub-component: Image preview with presigned URL ─────────────────────────
|
|
22
|
-
function ImagePreview({ imageUrl, label }) {
|
|
23
|
-
const { previewUrl } = usePresignedFileUrl(imageUrl);
|
|
24
|
-
if (!previewUrl)
|
|
25
|
-
return null;
|
|
26
|
-
return (_jsx("div", { className: "mt-2 rounded-xl border border-gray-200 overflow-hidden bg-white shadow-sm", children: _jsx("img", { src: previewUrl, alt: label, className: "h-72 w-full object-contain bg-gray-50 p-2" }) }));
|
|
27
|
-
}
|
|
28
|
-
export function ListenAndSpeakCompareImagesCreator({ questionIndex, initialData, onChange, onUnsavedChangesChange, validationRef, isFirstInGroup: isFirstInGroupProp, partId, groups, questionIndexInPart, }) {
|
|
29
|
-
// ---------------------------------------------------------------
|
|
30
|
-
// Determine if this is the first question in the group
|
|
31
|
-
// ---------------------------------------------------------------
|
|
32
|
-
const isFirstInGroup = useMemo(() => {
|
|
33
|
-
if (isFirstInGroupProp !== undefined)
|
|
34
|
-
return isFirstInGroupProp;
|
|
35
|
-
if (!groups || !groups.length || questionIndexInPart === undefined)
|
|
36
|
-
return true;
|
|
37
|
-
let cumulative = 0;
|
|
38
|
-
for (const group of groups) {
|
|
39
|
-
if (questionIndexInPart === cumulative)
|
|
40
|
-
return true;
|
|
41
|
-
cumulative += group.questionCount;
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
}, [isFirstInGroupProp, groups, questionIndexInPart]);
|
|
45
|
-
// ---------------------------------------------------------------
|
|
46
|
-
// 1. Group level state (Shared Compare Images)
|
|
47
|
-
// ---------------------------------------------------------------
|
|
48
|
-
const computedGroupNumber = useMemo(() => {
|
|
49
|
-
if (groups && groups.length > 0) {
|
|
50
|
-
if (questionIndexInPart === undefined)
|
|
51
|
-
return 1;
|
|
52
|
-
let cumulative = 0;
|
|
53
|
-
for (let i = 0; i < groups.length; i++) {
|
|
54
|
-
if (questionIndexInPart < cumulative + groups[i].questionCount) {
|
|
55
|
-
return i + 1;
|
|
56
|
-
}
|
|
57
|
-
cumulative += groups[i].questionCount;
|
|
58
|
-
}
|
|
59
|
-
return 1;
|
|
60
|
-
}
|
|
61
|
-
// Fallback: use groupNumber from initialData when groups prop is not yet available
|
|
62
|
-
const apiData = initialData;
|
|
63
|
-
return apiData?.groupNumber ?? apiData?.questionGroup?.groupNumber ?? undefined;
|
|
64
|
-
}, [groups, questionIndexInPart, initialData]);
|
|
65
|
-
const apiData = initialData;
|
|
66
|
-
const { candidateImageUrl, examinerImageUrl, updateCandidateImage, updateExaminerImage, isValid: isGroupHookValid, } = useCompareImagesGroup(partId, computedGroupNumber, {
|
|
67
|
-
candidateImageUrl: apiData?.candidateImageUrl,
|
|
68
|
-
examinerImageUrl: apiData?.examinerImageUrl,
|
|
69
|
-
});
|
|
70
|
-
// Group image validation errors (inline under each input)
|
|
71
|
-
const [groupImageErrors, setGroupImageErrors] = useState({});
|
|
72
|
-
// ---------------------------------------------------------------
|
|
73
|
-
// 2. Question level form data
|
|
74
|
-
// ---------------------------------------------------------------
|
|
75
|
-
const form = useReactHookForm({
|
|
76
|
-
zodSchema: listenAndSpeakCompareImagesSchema,
|
|
77
|
-
defaultValues: {
|
|
78
|
-
candidateImageUrl: apiData?.candidateImageUrl || candidateImageUrl || '',
|
|
79
|
-
examinerImageUrl: apiData?.examinerImageUrl || examinerImageUrl || '',
|
|
80
|
-
audioUrl: apiData?.audioUrl || '',
|
|
81
|
-
expectedAnswers: apiData?.expectedAnswers?.length > 0 ? apiData.expectedAnswers : [''],
|
|
82
|
-
points: apiData?.points ?? 1,
|
|
83
|
-
explanation: apiData?.explanation || '',
|
|
84
|
-
isBackup: apiData?.isBackup || false,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
const { control, watch, setValue, trigger, clearErrors } = form;
|
|
88
|
-
const watchedAudioUrl = watch('audioUrl');
|
|
89
|
-
const watchedExpectedAnswers = watch('expectedAnswers');
|
|
90
|
-
const watchedPoints = watch('points');
|
|
91
|
-
const watchedExplanation = watch('explanation');
|
|
92
|
-
const watchedIsBackup = watch('isBackup');
|
|
93
|
-
// ---------------------------------------------------------------
|
|
94
|
-
// Refs to prevent infinite loops
|
|
95
|
-
// ---------------------------------------------------------------
|
|
96
|
-
const previousInitialDataRef = useRef(initialData);
|
|
97
|
-
const isInitializingRef = useRef(false);
|
|
98
|
-
const onChangeRef = useRef(onChange);
|
|
99
|
-
const previousPayloadRef = useRef('');
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
onChangeRef.current = onChange;
|
|
102
|
-
}, [onChange]);
|
|
103
|
-
// Debounced onChange
|
|
104
|
-
const debouncedOnChange = useDebouncedCallback((data) => {
|
|
105
|
-
onChangeRef.current?.(data);
|
|
106
|
-
}, 300);
|
|
107
|
-
// ---------------------------------------------------------------
|
|
108
|
-
// Connect to external validation trigger
|
|
109
|
-
// ---------------------------------------------------------------
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
if (validationRef) {
|
|
112
|
-
validationRef.current = {
|
|
113
|
-
triggerValidation: async () => {
|
|
114
|
-
// If first in group, also validate that images are set
|
|
115
|
-
if (isFirstInGroup) {
|
|
116
|
-
const hasCandidate = !!candidateImageUrl;
|
|
117
|
-
const hasExaminer = !!examinerImageUrl;
|
|
118
|
-
const errors = {};
|
|
119
|
-
if (!hasCandidate)
|
|
120
|
-
errors.candidate = "Vui lòng tải lên Candidate's Copy Image!";
|
|
121
|
-
if (!hasExaminer)
|
|
122
|
-
errors.examiner = "Vui lòng tải lên Examiner's Copy Image!";
|
|
123
|
-
setGroupImageErrors(errors);
|
|
124
|
-
if (!hasCandidate || !hasExaminer) {
|
|
125
|
-
// Also trigger form validation to show other errors
|
|
126
|
-
await trigger();
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
setGroupImageErrors({});
|
|
132
|
-
}
|
|
133
|
-
return await trigger();
|
|
134
|
-
},
|
|
135
|
-
getFormData: () => ({
|
|
136
|
-
...form.getValues(),
|
|
137
|
-
candidateImageUrl: candidateImageUrl || '',
|
|
138
|
-
examinerImageUrl: examinerImageUrl || '',
|
|
139
|
-
groupNumber: isFirstInGroup ? displayGroupNumber : undefined,
|
|
140
|
-
}),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}, [validationRef, form, trigger, isFirstInGroup, candidateImageUrl, examinerImageUrl, setGroupImageErrors]);
|
|
144
|
-
// Clear group image errors when images are uploaded
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (candidateImageUrl && groupImageErrors.candidate) {
|
|
147
|
-
setGroupImageErrors(prev => ({ ...prev, candidate: undefined }));
|
|
148
|
-
}
|
|
149
|
-
if (examinerImageUrl && groupImageErrors.examiner) {
|
|
150
|
-
setGroupImageErrors(prev => ({ ...prev, examiner: undefined }));
|
|
151
|
-
}
|
|
152
|
-
}, [candidateImageUrl, examinerImageUrl]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
153
|
-
// ---------------------------------------------------------------
|
|
154
|
-
// Sync form values if initialData props change
|
|
155
|
-
// ---------------------------------------------------------------
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
const normalizedInitialData = {
|
|
158
|
-
candidateImageUrl: apiData?.candidateImageUrl || '',
|
|
159
|
-
examinerImageUrl: apiData?.examinerImageUrl || '',
|
|
160
|
-
audioUrl: apiData?.audioUrl || '',
|
|
161
|
-
expectedAnswers: apiData?.expectedAnswers?.length > 0 ? apiData.expectedAnswers : [''],
|
|
162
|
-
points: apiData?.points ?? 1,
|
|
163
|
-
explanation: apiData?.explanation || '',
|
|
164
|
-
isBackup: apiData?.isBackup || false,
|
|
165
|
-
};
|
|
166
|
-
const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
|
|
167
|
-
if (!initialDataChanged)
|
|
168
|
-
return;
|
|
169
|
-
previousInitialDataRef.current = initialData;
|
|
170
|
-
const currentFormData = {
|
|
171
|
-
audioUrl: watchedAudioUrl,
|
|
172
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
173
|
-
points: watchedPoints,
|
|
174
|
-
explanation: watchedExplanation,
|
|
175
|
-
isBackup: watchedIsBackup,
|
|
176
|
-
};
|
|
177
|
-
const isEchoFromOnChange = currentFormData.points === normalizedInitialData.points &&
|
|
178
|
-
currentFormData.explanation === normalizedInitialData.explanation &&
|
|
179
|
-
currentFormData.audioUrl === normalizedInitialData.audioUrl &&
|
|
180
|
-
JSON.stringify(currentFormData.expectedAnswers) === JSON.stringify(normalizedInitialData.expectedAnswers);
|
|
181
|
-
const wasJustSaved = initialData?.isCompleted === true;
|
|
182
|
-
if (isEchoFromOnChange && wasJustSaved)
|
|
183
|
-
return;
|
|
184
|
-
const shouldReset = !isEchoFromOnChange &&
|
|
185
|
-
(normalizedInitialData.audioUrl !== currentFormData.audioUrl ||
|
|
186
|
-
JSON.stringify(normalizedInitialData.expectedAnswers) !== JSON.stringify(currentFormData.expectedAnswers));
|
|
187
|
-
if (shouldReset) {
|
|
188
|
-
isInitializingRef.current = true;
|
|
189
|
-
form.reset(normalizedInitialData);
|
|
190
|
-
setTimeout(() => {
|
|
191
|
-
isInitializingRef.current = false;
|
|
192
|
-
}, 150);
|
|
193
|
-
}
|
|
194
|
-
}, [initialData, form, watchedAudioUrl, watchedExpectedAnswers, watchedExplanation, watchedPoints, watchedIsBackup]);
|
|
195
|
-
// ---------------------------------------------------------------
|
|
196
|
-
// Fire onChange when form values change
|
|
197
|
-
// ---------------------------------------------------------------
|
|
198
|
-
const watchedExpectedAnswersString = JSON.stringify(watchedExpectedAnswers);
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
if (isInitializingRef.current) {
|
|
201
|
-
const currentPayload = JSON.stringify({
|
|
202
|
-
audioUrl: watchedAudioUrl,
|
|
203
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
204
|
-
points: watchedPoints,
|
|
205
|
-
explanation: watchedExplanation,
|
|
206
|
-
isBackup: watchedIsBackup,
|
|
207
|
-
candidateImageUrl,
|
|
208
|
-
examinerImageUrl,
|
|
209
|
-
});
|
|
210
|
-
previousPayloadRef.current = currentPayload;
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const payload = {
|
|
214
|
-
audioUrl: watchedAudioUrl,
|
|
215
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
216
|
-
points: watchedPoints,
|
|
217
|
-
explanation: watchedExplanation,
|
|
218
|
-
isBackup: watchedIsBackup,
|
|
219
|
-
candidateImageUrl,
|
|
220
|
-
examinerImageUrl,
|
|
221
|
-
};
|
|
222
|
-
const payloadString = JSON.stringify(payload);
|
|
223
|
-
if (previousPayloadRef.current !== payloadString) {
|
|
224
|
-
previousPayloadRef.current = payloadString;
|
|
225
|
-
debouncedOnChange(payload);
|
|
226
|
-
}
|
|
227
|
-
}, [
|
|
228
|
-
watchedAudioUrl,
|
|
229
|
-
watchedExpectedAnswersString,
|
|
230
|
-
watchedPoints,
|
|
231
|
-
watchedExplanation,
|
|
232
|
-
watchedIsBackup,
|
|
233
|
-
candidateImageUrl,
|
|
234
|
-
examinerImageUrl,
|
|
235
|
-
debouncedOnChange,
|
|
236
|
-
]);
|
|
237
|
-
// Track unsaved changes
|
|
238
|
-
useEffect(() => {
|
|
239
|
-
if (onUnsavedChangesChange) {
|
|
240
|
-
onUnsavedChangesChange(form.formState.isDirty);
|
|
241
|
-
}
|
|
242
|
-
}, [form.formState.isDirty, onUnsavedChangesChange]);
|
|
243
|
-
// ---------------------------------------------------------------
|
|
244
|
-
// Handlers
|
|
245
|
-
// ---------------------------------------------------------------
|
|
246
|
-
const addExpectedAnswer = () => {
|
|
247
|
-
const currentAnswers = form.getValues('expectedAnswers');
|
|
248
|
-
form.setValue('expectedAnswers', [...currentAnswers, ''], { shouldValidate: true, shouldDirty: true });
|
|
249
|
-
};
|
|
250
|
-
const handleExpectedAnswerChange = (index, value) => {
|
|
251
|
-
const newAnswers = [...watchedExpectedAnswers];
|
|
252
|
-
newAnswers[index] = value;
|
|
253
|
-
form.setValue('expectedAnswers', newAnswers, { shouldValidate: true, shouldDirty: true });
|
|
254
|
-
};
|
|
255
|
-
const removeExpectedAnswer = (index) => {
|
|
256
|
-
if (watchedExpectedAnswers.length > 1) {
|
|
257
|
-
const newAnswers = watchedExpectedAnswers.filter((_, i) => i !== index);
|
|
258
|
-
form.setValue('expectedAnswers', newAnswers, { shouldValidate: true, shouldDirty: true });
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
// Compute display question number safely
|
|
262
|
-
const displayQuestionNumber = typeof questionIndex === 'number' && !isNaN(questionIndex)
|
|
263
|
-
? questionIndex + 1
|
|
264
|
-
: (typeof questionIndexInPart === 'number' && !isNaN(questionIndexInPart)
|
|
265
|
-
? questionIndexInPart + 1
|
|
266
|
-
: '?');
|
|
267
|
-
// Compute group number for display (reuse the computed group number)
|
|
268
|
-
const displayGroupNumber = computedGroupNumber ?? 1;
|
|
269
|
-
// ---------------------------------------------------------------
|
|
270
|
-
// Preview mode
|
|
271
|
-
// ---------------------------------------------------------------
|
|
272
|
-
const [isClientMode, setIsClientMode] = useState(false);
|
|
273
|
-
if (isClientMode) {
|
|
274
|
-
const questionData = {
|
|
275
|
-
audioUrl: watchedAudioUrl,
|
|
276
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
277
|
-
explanation: watchedExplanation,
|
|
278
|
-
points: watchedPoints,
|
|
279
|
-
isBackup: watchedIsBackup,
|
|
280
|
-
groupNumber: displayGroupNumber,
|
|
281
|
-
candidateImageUrl: candidateImageUrl || '',
|
|
282
|
-
examinerImageUrl: examinerImageUrl || '',
|
|
283
|
-
};
|
|
284
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Nghe v\u00E0 n\u00F3i \u2014 So s\u00E1nh h\u00ECnh \u1EA3nh (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Preview mode" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(ListenAndSpeakCompareImagesClient, { questionData: questionData, isReviewMode: true })] }));
|
|
285
|
-
}
|
|
286
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsx(CreatorGuide, { colorScheme: "blue", steps: [
|
|
287
|
-
{ text: 'Upload 2 hình ảnh so sánh: Hình thí sinh (Candidate) và Hình giám khảo (Examiner) — dùng chung cho nhóm' },
|
|
288
|
-
{ text: 'Câu hỏi đầu tiên trong nhóm sẽ quản lý hình ảnh chung, các câu sau tham chiếu' },
|
|
289
|
-
{ text: 'Upload audio câu hỏi cho từng câu — giám khảo hỏi về sự khác biệt giữa 2 hình' },
|
|
290
|
-
{ text: 'Nhập các đáp án mong muốn mô tả sự khác biệt', highlight: true },
|
|
291
|
-
{ text: 'Thêm giải thích cho từng điểm khác biệt' },
|
|
292
|
-
], tips: ['Chọn hình có sự khác biệt rõ ràng nhưng không quá dễ phát hiện'] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 shadow-md", children: _jsx(ListChecks, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl font-bold text-gray-900", children: "Nh\u00F3m \u0111\u00E1p \u00E1n" }), _jsxs("div", { className: "mt-1.5 inline-flex items-center gap-1.5 rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-700", children: [_jsx("span", { children: "\uD83D\uDDBC\uFE0F" }), _jsxs("span", { children: ["Nh\u00F3m ", displayGroupNumber] }), _jsx("span", { className: "text-xs font-normal opacity-75 ml-1", children: "(D\u00F9ng chung)" })] })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !watchedAudioUrl || watchedExpectedAnswers.filter((a) => a.trim()).length === 0, className: "gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 hover:text-blue-700", children: [_jsx(Eye, { className: "h-4 w-4" }), "Preview"] })] }) })] }), isFirstInGroup ? (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-amber-400 to-orange-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-100", children: _jsx(ImageIcon, { className: "h-3.5 w-3.5 text-amber-600" }) }), _jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "H\u00ECnh \u1EA3nh so s\u00E1nh" })] }) }), _jsx(CardContent, { className: "space-y-4", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-sm font-semibold text-gray-800", children: ["\uD83D\uDCF7 Candidate's Copy Image ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: `candidate-img-group-${displayGroupNumber}`, label: "", accept: "image/*", value: candidateImageUrl || '', onChange: (url) => updateCandidateImage(url), maxSize: 5, placeholder: "Upload Candidate's Copy", autoUpload: true, prefix: "questions/compare-images" }), candidateImageUrl && (_jsx(ImagePreview, { imageUrl: candidateImageUrl, label: "Candidate's Copy" })), !candidateImageUrl && (_jsxs("div", { className: `flex flex-col items-center justify-center rounded-xl border-2 border-dashed ${groupImageErrors.candidate ? 'border-red-300 bg-red-50/50' : 'border-amber-200 bg-amber-50/50'} py-6 px-4 text-center`, children: [_jsx(ImageIcon, { className: `h-8 w-8 ${groupImageErrors.candidate ? 'text-red-400' : 'text-amber-400'} mb-2` }), _jsx("p", { className: "text-xs text-gray-500", children: "H\u00ECnh \u1EA3nh d\u00E0nh cho th\u00ED sinh" })] })), groupImageErrors.candidate && (_jsx("p", { className: "text-sm font-medium text-destructive", children: groupImageErrors.candidate }))] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-sm font-semibold text-gray-800", children: ["\uD83D\uDCF7 Examiner's Copy Image ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: `examiner-img-group-${displayGroupNumber}`, label: "", accept: "image/*", value: examinerImageUrl || '', onChange: (url) => updateExaminerImage(url), maxSize: 5, placeholder: "Upload Examiner's Copy", autoUpload: true, prefix: "questions/compare-images" }), examinerImageUrl && (_jsx(ImagePreview, { imageUrl: examinerImageUrl, label: "Examiner's Copy" })), !examinerImageUrl && (_jsxs("div", { className: `flex flex-col items-center justify-center rounded-xl border-2 border-dashed ${groupImageErrors.examiner ? 'border-red-300 bg-red-50/50' : 'border-orange-200 bg-orange-50/50'} py-6 px-4 text-center`, children: [_jsx(ImageIcon, { className: `h-8 w-8 ${groupImageErrors.examiner ? 'text-red-400' : 'text-orange-400'} mb-2` }), _jsx("p", { className: "text-xs text-gray-500", children: "H\u00ECnh \u1EA3nh d\u00E0nh cho gi\u00E1m kh\u1EA3o" })] })), groupImageErrors.examiner && (_jsx("p", { className: "text-sm font-medium text-destructive", children: groupImageErrors.examiner }))] })] }) })] })) : (
|
|
293
|
-
/* Read-only view of shared images for non-first questions in group */
|
|
294
|
-
_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow shadow-amber-100/30", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-amber-400 to-orange-400" }), _jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-100", children: _jsx(ImageIcon, { className: "h-3.5 w-3.5 text-amber-600" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "H\u00ECnh \u1EA3nh so s\u00E1nh" }), _jsxs("span", { className: "inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-500", children: ["Nh\u00F3m ", displayGroupNumber, " \u2014 Ch\u1EC9 \u0111\u1ECDc"] })] }), _jsx("p", { className: "text-xs text-gray-400 mt-0.5", children: "\u0110\u01B0\u1EE3c qu\u1EA3n l\u00FD b\u1EDFi c\u00E2u h\u1ECFi \u0111\u1EA7u ti\u00EAn trong nh\u00F3m" })] })] }) }), _jsx(CardContent, { className: "pt-0", children: candidateImageUrl || examinerImageUrl ? (_jsxs("div", { className: "space-y-4", children: [candidateImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-amber-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-amber-300/50", children: "Candidate's Copy" }) }), _jsx(ImagePreview, { imageUrl: candidateImageUrl, label: "Candidate's Copy" })] })), examinerImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-orange-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-orange-300/50", children: "Examiner's Copy" }) }), _jsx(ImagePreview, { imageUrl: examinerImageUrl, label: "Examiner's Copy" })] }))] })) : (_jsx("div", { className: "text-center py-4 text-sm text-gray-400 italic", children: "Ch\u01B0a c\u00F3 h\u00ECnh \u1EA3nh n\u00E0o trong nh\u00F3m" })) })] })), _jsxs(Form, { ...form, children: [_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-indigo-400 to-purple-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 shadow-sm", children: _jsx(Mic, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi Nghe & So s\u00E1nh" }), _jsx("p", { className: "text-sm text-gray-500", children: "H\u1ECDc sinh nghe \u00E2m thanh v\u00E0 n\u00F3i \u0111i\u1EC3m kh\u00E1c bi\u1EC7t gi\u1EEFa hai h\u00ECnh \u1EA3nh" })] })] }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsx("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: _jsxs("div", { className: "flex flex-col md:flex-row gap-6", children: [_jsx("div", { className: "flex-1", children: _jsx(PointsInput, { ...form.register('points', { valueAsNumber: true }), error: form.formState.errors?.points?.message }) }), _jsx("div", { className: "flex-1 flex flex-col justify-center pt-2" })] }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-sm font-semibold text-gray-800", children: [_jsx(Volume2, { className: "h-4 w-4 text-indigo-500" }), "\u00C2m thanh c\u00E2u h\u1ECFi ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FormField, { control: control, name: "audioUrl", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(FileUpload, { id: `audio-compare-${displayQuestionNumber}`, label: "", accept: "audio/*", value: field.value, onChange: (url) => { field.onChange(url); clearErrors('audioUrl'); }, maxSize: 10, placeholder: "T\u1EA3i l\u00EAn file \u00E2m thanh (mp3, wav...)", autoUpload: true, prefix: "listen-and-speak" }) }), _jsx(FormMessage, {})] })) })] })] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-green-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-green-400 to-emerald-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 shadow-sm", children: _jsx(MessageSquareText, { className: "h-4 w-4 text-white" }) }), _jsx("div", { children: _jsxs(CardTitle, { className: "text-lg font-semibold text-gray-800", children: ["\u0110\u00E1p \u00E1n mong mu\u1ED1n (", watchedExpectedAnswers.filter((a) => a.trim()).length, ")"] }) })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addExpectedAnswer, className: "gap-2 border-green-200 text-green-600 hover:bg-green-50", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm \u0111\u00E1p \u00E1n"] })] }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-blue-100 bg-blue-50/60 px-3 py-2.5", children: [_jsx(Info, { className: "h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" }), _jsxs("div", { className: "text-xs text-blue-700 space-y-1", children: [_jsx("p", { children: "Nh\u1EADp c\u00E1c c\u00E2u tr\u1EA3 l\u1EDDi mong mu\u1ED1n. C\u00F3 th\u1EC3 th\u00EAm nhi\u1EC1u \u0111\u00E1p \u00E1n n\u1EBFu c\u00F3 nhi\u1EC1u c\u00E1ch tr\u1EA3 l\u1EDDi \u0111\u00FAng." }), _jsxs("p", { children: [_jsx("span", { className: "font-semibold", children: "\u2022 Ch\u1EA5m th\u1EE7 c\u00F4ng:" }), " Gi\u00E1o vi\u00EAn d\u00F9ng c\u00E1c \u0111\u00E1p \u00E1n n\u00E0y l\u00E0m tham kh\u1EA3o \u0111\u1EC3 ch\u1EA5m \u0111i\u1EC3m."] }), _jsxs("p", { children: [_jsx("span", { className: "font-semibold", children: "\u2022 Ch\u1EA5m t\u1EF1 \u0111\u1ED9ng (AI):" }), " H\u1EC7 th\u1ED1ng d\u00F9ng AI so s\u00E1nh gi\u1ECDng n\u00F3i c\u1EE7a h\u1ECDc sinh v\u1EDBi c\u00E1c \u0111\u00E1p \u00E1n \u0111\u1EC3 t\u1EF1 \u0111\u1ED9ng ch\u1EA5m \u0111i\u1EC3m."] })] })] }), _jsxs("div", { className: "space-y-3", children: [watchedExpectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-start gap-3 group/answer", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-green-100 text-green-700 font-semibold shadow-sm text-sm", children: index + 1 }), _jsxs("div", { className: "flex-1", children: [_jsx(Input, { placeholder: `VD: ${index === 0 ? 'The tree is bigger in picture A' : 'The dog is brown in picture B'}`, value: answer || '', onChange: (e) => handleExpectedAnswerChange(index, e.target.value), className: "border-gray-200 focus:border-green-300 focus:ring-green-200" }), form.formState.errors?.expectedAnswers?.[index]?.message && (_jsx("p", { className: "mt-1 text-sm text-red-500", children: form.formState.errors.expectedAnswers[index]?.message }))] }), watchedExpectedAnswers.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeExpectedAnswer(index), className: "text-red-400 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/answer:opacity-100 transition-opacity", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }, index))), form.formState.errors?.expectedAnswers && !Array.isArray(form.formState.errors.expectedAnswers) && (_jsx("p", { className: "text-sm text-red-500", children: form.formState.errors.expectedAnswers.message }))] })] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-amber-400 to-orange-400" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-lg font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "text-sm text-gray-500 mt-0.5", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { children: _jsx(Textarea, { ...form.register('explanation'), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", className: "min-h-[80px] border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200 resize-none", rows: 3 }) })] })] })] }));
|
|
295
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useEffect as a,useMemo as t,useRef as i,useState as n}from"react";import{Card as s,CardContent as l,CardHeader as d,CardTitle as c}from"../../../../components/ui/card";import{Label as o}from"../../../../components/ui/label";import{Button as m}from"../../../../components/ui/button";import{Input as h}from"../../../../components/ui/input";import{Textarea as g}from"../../../../components/ui/textarea";import{Form as p,FormControl as u,FormField as x,FormItem as f,FormMessage as b}from"../../../../components/ui/form";import{FileUpload as N}from"../../../../components/ui/file-upload";import{PointsInput as v}from"../../../../components/ui/points-input";import{useCompareImagesGroup as w}from"../../_shared/hooks/useCompareImagesGroup";import{useReactHookForm as y}from"../../../../shared/lib/hooks/useReactHookForm";import{useDebouncedCallback as C,usePresignedFileUrl as U}from"../../../../shared/lib/hooks";import{listenAndSpeakCompareImagesSchema as k}from"../../../../shared/lib/validation/listen-and-speak-compare-images.validation";import{Plus as I,Trash2 as A,Mic as S,Volume2 as j,ImageIcon as V,MessageSquareText as q,ListChecks as D,BookOpen as B,Eye as E,Pencil as z,Info as H}from"lucide-react";import{CreatorGuide as J}from"../../components/CreatorGuide";import{ListenAndSpeakCompareImagesClient as O}from"./ListenAndSpeakCompareImagesClient";function G({imageUrl:r,label:a}){const{previewUrl:t}=U(r);return t?e("div",{className:"mt-2 rounded-xl border border-gray-200 overflow-hidden bg-white shadow-sm",children:e("img",{src:t,alt:a,className:"h-72 w-full object-contain bg-gray-50 p-2"})}):null}export function ListenAndSpeakCompareImagesCreator({questionIndex:U,initialData:$,onChange:T,onUnsavedChangesChange:P,validationRef:F,isFirstInGroup:R,partId:L,groups:M,questionIndexInPart:K}){const Q=t(()=>{if(void 0!==R)return R;if(!M||!M.length||void 0===K)return!0;let e=0;for(const r of M){if(K===e)return!0;e+=r.questionCount}return!1},[R,M,K]),_=t(()=>{if(M&&M.length>0){if(void 0===K)return 1;let e=0;for(let r=0;r<M.length;r++){if(K<e+M[r].questionCount)return r+1;e+=M[r].questionCount}return 1}const e=$;return e?.groupNumber??e?.questionGroup?.groupNumber??void 0},[M,K,$]),W=$,{candidateImageUrl:X,examinerImageUrl:Y,updateCandidateImage:Z,updateExaminerImage:ee,isValid:re}=w(L,_,{candidateImageUrl:W?.candidateImageUrl,examinerImageUrl:W?.examinerImageUrl}),[ae,te]=n({}),ie=y({zodSchema:k,defaultValues:{candidateImageUrl:W?.candidateImageUrl||X||"",examinerImageUrl:W?.examinerImageUrl||Y||"",audioUrl:W?.audioUrl||"",expectedAnswers:W?.expectedAnswers?.length>0?W.expectedAnswers:[""],points:W?.points??1,explanation:W?.explanation||"",isBackup:W?.isBackup||!1}}),{control:ne,watch:se,setValue:le,trigger:de,clearErrors:ce}=ie,oe=se("audioUrl"),me=se("expectedAnswers"),he=se("points"),ge=se("explanation"),pe=se("isBackup"),ue=i($),xe=i(!1),fe=i(T),be=i("");a(()=>{fe.current=T},[T]);const Ne=C(e=>{fe.current?.(e)},300);a(()=>{F&&(F.current={triggerValidation:async()=>{if(Q){const e=!!X,r=!!Y,a={};if(e||(a.candidate="Vui lòng tải lên Candidate's Copy Image!"),r||(a.examiner="Vui lòng tải lên Examiner's Copy Image!"),te(a),!e||!r)return await de(),!1}else te({});return await de()},getFormData:()=>({...ie.getValues(),candidateImageUrl:X||"",examinerImageUrl:Y||"",groupNumber:Q?ye:void 0})})},[F,ie,de,Q,X,Y,te]),a(()=>{X&&ae.candidate&&te(e=>({...e,candidate:void 0})),Y&&ae.examiner&&te(e=>({...e,examiner:void 0}))},[X,Y]),a(()=>{const e={candidateImageUrl:W?.candidateImageUrl||"",examinerImageUrl:W?.examinerImageUrl||"",audioUrl:W?.audioUrl||"",expectedAnswers:W?.expectedAnswers?.length>0?W.expectedAnswers:[""],points:W?.points??1,explanation:W?.explanation||"",isBackup:W?.isBackup||!1};if(!(JSON.stringify(ue.current)!==JSON.stringify($)))return;ue.current=$;const r={audioUrl:oe,expectedAnswers:me,points:he,explanation:ge,isBackup:pe},a=r.points===e.points&&r.explanation===e.explanation&&r.audioUrl===e.audioUrl&&JSON.stringify(r.expectedAnswers)===JSON.stringify(e.expectedAnswers);if(a&&!0===$?.isCompleted)return;!a&&(e.audioUrl!==r.audioUrl||JSON.stringify(e.expectedAnswers)!==JSON.stringify(r.expectedAnswers))&&(xe.current=!0,ie.reset(e),setTimeout(()=>{xe.current=!1},150))},[$,ie,oe,me,ge,he,pe]);const ve=JSON.stringify(me);a(()=>{if(xe.current){const e=JSON.stringify({audioUrl:oe,expectedAnswers:me,points:he,explanation:ge,isBackup:pe,candidateImageUrl:X,examinerImageUrl:Y});return void(be.current=e)}const e={audioUrl:oe,expectedAnswers:me,points:he,explanation:ge,isBackup:pe,candidateImageUrl:X,examinerImageUrl:Y},r=JSON.stringify(e);be.current!==r&&(be.current=r,Ne(e))},[oe,ve,he,ge,pe,X,Y,Ne]),a(()=>{P&&P(ie.formState.isDirty)},[ie.formState.isDirty,P]);const we="number"!=typeof U||isNaN(U)?"number"!=typeof K||isNaN(K)?"?":K+1:U+1,ye=_??1,[Ce,Ue]=n(!1);if(Ce){const a={audioUrl:oe,expectedAnswers:me,explanation:ge,points:he,isBackup:pe,groupNumber:ye,candidateImageUrl:X||"",examinerImageUrl:Y||""};return r("div",{className:"space-y-6",children:[e(s,{children:e(d,{children:r("div",{className:"flex items-center justify-between",children:[r("div",{children:[e(c,{children:"Nghe và nói — So sánh hình ảnh (Preview)"}),e("p",{className:"mt-1 text-sm text-gray-500",children:"Preview mode"})]}),r(m,{variant:"outline",size:"sm",onClick:()=>Ue(!1),children:[e(z,{className:"mr-2 h-4 w-4"}),"Quay lại Edit Mode"]})]})})}),e(O,{questionData:a,isReviewMode:!0})]})}return r("div",{className:"space-y-6",children:[e(J,{colorScheme:"blue",steps:[{text:"Upload 2 hình ảnh so sánh: Hình thí sinh (Candidate) và Hình giám khảo (Examiner) — dùng chung cho nhóm"},{text:"Câu hỏi đầu tiên trong nhóm sẽ quản lý hình ảnh chung, các câu sau tham chiếu"},{text:"Upload audio câu hỏi cho từng câu — giám khảo hỏi về sự khác biệt giữa 2 hình"},{text:"Nhập các đáp án mong muốn mô tả sự khác biệt",highlight:!0},{text:"Thêm giải thích cho từng điểm khác biệt"}],tips:["Chọn hình có sự khác biệt rõ ràng nhưng không quá dễ phát hiện"]}),r(s,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500"}),e(d,{className:"pb-4",children:r("div",{className:"flex items-center justify-between",children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 shadow-md",children:e(D,{className:"h-5 w-5 text-white"})}),r("div",{children:[e(c,{className:"text-xl font-bold text-gray-900",children:"Nhóm đáp án"}),r("div",{className:"mt-1.5 inline-flex items-center gap-1.5 rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-700",children:[e("span",{children:"🖼️"}),r("span",{children:["Nhóm ",ye]}),e("span",{className:"text-xs font-normal opacity-75 ml-1",children:"(Dùng chung)"})]})]})]}),r(m,{variant:"outline",size:"sm",onClick:()=>Ue(!0),disabled:!oe||0===me.filter(e=>e.trim()).length,className:"gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 hover:text-blue-700",children:[e(E,{className:"h-4 w-4"}),"Preview"]})]})})]}),r(s,Q?{className:"overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(d,{className:"pb-4",children:r("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-100",children:e(V,{className:"h-3.5 w-3.5 text-amber-600"})}),e(c,{className:"text-lg font-semibold text-gray-800",children:"Hình ảnh so sánh"})]})}),e(l,{className:"space-y-4",children:r("div",{className:"space-y-6",children:[r("div",{className:"space-y-2",children:[r(o,{className:"flex items-center gap-2 text-sm font-semibold text-gray-800",children:["📷 Candidate's Copy Image ",e("span",{className:"text-red-500",children:"*"})]}),e(N,{id:`candidate-img-group-${ye}`,label:"",accept:"image/*",value:X||"",onChange:e=>Z(e),maxSize:5,placeholder:"Upload Candidate's Copy",autoUpload:!0,prefix:"questions/compare-images"}),X&&e(G,{imageUrl:X,label:"Candidate's Copy"}),!X&&r("div",{className:`flex flex-col items-center justify-center rounded-xl border-2 border-dashed ${ae.candidate?"border-red-300 bg-red-50/50":"border-amber-200 bg-amber-50/50"} py-6 px-4 text-center`,children:[e(V,{className:`h-8 w-8 ${ae.candidate?"text-red-400":"text-amber-400"} mb-2`}),e("p",{className:"text-xs text-gray-500",children:"Hình ảnh dành cho thí sinh"})]}),ae.candidate&&e("p",{className:"text-sm font-medium text-destructive",children:ae.candidate})]}),r("div",{className:"space-y-2",children:[r(o,{className:"flex items-center gap-2 text-sm font-semibold text-gray-800",children:["📷 Examiner's Copy Image ",e("span",{className:"text-red-500",children:"*"})]}),e(N,{id:`examiner-img-group-${ye}`,label:"",accept:"image/*",value:Y||"",onChange:e=>ee(e),maxSize:5,placeholder:"Upload Examiner's Copy",autoUpload:!0,prefix:"questions/compare-images"}),Y&&e(G,{imageUrl:Y,label:"Examiner's Copy"}),!Y&&r("div",{className:`flex flex-col items-center justify-center rounded-xl border-2 border-dashed ${ae.examiner?"border-red-300 bg-red-50/50":"border-orange-200 bg-orange-50/50"} py-6 px-4 text-center`,children:[e(V,{className:`h-8 w-8 ${ae.examiner?"text-red-400":"text-orange-400"} mb-2`}),e("p",{className:"text-xs text-gray-500",children:"Hình ảnh dành cho giám khảo"})]}),ae.examiner&&e("p",{className:"text-sm font-medium text-destructive",children:ae.examiner})]})]})})]}:{className:"overflow-hidden border-0 bg-white shadow shadow-amber-100/30",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(d,{className:"pb-3",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-100",children:e(V,{className:"h-3.5 w-3.5 text-amber-600"})}),r("div",{className:"flex-1",children:[r("div",{className:"flex items-center gap-2",children:[e(c,{className:"text-lg font-semibold text-gray-800",children:"Hình ảnh so sánh"}),r("span",{className:"inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-500",children:["Nhóm ",ye," — Chỉ đọc"]})]}),e("p",{className:"text-xs text-gray-400 mt-0.5",children:"Được quản lý bởi câu hỏi đầu tiên trong nhóm"})]})]})}),e(l,{className:"pt-0",children:X||Y?r("div",{className:"space-y-4",children:[X&&r("div",{className:"space-y-2",children:[e("div",{className:"flex justify-center",children:e("span",{className:"inline-block rounded-lg border-2 border-amber-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-amber-300/50",children:"Candidate's Copy"})}),e(G,{imageUrl:X,label:"Candidate's Copy"})]}),Y&&r("div",{className:"space-y-2",children:[e("div",{className:"flex justify-center",children:e("span",{className:"inline-block rounded-lg border-2 border-orange-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-orange-300/50",children:"Examiner's Copy"})}),e(G,{imageUrl:Y,label:"Examiner's Copy"})]})]}):e("div",{className:"text-center py-4 text-sm text-gray-400 italic",children:"Chưa có hình ảnh nào trong nhóm"})})]}),r(p,{...ie,children:[r(s,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-indigo-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-indigo-400 to-purple-400"}),e(d,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 shadow-sm",children:e(S,{className:"h-4 w-4 text-white"})}),r("div",{children:[e(c,{className:"text-lg font-semibold text-gray-800",children:"Câu hỏi Nghe & So sánh"}),e("p",{className:"text-sm text-gray-500",children:"Học sinh nghe âm thanh và nói điểm khác biệt giữa hai hình ảnh"})]})]})}),r(l,{className:"space-y-6",children:[e("div",{className:"rounded-xl border border-gray-200 bg-gray-50 p-4",children:r("div",{className:"flex flex-col md:flex-row gap-6",children:[e("div",{className:"flex-1",children:e(v,{...ie.register("points",{valueAsNumber:!0}),error:ie.formState.errors?.points?.message})}),e("div",{className:"flex-1 flex flex-col justify-center pt-2"})]})}),r("div",{className:"space-y-2",children:[r(o,{className:"flex items-center gap-2 text-sm font-semibold text-gray-800",children:[e(j,{className:"h-4 w-4 text-indigo-500"}),"Âm thanh câu hỏi ",e("span",{className:"text-red-500",children:"*"})]}),e(x,{control:ne,name:"audioUrl",render:({field:a})=>r(f,{children:[e(u,{children:e(N,{id:`audio-compare-${we}`,label:"",accept:"audio/*",value:a.value,onChange:e=>{a.onChange(e),ce("audioUrl")},maxSize:10,placeholder:"Tải lên file âm thanh (mp3, wav...)",autoUpload:!0,prefix:"listen-and-speak"})}),e(b,{})]})})]})]})]}),r(s,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-green-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-green-400 to-emerald-500"}),e(d,{className:"pb-4",children:r("div",{className:"flex items-center justify-between",children:[r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 shadow-sm",children:e(q,{className:"h-4 w-4 text-white"})}),e("div",{children:r(c,{className:"text-lg font-semibold text-gray-800",children:["Đáp án mong muốn (",me.filter(e=>e.trim()).length,")"]})})]}),r(m,{type:"button",variant:"outline",size:"sm",onClick:()=>{const e=ie.getValues("expectedAnswers");ie.setValue("expectedAnswers",[...e,""],{shouldValidate:!0,shouldDirty:!0})},className:"gap-2 border-green-200 text-green-600 hover:bg-green-50",children:[e(I,{className:"h-4 w-4"}),"Thêm đáp án"]})]})}),r(l,{className:"space-y-4",children:[r("div",{className:"flex items-start gap-2 rounded-lg border border-blue-100 bg-blue-50/60 px-3 py-2.5",children:[e(H,{className:"h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0"}),r("div",{className:"text-xs text-blue-700 space-y-1",children:[e("p",{children:"Nhập các câu trả lời mong muốn. Có thể thêm nhiều đáp án nếu có nhiều cách trả lời đúng."}),r("p",{children:[e("span",{className:"font-semibold",children:"• Chấm thủ công:"})," Giáo viên dùng các đáp án này làm tham khảo để chấm điểm."]}),r("p",{children:[e("span",{className:"font-semibold",children:"• Chấm tự động (AI):"})," Hệ thống dùng AI so sánh giọng nói của học sinh với các đáp án để tự động chấm điểm."]})]})]}),r("div",{className:"space-y-3",children:[me.map((a,t)=>r("div",{className:"flex items-start gap-3 group/answer",children:[e("div",{className:"flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-green-100 text-green-700 font-semibold shadow-sm text-sm",children:t+1}),r("div",{className:"flex-1",children:[e(h,{placeholder:"VD: "+(0===t?"The tree is bigger in picture A":"The dog is brown in picture B"),value:a||"",onChange:e=>((e,r)=>{const a=[...me];a[e]=r,ie.setValue("expectedAnswers",a,{shouldValidate:!0,shouldDirty:!0})})(t,e.target.value),className:"border-gray-200 focus:border-green-300 focus:ring-green-200"}),ie.formState.errors?.expectedAnswers?.[t]?.message&&e("p",{className:"mt-1 text-sm text-red-500",children:ie.formState.errors.expectedAnswers[t]?.message})]}),me.length>1&&e(m,{type:"button",variant:"ghost",size:"sm",onClick:()=>(e=>{if(me.length>1){const r=me.filter((r,a)=>a!==e);ie.setValue("expectedAnswers",r,{shouldValidate:!0,shouldDirty:!0})}})(t),className:"text-red-400 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/answer:opacity-100 transition-opacity",children:e(A,{className:"h-4 w-4"})})]},t)),ie.formState.errors?.expectedAnswers&&!Array.isArray(ie.formState.errors.expectedAnswers)&&e("p",{className:"text-sm text-red-500",children:ie.formState.errors.expectedAnswers.message})]})]})]}),r(s,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-amber-400 to-orange-400"}),e(d,{className:"pb-4",children:r("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm",children:e(B,{className:"h-4 w-4 text-white"})}),r("div",{children:[r("div",{className:"flex items-center gap-2",children:[e(c,{className:"text-lg font-semibold text-gray-800",children:"Giải thích"}),e("span",{className:"inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600",children:"Không bắt buộc"})]}),e("p",{className:"text-sm text-gray-500 mt-0.5",children:"Giải thích cho đáp án đúng — hiển thị sau khi học sinh hoàn thành câu hỏi"})]})]})}),e(l,{children:e(g,{...ie.register("explanation"),placeholder:"VD: Câu trả lời đúng là... vì...",className:"min-h-[80px] border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200 resize-none",rows:3})})]})]})]})}
|