@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.2
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.js +1 -98
- 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/CreateExamInfoForm.js +1 -35
- package/dist/components/exams/CreateExamPageContainer.js +1 -103
- package/dist/components/exams/ExamCreator.js +1 -89
- package/dist/components/exams/ExamPartTabs.js +1 -40
- package/dist/components/exams/ExamQuestionGrid.js +1 -18
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
- package/dist/components/exams/PartAudioUploader.js +1 -9
- package/dist/components/exams/TemplateSelector.js +1 -66
- package/dist/components/exams/api.js +1 -3
- package/dist/components/exams/create-exam.validation.js +1 -18
- package/dist/components/exams/exam-template.utils.js +1 -18
- 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.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.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-transform-types.js +1 -1
- 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 +4 -2
|
@@ -1,26 +1 @@
|
|
|
1
|
-
export const transformListenAndSpeakImageGroup = (
|
|
2
|
-
const contentData = question.content;
|
|
3
|
-
const answerData = question.answer;
|
|
4
|
-
const questionGroup = question.questionGroup;
|
|
5
|
-
const expectedAnswers = answerData?.expectedAnswers || [];
|
|
6
|
-
const apiContent = {
|
|
7
|
-
audioUrl: contentData?.audioUrl || answerData?.audioUrl || '',
|
|
8
|
-
activeImageId: contentData?.activeImageId || answerData?.activeImageId || '',
|
|
9
|
-
isBackup: contentData?.isBackup || answerData?.isBackup || false,
|
|
10
|
-
};
|
|
11
|
-
const result = {
|
|
12
|
-
apiContent,
|
|
13
|
-
apiCorrectAnswer: {
|
|
14
|
-
expectedAnswers: expectedAnswers,
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
if (contentData?.groupImages) {
|
|
18
|
-
result.sceneData = { images: contentData.groupImages };
|
|
19
|
-
result.groupNumber = contentData.groupNumber || questionGroup?.groupNumber || 1;
|
|
20
|
-
}
|
|
21
|
-
else if (questionGroup?.payload?.images) {
|
|
22
|
-
result.sceneData = { images: questionGroup.payload.images };
|
|
23
|
-
result.groupNumber = questionGroup.groupNumber;
|
|
24
|
-
}
|
|
25
|
-
return result;
|
|
26
|
-
};
|
|
1
|
+
export const transformListenAndSpeakImageGroup=e=>{const a=e.content,r=e.answer,o=e.questionGroup,s=r?.expectedAnswers||[],u={apiContent:{audioUrl:a?.audioUrl||r?.audioUrl||"",activeImageId:a?.activeImageId||r?.activeImageId||"",isBackup:a?.isBackup||r?.isBackup||!1},apiCorrectAnswer:{expectedAnswers:s}};return a?.groupImages?(u.sceneData={images:a.groupImages},u.groupNumber=a.groupNumber||o?.groupNumber||1):o?.payload?.images&&(u.sceneData={images:o.payload.images},u.groupNumber=o.groupNumber),u};
|
|
@@ -1,38 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Volume2, Mic, CheckCircle2, Play, Pause, Info, ArrowLeftRight } from 'lucide-react';
|
|
4
|
-
import { Button } from '../../../../components/ui/button';
|
|
5
|
-
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
6
|
-
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
7
|
-
function QuestionAudioPlayer({ audioUrl }) {
|
|
8
|
-
const { previewUrl } = usePresignedFileUrl(audioUrl);
|
|
9
|
-
const { isPlaying, currentTime, duration, progressPercentage, handlePlayPause, handleProgressClick, formatTime, } = useAudioPreview(previewUrl);
|
|
10
|
-
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" })] }));
|
|
11
|
-
}
|
|
12
|
-
// ─── Sub-component: Presigned Image Preview ──────────────────────────────────
|
|
13
|
-
function ImagePreview({ imageUrl, title }) {
|
|
14
|
-
const { previewUrl, isLoading } = usePresignedFileUrl(imageUrl);
|
|
15
|
-
if (isLoading) {
|
|
16
|
-
return (_jsx("div", { className: "w-full aspect-[4/3] rounded-xl border-2 border-gray-200 bg-gray-100 animate-pulse flex items-center justify-center", children: _jsx("span", { className: "text-gray-400 text-sm", children: "\u0110ang t\u1EA3i..." }) }));
|
|
17
|
-
}
|
|
18
|
-
if (!previewUrl)
|
|
19
|
-
return null;
|
|
20
|
-
return (_jsx("img", { src: previewUrl, alt: title || 'Illustration', className: "w-full rounded-xl border-2 border-gray-200 object-cover shadow-sm" }));
|
|
21
|
-
}
|
|
22
|
-
// ─── Sub-component: Info Table ────────────────────────────────────────────────
|
|
23
|
-
function InfoTable({ label, title, rows, headerColor, }) {
|
|
24
|
-
return (_jsxs("div", { className: "rounded-xl overflow-hidden border border-gray-200 shadow-sm", children: [_jsx("div", { className: `${headerColor} text-white text-xs font-bold text-center py-1.5 tracking-widest uppercase`, children: label }), _jsx("div", { className: "bg-amber-50 text-center font-semibold text-sm py-2 border-b border-gray-200", children: title }), _jsx("table", { className: "w-full text-sm", children: _jsx("tbody", { children: rows.map((row, index) => (_jsxs("tr", { className: "border-b border-gray-100 last:border-0 bg-white", children: [_jsx("td", { className: "px-3 py-2 font-medium text-gray-700 w-1/2", children: row.key }), _jsx("td", { className: `px-3 py-2 w-1/2 ${row.value === '?' ? 'text-gray-400 font-bold text-base' : 'text-gray-800'}`, children: row.value })] }, index))) }) })] }));
|
|
25
|
-
}
|
|
26
|
-
// ─── Main Client Component ───────────────────────────────────────────────────
|
|
27
|
-
export function ListenAndSpeakInfoExchangeClient({ questionData, isReviewMode = false, viewMode = 'ANSWER', }) {
|
|
28
|
-
const { audioUrl, title, imageUrl, fields, expectedAnswers = [], explanation } = questionData;
|
|
29
|
-
const qData = questionData;
|
|
30
|
-
// Use dual fields if available, fall back to legacy single fields
|
|
31
|
-
const candidateRows = (qData.candidateFields || fields || [])
|
|
32
|
-
.map((f) => ({ key: f.key, value: f.answer }));
|
|
33
|
-
const examinerRows = (qData.examinerFields || [])
|
|
34
|
-
.map((f) => ({ key: f.key, value: f.answer }));
|
|
35
|
-
return (_jsx("div", { className: "space-y-4", children: _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(ArrowLeftRight, { className: "h-4 w-4" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["Nghe v\u00E0 n\u00F3i (Trao \u0111\u1ED5i th\u00F4ng tin \u2014 ", viewMode === 'ANSWER' ? 'Trả lời' : 'Đặt câu hỏi', ")"] })] }) }), _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" })] })), _jsxs("div", { className: "flex flex-col lg:flex-row gap-4", children: [imageUrl && (_jsx("div", { className: "lg:w-1/2", children: _jsx(ImagePreview, { imageUrl: imageUrl, title: title }) })), _jsxs("div", { className: "flex-1 space-y-4", children: [_jsx(InfoTable, { label: "CANDIDATE'S COPY", title: title, rows: candidateRows, headerColor: "bg-teal-600" }), _jsx(InfoTable, { label: "EXAMINER'S COPY", title: title, rows: examinerRows, headerColor: "bg-amber-600" })] })] }), _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: viewMode === 'ANSWER'
|
|
36
|
-
? 'Học sinh sẽ nhấn nút ghi âm và nói câu trả lời tại đây'
|
|
37
|
-
: 'Học sinh sẽ nhấn nút ghi âm và đặt câu hỏi tại đây' })] }), 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: viewMode === 'ANSWER' ? 'Các đáp án mong muốn' : 'Các câu hỏi mong muốn' })] }), _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 })] }))] })] }) }));
|
|
38
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{Volume2 as i,Mic as t,CheckCircle2 as a,Play as l,Pause as s,Info as d,ArrowLeftRight as n}from"lucide-react";import{Button as o}from"../../../../components/ui/button";import{usePresignedFileUrl as c,useAudioPreview as m}from"../../../../shared/lib/hooks";function g({audioUrl:t}){const{previewUrl:a}=c(t),{isPlaying:d,currentTime:n,duration:g,progressPercentage:h,handlePlayPause:x,handleProgressClick:b,formatTime:u}=m(a);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(o,{variant:"outline",size:"icon",onClick:x,disabled:!a,className:"h-8 w-8 border-indigo-300 text-indigo-600 hover:bg-indigo-100",children:e(d?s:l,{className:"h-3.5 w-3.5"})}),r("div",{className:"text-muted-foreground min-w-[70px] text-xs",children:[u(n)," / ",u(g)]}),e("div",{className:"bg-indigo-200 relative h-1.5 flex-1 cursor-pointer rounded-full",onClick:b,children:e("div",{className:"bg-indigo-500 absolute top-0 left-0 h-full rounded-full transition-all",style:{width:`${h}%`}})}),e(i,{className:"h-4 w-4 text-indigo-400"})]})}function h({imageUrl:r,title:i}){const{previewUrl:t,isLoading:a}=c(r);return a?e("div",{className:"w-full aspect-[4/3] rounded-xl border-2 border-gray-200 bg-gray-100 animate-pulse flex items-center justify-center",children:e("span",{className:"text-gray-400 text-sm",children:"Đang tải..."})}):t?e("img",{src:t,alt:i||"Illustration",className:"w-full rounded-xl border-2 border-gray-200 object-cover shadow-sm"}):null}function x({label:i,title:t,rows:a,headerColor:l}){return r("div",{className:"rounded-xl overflow-hidden border border-gray-200 shadow-sm",children:[e("div",{className:`${l} text-white text-xs font-bold text-center py-1.5 tracking-widest uppercase`,children:i}),e("div",{className:"bg-amber-50 text-center font-semibold text-sm py-2 border-b border-gray-200",children:t}),e("table",{className:"w-full text-sm",children:e("tbody",{children:a.map((i,t)=>r("tr",{className:"border-b border-gray-100 last:border-0 bg-white",children:[e("td",{className:"px-3 py-2 font-medium text-gray-700 w-1/2",children:i.key}),e("td",{className:"px-3 py-2 w-1/2 "+("?"===i.value?"text-gray-400 font-bold text-base":"text-gray-800"),children:i.value})]},t))})})]})}export function ListenAndSpeakInfoExchangeClient({questionData:l,isReviewMode:s=!1,viewMode:o="ANSWER"}){const{audioUrl:c,title:m,imageUrl:b,fields:u,expectedAnswers:p=[],explanation:f}=l,N=l,y=(N.candidateFields||u||[]).map(e=>({key:e.key,value:e.answer})),v=(N.examinerFields||[]).map(e=>({key:e.key,value:e.answer}));return e("div",{className:"space-y-4",children: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(n,{className:"h-4 w-4"})}),r("span",{className:"text-sm font-medium text-gray-700",children:["Nghe và nói (Trao đổi thông tin — ","ANSWER"===o?"Trả lời":"Đặt câu hỏi",")"]})]})}),r("div",{className:"p-5 space-y-5",children:[c?e(g,{audioUrl:c}):r("div",{className:"flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3",children:[e(i,{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"})]}),r("div",{className:"flex flex-col lg:flex-row gap-4",children:[b&&e("div",{className:"lg:w-1/2",children:e(h,{imageUrl:b,title:m})}),r("div",{className:"flex-1 space-y-4",children:[e(x,{label:"CANDIDATE'S COPY",title:m,rows:y,headerColor:"bg-teal-600"}),e(x,{label:"EXAMINER'S COPY",title:m,rows:v,headerColor:"bg-amber-600"})]})]}),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(t,{className:"w-4 h-4 text-indigo-400"})}),e("p",{className:"text-xs text-gray-400",children:"ANSWER"===o?"Học sinh sẽ nhấn nút ghi âm và nói câu trả lời tại đây":"Học sinh sẽ nhấn nút ghi âm và đặt câu hỏi tại đây"})]}),s&&p.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(a,{className:"h-4 w-4 text-green-600"}),e("h4",{className:"text-sm font-semibold text-green-800",children:"ANSWER"===o?"Các đáp án mong muốn":"Các câu hỏi mong muốn"})]}),e("div",{className:"space-y-2",children:p.map((i,t)=>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:t+1}),e("span",{className:"text-sm text-green-800",children:i})]},t))})]}),s&&f&&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(d,{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:f})]})]})]})})}
|
|
@@ -1,385 +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 { useInfoExchangeGroup } from '../../_shared/hooks/useInfoExchangeGroup';
|
|
14
|
-
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
15
|
-
import { listenAndSpeakInfoExchangeSchema, } from '../../../../shared/lib/validation/listen-and-speak-info-exchange.validation';
|
|
16
|
-
// Utilities & Icons
|
|
17
|
-
import { Plus, Trash2, Mic, Volume2, Table2, MessageSquareText, ImageIcon, BookOpen, Eye, Pencil, Info, ListChecks } from 'lucide-react';
|
|
18
|
-
import { useReactHookForm } from '../../../../shared/lib/hooks/useReactHookForm';
|
|
19
|
-
import { useDebouncedCallback } from '../../../../shared/lib/hooks/useDebouncedCallback';
|
|
20
|
-
import { CreatorGuide } from '../../components/CreatorGuide';
|
|
21
|
-
import { ListenAndSpeakInfoExchangeClient } from './ListenAndSpeakInfoExchangeClient';
|
|
22
|
-
// ─── Sub-component: Image preview with presigned URL ─────────────────────────
|
|
23
|
-
function ImagePreview({ imageUrl, label }) {
|
|
24
|
-
const { previewUrl } = usePresignedFileUrl(imageUrl);
|
|
25
|
-
if (!previewUrl)
|
|
26
|
-
return null;
|
|
27
|
-
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: "max-h-80 w-full object-contain bg-gray-50 p-2" }) }));
|
|
28
|
-
}
|
|
29
|
-
const DEFAULT_FIELDS = [
|
|
30
|
-
{ key: '', answer: '' },
|
|
31
|
-
];
|
|
32
|
-
export function ListenAndSpeakInfoExchangeCreator({ questionIndex, initialData, onChange, validationRef, onUnsavedChangesChange, isFirstInGroup: isFirstInGroupProp, partId, groups, questionIndexInPart, viewMode = 'ANSWER', }) {
|
|
33
|
-
const isGrouped = Boolean(groups && groups.length > 0);
|
|
34
|
-
const isFirstInGroup = useMemo(() => {
|
|
35
|
-
if (isFirstInGroupProp !== undefined)
|
|
36
|
-
return isFirstInGroupProp;
|
|
37
|
-
if (!groups || !groups.length || questionIndexInPart === undefined)
|
|
38
|
-
return true;
|
|
39
|
-
let cumulative = 0;
|
|
40
|
-
for (const group of groups) {
|
|
41
|
-
if (questionIndexInPart === cumulative)
|
|
42
|
-
return true;
|
|
43
|
-
cumulative += group.questionCount;
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}, [isFirstInGroupProp, groups, questionIndexInPart]);
|
|
47
|
-
// ---------------------------------------------------------------
|
|
48
|
-
// 1. Group level state (Title, Image, CandidateFields, ExaminerFields) via shared store
|
|
49
|
-
// ---------------------------------------------------------------
|
|
50
|
-
const currentGroupNumber = useMemo(() => {
|
|
51
|
-
if (groups && groups.length > 0) {
|
|
52
|
-
if (questionIndexInPart === undefined)
|
|
53
|
-
return 1;
|
|
54
|
-
let cumulative = 0;
|
|
55
|
-
for (let i = 0; i < groups.length; i++) {
|
|
56
|
-
cumulative += groups[i].questionCount;
|
|
57
|
-
if (questionIndexInPart < cumulative)
|
|
58
|
-
return i + 1;
|
|
59
|
-
}
|
|
60
|
-
return 1;
|
|
61
|
-
}
|
|
62
|
-
// Fallback: use groupNumber from initialData when groups prop is not yet available
|
|
63
|
-
const apiData = initialData;
|
|
64
|
-
return apiData?.groupNumber ?? 1;
|
|
65
|
-
}, [groups, questionIndexInPart, initialData]);
|
|
66
|
-
const { title, imageUrl, fields, candidateFields, examinerFields, updateGroupData } = useInfoExchangeGroup(partId, currentGroupNumber);
|
|
67
|
-
// Initialize group data from initialData on first question (also re-run when initialData changes)
|
|
68
|
-
const initialDataRef = useRef(null);
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (isFirstInGroup && initialData) {
|
|
71
|
-
// Skip if initialData hasn't meaningfully changed
|
|
72
|
-
const initDataKey = JSON.stringify({
|
|
73
|
-
title: initialData.title,
|
|
74
|
-
imageUrl: initialData.imageUrl,
|
|
75
|
-
candidateFields: initialData.candidateFields,
|
|
76
|
-
examinerFields: initialData.examinerFields,
|
|
77
|
-
fields: initialData.fields,
|
|
78
|
-
});
|
|
79
|
-
if (initialDataRef.current === initDataKey)
|
|
80
|
-
return;
|
|
81
|
-
initialDataRef.current = initDataKey;
|
|
82
|
-
const initCandidateFields = initialData.candidateFields && initialData.candidateFields.length > 0
|
|
83
|
-
? initialData.candidateFields
|
|
84
|
-
: (initialData.fields && initialData.fields.length > 0 ? initialData.fields : DEFAULT_FIELDS);
|
|
85
|
-
const initExaminerFields = initialData.examinerFields && initialData.examinerFields.length > 0
|
|
86
|
-
? initialData.examinerFields
|
|
87
|
-
: DEFAULT_FIELDS;
|
|
88
|
-
updateGroupData({
|
|
89
|
-
title: initialData.title || '',
|
|
90
|
-
imageUrl: initialData.imageUrl || '',
|
|
91
|
-
fields: initCandidateFields, // backward compat
|
|
92
|
-
candidateFields: initCandidateFields,
|
|
93
|
-
examinerFields: initExaminerFields,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}, [isFirstInGroup, initialData, updateGroupData]);
|
|
97
|
-
// Use group store data, fallback to initialData
|
|
98
|
-
const groupTitle = title || initialData?.title || '';
|
|
99
|
-
const groupImageUrl = imageUrl || initialData?.imageUrl || '';
|
|
100
|
-
const groupCandidateFields = candidateFields && candidateFields.length > 0
|
|
101
|
-
? candidateFields
|
|
102
|
-
: (initialData?.candidateFields && initialData.candidateFields.length > 0
|
|
103
|
-
? initialData.candidateFields
|
|
104
|
-
: (initialData?.fields && initialData.fields.length > 0 ? initialData.fields : DEFAULT_FIELDS));
|
|
105
|
-
const groupExaminerFields = examinerFields && examinerFields.length > 0
|
|
106
|
-
? examinerFields
|
|
107
|
-
: (initialData?.examinerFields && initialData.examinerFields.length > 0
|
|
108
|
-
? initialData.examinerFields
|
|
109
|
-
: DEFAULT_FIELDS);
|
|
110
|
-
// ---------------------------------------------------------------
|
|
111
|
-
// 2. Per-question form data (Audio, ExpectedAnswers, Points, Explanation)
|
|
112
|
-
// ---------------------------------------------------------------
|
|
113
|
-
const form = useReactHookForm({
|
|
114
|
-
zodSchema: listenAndSpeakInfoExchangeSchema,
|
|
115
|
-
defaultValues: {
|
|
116
|
-
title: groupTitle,
|
|
117
|
-
imageUrl: groupImageUrl,
|
|
118
|
-
fields: groupCandidateFields, // backward compat
|
|
119
|
-
candidateFields: groupCandidateFields,
|
|
120
|
-
examinerFields: groupExaminerFields,
|
|
121
|
-
audioUrl: initialData?.audioUrl || '',
|
|
122
|
-
expectedAnswers: initialData?.expectedAnswers?.length
|
|
123
|
-
? initialData.expectedAnswers
|
|
124
|
-
: [''],
|
|
125
|
-
points: initialData?.points ?? 1,
|
|
126
|
-
explanation: initialData?.explanation || '',
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
const { control, watch, setValue, trigger, clearErrors } = form;
|
|
130
|
-
const watchedAudioUrl = watch('audioUrl');
|
|
131
|
-
const watchedExpectedAnswers = watch('expectedAnswers');
|
|
132
|
-
const watchedPoints = watch('points');
|
|
133
|
-
const watchedExplanation = watch('explanation');
|
|
134
|
-
// ---------------------------------------------------------------
|
|
135
|
-
// Refs to prevent infinite loops (echo-detection pattern)
|
|
136
|
-
// ---------------------------------------------------------------
|
|
137
|
-
const previousInitialDataRef = useRef(initialData);
|
|
138
|
-
const isInitializingRef = useRef(false);
|
|
139
|
-
const onChangeRef = useRef(onChange);
|
|
140
|
-
const previousPayloadRef = useRef('');
|
|
141
|
-
useEffect(() => {
|
|
142
|
-
onChangeRef.current = onChange;
|
|
143
|
-
}, [onChange]);
|
|
144
|
-
// Notify parent onChange (debounced)
|
|
145
|
-
const debouncedOnChange = useDebouncedCallback((data) => {
|
|
146
|
-
onChangeRef.current?.(data);
|
|
147
|
-
}, 300);
|
|
148
|
-
// Track unsaved changes
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
onUnsavedChangesChange?.(form.formState.isDirty);
|
|
151
|
-
}, [form.formState.isDirty, onUnsavedChangesChange]);
|
|
152
|
-
// ---------------------------------------------------------------
|
|
153
|
-
// Sync form values if initialData props change
|
|
154
|
-
// ---------------------------------------------------------------
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
const normalizedInitialData = {
|
|
157
|
-
title: groupTitle,
|
|
158
|
-
imageUrl: groupImageUrl,
|
|
159
|
-
fields: groupCandidateFields,
|
|
160
|
-
candidateFields: groupCandidateFields,
|
|
161
|
-
examinerFields: groupExaminerFields,
|
|
162
|
-
audioUrl: initialData?.audioUrl || '',
|
|
163
|
-
expectedAnswers: initialData?.expectedAnswers?.length
|
|
164
|
-
? initialData.expectedAnswers
|
|
165
|
-
: [''],
|
|
166
|
-
points: initialData?.points ?? 1,
|
|
167
|
-
explanation: initialData?.explanation || '',
|
|
168
|
-
};
|
|
169
|
-
const initialDataChanged = JSON.stringify(previousInitialDataRef.current) !== JSON.stringify(initialData);
|
|
170
|
-
if (!initialDataChanged)
|
|
171
|
-
return;
|
|
172
|
-
previousInitialDataRef.current = initialData;
|
|
173
|
-
const currentFormData = {
|
|
174
|
-
audioUrl: watchedAudioUrl,
|
|
175
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
176
|
-
points: watchedPoints,
|
|
177
|
-
explanation: watchedExplanation,
|
|
178
|
-
};
|
|
179
|
-
const isEchoFromOnChange = currentFormData.points === normalizedInitialData.points &&
|
|
180
|
-
currentFormData.explanation === normalizedInitialData.explanation &&
|
|
181
|
-
currentFormData.audioUrl === normalizedInitialData.audioUrl &&
|
|
182
|
-
JSON.stringify(currentFormData.expectedAnswers) === JSON.stringify(normalizedInitialData.expectedAnswers);
|
|
183
|
-
const wasJustSaved = initialData?.isCompleted === true;
|
|
184
|
-
if (isEchoFromOnChange && wasJustSaved)
|
|
185
|
-
return;
|
|
186
|
-
const shouldReset = !isEchoFromOnChange &&
|
|
187
|
-
(normalizedInitialData.audioUrl !== currentFormData.audioUrl ||
|
|
188
|
-
JSON.stringify(normalizedInitialData.expectedAnswers) !== JSON.stringify(currentFormData.expectedAnswers));
|
|
189
|
-
if (shouldReset) {
|
|
190
|
-
isInitializingRef.current = true;
|
|
191
|
-
form.reset(normalizedInitialData);
|
|
192
|
-
setTimeout(() => {
|
|
193
|
-
isInitializingRef.current = false;
|
|
194
|
-
}, 150);
|
|
195
|
-
}
|
|
196
|
-
}, [initialData, form, watchedAudioUrl, watchedExpectedAnswers, watchedExplanation, watchedPoints, groupTitle, groupImageUrl, groupCandidateFields, groupExaminerFields]);
|
|
197
|
-
// ---------------------------------------------------------------
|
|
198
|
-
// Fire onChange when form values change
|
|
199
|
-
// ---------------------------------------------------------------
|
|
200
|
-
const watchedExpectedAnswersString = JSON.stringify(watchedExpectedAnswers);
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
if (isInitializingRef.current) {
|
|
203
|
-
const currentPayload = JSON.stringify({
|
|
204
|
-
title: groupTitle,
|
|
205
|
-
imageUrl: groupImageUrl,
|
|
206
|
-
fields: groupCandidateFields,
|
|
207
|
-
candidateFields: groupCandidateFields,
|
|
208
|
-
examinerFields: groupExaminerFields,
|
|
209
|
-
audioUrl: watchedAudioUrl,
|
|
210
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
211
|
-
points: watchedPoints,
|
|
212
|
-
explanation: watchedExplanation,
|
|
213
|
-
groupNumber: isGrouped ? currentGroupNumber : undefined,
|
|
214
|
-
});
|
|
215
|
-
previousPayloadRef.current = currentPayload;
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const payload = {
|
|
219
|
-
title: groupTitle,
|
|
220
|
-
imageUrl: groupImageUrl,
|
|
221
|
-
fields: groupCandidateFields,
|
|
222
|
-
candidateFields: groupCandidateFields,
|
|
223
|
-
examinerFields: groupExaminerFields,
|
|
224
|
-
audioUrl: watchedAudioUrl,
|
|
225
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
226
|
-
points: watchedPoints,
|
|
227
|
-
explanation: watchedExplanation,
|
|
228
|
-
groupNumber: isGrouped ? currentGroupNumber : undefined,
|
|
229
|
-
};
|
|
230
|
-
const payloadString = JSON.stringify(payload);
|
|
231
|
-
if (previousPayloadRef.current !== payloadString) {
|
|
232
|
-
previousPayloadRef.current = payloadString;
|
|
233
|
-
debouncedOnChange(payload);
|
|
234
|
-
}
|
|
235
|
-
}, [
|
|
236
|
-
watchedAudioUrl,
|
|
237
|
-
watchedExpectedAnswersString,
|
|
238
|
-
watchedPoints,
|
|
239
|
-
watchedExplanation,
|
|
240
|
-
groupTitle,
|
|
241
|
-
groupImageUrl,
|
|
242
|
-
groupCandidateFields,
|
|
243
|
-
groupExaminerFields,
|
|
244
|
-
isGrouped,
|
|
245
|
-
currentGroupNumber,
|
|
246
|
-
debouncedOnChange,
|
|
247
|
-
]);
|
|
248
|
-
// ---------------------------------------------------------------
|
|
249
|
-
// Validation ref
|
|
250
|
-
// ---------------------------------------------------------------
|
|
251
|
-
useEffect(() => {
|
|
252
|
-
if (validationRef) {
|
|
253
|
-
validationRef.current = {
|
|
254
|
-
triggerValidation: async () => {
|
|
255
|
-
// Sync group data into form for validation
|
|
256
|
-
setValue('title', groupTitle);
|
|
257
|
-
setValue('imageUrl', groupImageUrl);
|
|
258
|
-
setValue('fields', groupCandidateFields);
|
|
259
|
-
setValue('candidateFields', groupCandidateFields);
|
|
260
|
-
setValue('examinerFields', groupExaminerFields);
|
|
261
|
-
const isValid = await trigger();
|
|
262
|
-
return isValid;
|
|
263
|
-
},
|
|
264
|
-
getFormData: () => ({
|
|
265
|
-
...form.getValues(),
|
|
266
|
-
title: groupTitle,
|
|
267
|
-
imageUrl: groupImageUrl,
|
|
268
|
-
fields: groupCandidateFields,
|
|
269
|
-
candidateFields: groupCandidateFields,
|
|
270
|
-
examinerFields: groupExaminerFields,
|
|
271
|
-
groupNumber: isGrouped ? currentGroupNumber : undefined,
|
|
272
|
-
}),
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
}, [trigger, validationRef, form, groupTitle, groupImageUrl, groupCandidateFields, groupExaminerFields, isGrouped, currentGroupNumber, setValue]);
|
|
276
|
-
// Track isDirty
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
onUnsavedChangesChange?.(form.formState.isDirty);
|
|
279
|
-
}, [form.formState.isDirty, onUnsavedChangesChange]);
|
|
280
|
-
// ---------------------------------------------------------------
|
|
281
|
-
// Handlers for Candidate Fields table
|
|
282
|
-
// ---------------------------------------------------------------
|
|
283
|
-
const handleUpdateCandidateField = (index, part, value) => {
|
|
284
|
-
const newFields = groupCandidateFields.map((f, i) => i === index ? { ...f, [part]: value } : f);
|
|
285
|
-
updateGroupData({ candidateFields: newFields, fields: newFields });
|
|
286
|
-
setValue('candidateFields', newFields, { shouldDirty: true });
|
|
287
|
-
setValue('fields', newFields, { shouldDirty: true });
|
|
288
|
-
};
|
|
289
|
-
const handleAddCandidateField = () => {
|
|
290
|
-
const newFields = [...groupCandidateFields, { key: '', answer: '' }];
|
|
291
|
-
updateGroupData({ candidateFields: newFields, fields: newFields });
|
|
292
|
-
setValue('candidateFields', newFields, { shouldDirty: true });
|
|
293
|
-
setValue('fields', newFields, { shouldDirty: true });
|
|
294
|
-
};
|
|
295
|
-
const handleRemoveCandidateField = (index) => {
|
|
296
|
-
const newFields = groupCandidateFields.filter((_, i) => i !== index);
|
|
297
|
-
updateGroupData({ candidateFields: newFields, fields: newFields });
|
|
298
|
-
setValue('candidateFields', newFields, { shouldDirty: true });
|
|
299
|
-
setValue('fields', newFields, { shouldDirty: true });
|
|
300
|
-
};
|
|
301
|
-
// ---------------------------------------------------------------
|
|
302
|
-
// Handlers for Examiner Fields table
|
|
303
|
-
// ---------------------------------------------------------------
|
|
304
|
-
const handleUpdateExaminerField = (index, part, value) => {
|
|
305
|
-
const newFields = groupExaminerFields.map((f, i) => i === index ? { ...f, [part]: value } : f);
|
|
306
|
-
updateGroupData({ examinerFields: newFields });
|
|
307
|
-
setValue('examinerFields', newFields, { shouldDirty: true });
|
|
308
|
-
};
|
|
309
|
-
const handleAddExaminerField = () => {
|
|
310
|
-
const newFields = [...groupExaminerFields, { key: '', answer: '' }];
|
|
311
|
-
updateGroupData({ examinerFields: newFields });
|
|
312
|
-
setValue('examinerFields', newFields, { shouldDirty: true });
|
|
313
|
-
};
|
|
314
|
-
const handleRemoveExaminerField = (index) => {
|
|
315
|
-
const newFields = groupExaminerFields.filter((_, i) => i !== index);
|
|
316
|
-
updateGroupData({ examinerFields: newFields });
|
|
317
|
-
setValue('examinerFields', newFields, { shouldDirty: true });
|
|
318
|
-
};
|
|
319
|
-
// ---------------------------------------------------------------
|
|
320
|
-
// Handlers for Expected Answers
|
|
321
|
-
// ---------------------------------------------------------------
|
|
322
|
-
const addExpectedAnswer = () => {
|
|
323
|
-
setValue('expectedAnswers', [...watchedExpectedAnswers, ''], { shouldValidate: true, shouldDirty: true });
|
|
324
|
-
};
|
|
325
|
-
const handleExpectedAnswerChange = (index, value) => {
|
|
326
|
-
const newAnswers = [...watchedExpectedAnswers];
|
|
327
|
-
newAnswers[index] = value;
|
|
328
|
-
setValue('expectedAnswers', newAnswers, { shouldValidate: true, shouldDirty: true });
|
|
329
|
-
};
|
|
330
|
-
const removeExpectedAnswer = (index) => {
|
|
331
|
-
if (watchedExpectedAnswers.length > 1) {
|
|
332
|
-
const newAnswers = watchedExpectedAnswers.filter((_, i) => i !== index);
|
|
333
|
-
setValue('expectedAnswers', newAnswers, { shouldValidate: true, shouldDirty: true });
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
// ---------------------------------------------------------------
|
|
337
|
-
// Display helpers
|
|
338
|
-
// ---------------------------------------------------------------
|
|
339
|
-
const displayQuestionNumber = typeof questionIndex === 'number' && !isNaN(questionIndex)
|
|
340
|
-
? questionIndex + 1
|
|
341
|
-
: (typeof questionIndexInPart === 'number' && !isNaN(questionIndexInPart)
|
|
342
|
-
? questionIndexInPart + 1
|
|
343
|
-
: '?');
|
|
344
|
-
const modeLabel = viewMode === 'ANSWER' ? 'Part 2a (Examiner hỏi - Candidate trả lời)' : 'Part 2b (Candidate hỏi - Examiner trả lời)';
|
|
345
|
-
const audioLabel = viewMode === 'ANSWER' ? 'Audio câu hỏi của Examiner' : 'Audio câu trả lời của Examiner';
|
|
346
|
-
const answerLabel = viewMode === 'ANSWER' ? 'Đáp án mong muốn (Candidate nói gì)' : 'Câu hỏi mong muốn (Candidate hỏi gì)';
|
|
347
|
-
const answerPlaceholder = viewMode === 'ANSWER' ? "VD: It's in King Road." : "VD: Where is Alex's house?";
|
|
348
|
-
// Color scheme based on viewMode
|
|
349
|
-
const modeColors = viewMode === 'ANSWER'
|
|
350
|
-
? { badge: 'bg-blue-50 text-blue-700 border-blue-200', icon: '📣', gradientFrom: 'from-blue-500', gradientTo: 'to-cyan-500' }
|
|
351
|
-
: { badge: 'bg-purple-50 text-purple-700 border-purple-200', icon: '🎤', gradientFrom: 'from-purple-500', gradientTo: 'to-pink-500' };
|
|
352
|
-
const [isClientMode, setIsClientMode] = useState(false);
|
|
353
|
-
// ─── Preview Mode ────────────────────────────────────────────────
|
|
354
|
-
if (isClientMode) {
|
|
355
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx(CardTitle, { className: "text-lg font-bold", children: "\uD83D\uDC41\uFE0F Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc (Preview)" }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i ch\u1EC9nh s\u1EEDa"] })] }) }) }), _jsx(ListenAndSpeakInfoExchangeClient, { questionData: {
|
|
356
|
-
audioUrl: watchedAudioUrl,
|
|
357
|
-
title: groupTitle,
|
|
358
|
-
imageUrl: groupImageUrl,
|
|
359
|
-
fields: groupCandidateFields,
|
|
360
|
-
candidateFields: groupCandidateFields,
|
|
361
|
-
examinerFields: groupExaminerFields,
|
|
362
|
-
expectedAnswers: watchedExpectedAnswers,
|
|
363
|
-
points: watchedPoints,
|
|
364
|
-
explanation: watchedExplanation,
|
|
365
|
-
}, viewMode: viewMode })] }));
|
|
366
|
-
}
|
|
367
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsx(CreatorGuide, { colorScheme: "blue", steps: [
|
|
368
|
-
{ text: 'Nhập tiêu đề chung cho bảng thông tin (ví dụ: Shopping List, Holiday Plans)' },
|
|
369
|
-
{ text: 'Upload hình ảnh minh họa nếu có' },
|
|
370
|
-
{ text: 'Thiết lập bảng thông tin Candidate và Examiner — mỗi bảng có các cặp key/value' },
|
|
371
|
-
{ text: 'Upload audio câu hỏi cho từng câu' },
|
|
372
|
-
{ text: 'Chọn chế độ: ANSWER (trả lời) hoặc ASK (đặt câu hỏi)' },
|
|
373
|
-
{ text: 'Nhập đáp án/câu hỏi mong muốn và giải thích' },
|
|
374
|
-
], tips: ['Trường có giá trị "?" là trường học sinh cần tìm câu trả lời'] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50", children: [_jsx("div", { className: "h-1 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-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 shadow-sm", children: _jsx(ListChecks, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsxs(CardTitle, { className: "text-lg font-bold text-gray-900", children: ["Trao \u0111\u1ED5i th\u00F4ng tin \u2014 C\u00E2u #", displayQuestionNumber] }), _jsx("p", { className: "text-sm text-gray-500", children: modeLabel })] })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !watchedAudioUrl, className: "gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 disabled:opacity-50", children: [_jsx(Eye, { className: "h-4 w-4" }), "Preview"] })] }) })] }), _jsxs("div", { className: `inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semibold border ${modeColors.badge}`, children: [modeColors.icon, " ", viewMode === 'ANSWER' ? 'Examiner → Candidate' : 'Candidate → Examiner', _jsx("span", { className: "text-gray-400", children: "|" }), " ", modeLabel] }), isFirstInGroup ? (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-teal-500 via-emerald-500 to-cyan-500" }), _jsx(CardHeader, { className: "pb-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-teal-500 to-emerald-600 shadow-md", children: _jsx(Table2, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-lg font-bold text-gray-900", children: "Th\u00F4ng tin chung" }), isGrouped && (_jsxs("span", { className: "inline-flex items-center rounded-full border border-teal-300 bg-teal-100 px-2.5 py-0.5 text-xs font-semibold text-teal-700", children: ["Nh\u00F3m ", currentGroupNumber] }))] }), _jsx("p", { className: "text-sm text-gray-500", children: "D\u00F9ng chung cho t\u1EA5t c\u1EA3 c\u00E2u h\u1ECFi trong nh\u00F3m \u2014 Ti\u00EAu \u0111\u1EC1, h\u00ECnh \u1EA3nh, v\u00E0 2 b\u1EA3ng th\u00F4ng tin" })] })] }) }), _jsxs(CardContent, { className: "space-y-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-sm font-semibold text-gray-800", children: [_jsx(MessageSquareText, { className: "h-4 w-4 text-teal-500" }), "Ti\u00EAu \u0111\u1EC1 ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { value: groupTitle, onChange: (e) => {
|
|
375
|
-
updateGroupData({ title: e.target.value });
|
|
376
|
-
setValue('title', e.target.value, { shouldDirty: true });
|
|
377
|
-
}, placeholder: "VD: Katy's house", className: "border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-200" })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-sm font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-teal-500" }), "H\u00ECnh \u1EA3nh minh ho\u1EA1"] }), _jsx(FileUpload, { accept: "image/*", value: groupImageUrl, onChange: (url) => {
|
|
378
|
-
updateGroupData({ imageUrl: url });
|
|
379
|
-
setValue('imageUrl', url, { shouldDirty: true });
|
|
380
|
-
}, placeholder: "Upload h\u00ECnh \u1EA3nh", label: "", autoUpload: true, prefix: "info-exchange" }), groupImageUrl && (_jsx(ImagePreview, { imageUrl: groupImageUrl, label: "H\u00ECnh \u1EA3nh minh ho\u1EA1" }))] }), _jsxs("div", { className: "space-y-5", children: [_jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "rounded-xl border border-teal-200 overflow-hidden shadow-sm bg-white", children: [_jsx("div", { className: "bg-gradient-to-r from-teal-600 to-emerald-600 px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-white/20 backdrop-blur-sm", children: _jsx(Table2, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx("h4", { className: "text-sm font-bold text-white tracking-wide", children: "Candidate's Copy" }), _jsx("p", { className: "text-[10px] text-teal-100 font-medium", children: "B\u1EA3ng th\u00F4ng tin h\u1ECDc sinh nh\u00ECn th\u1EA5y" })] })] }), _jsx("div", { className: "flex items-center gap-2", children: _jsxs("span", { className: "inline-flex items-center rounded-full bg-white/20 backdrop-blur-sm px-2 py-0.5 text-[10px] font-bold text-white", children: [groupCandidateFields.length, " d\u00F2ng"] }) })] }) }), _jsx("div", { className: "bg-teal-50/60 border-b border-teal-100 px-4 py-2", children: _jsx("p", { className: "text-xs font-semibold text-teal-800 text-center", children: groupTitle || '(Chưa nhập tiêu đề)' }) }), _jsxs("div", { className: "grid grid-cols-[40px_1fr_1fr_36px] bg-gradient-to-r from-teal-50/80 to-emerald-50/80 border-b border-teal-100", children: [_jsx("div", { className: "px-2 py-2 text-[10px] font-bold text-teal-600 uppercase tracking-wider text-center", children: "#" }), _jsx("div", { className: "px-2 py-2 text-[10px] font-bold text-teal-600 uppercase tracking-wider", children: "Key" }), _jsx("div", { className: "px-2 py-2 text-[10px] font-bold text-teal-600 uppercase tracking-wider", children: "Value" }), _jsx("div", {})] }), _jsx("div", { className: "divide-y divide-gray-100", children: groupCandidateFields.map((field, index) => (_jsxs("div", { className: "grid grid-cols-[40px_1fr_1fr_36px] items-center bg-white hover:bg-teal-50/30 transition-colors group/row", children: [_jsx("div", { className: "px-2 py-2 flex justify-center", children: _jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-teal-100 text-teal-700 font-bold text-[11px]", children: index + 1 }) }), _jsx("div", { className: "px-1.5 py-1.5", children: _jsx(Input, { value: field.key, onChange: (e) => handleUpdateCandidateField(index, 'key', e.target.value), placeholder: "VD: Where", className: "h-8 text-sm border-gray-200 bg-white focus:border-teal-400 focus:ring-teal-200 transition-all" }) }), _jsx("div", { className: "px-1.5 py-1.5", children: _jsx(Input, { value: field.answer, onChange: (e) => handleUpdateCandidateField(index, 'answer', e.target.value), placeholder: "VD: King Road", className: "h-8 text-sm border-gray-200 bg-white focus:border-teal-400 focus:ring-teal-200 transition-all" }) }), _jsx("div", { className: "px-1 py-1.5 flex justify-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => handleRemoveCandidateField(index), disabled: groupCandidateFields.length <= 1, className: "h-7 w-7 text-gray-300 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/row:opacity-100 transition-all disabled:opacity-0 disabled:group-hover/row:opacity-30", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) })] }, index))) }), _jsx("div", { className: "border-t border-dashed border-teal-200 bg-teal-50/30", children: _jsxs("button", { type: "button", onClick: handleAddCandidateField, className: "w-full flex items-center justify-center gap-1.5 py-2.5 text-xs font-semibold text-teal-600 hover:text-teal-700 hover:bg-teal-50 transition-colors cursor-pointer", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm d\u00F2ng"] }) })] }) }), _jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "rounded-xl border border-amber-200 overflow-hidden shadow-sm bg-white", children: [_jsx("div", { className: "bg-gradient-to-r from-amber-600 to-orange-600 px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-white/20 backdrop-blur-sm", children: _jsx(Table2, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx("h4", { className: "text-sm font-bold text-white tracking-wide", children: "Examiner's Copy" }), _jsx("p", { className: "text-[10px] text-amber-100 font-medium", children: "B\u1EA3ng th\u00F4ng tin gi\u00E1m kh\u1EA3o nh\u00ECn th\u1EA5y" })] })] }), _jsx("div", { className: "flex items-center gap-2", children: _jsxs("span", { className: "inline-flex items-center rounded-full bg-white/20 backdrop-blur-sm px-2 py-0.5 text-[10px] font-bold text-white", children: [groupExaminerFields.length, " d\u00F2ng"] }) })] }) }), _jsx("div", { className: "bg-amber-50/60 border-b border-amber-100 px-4 py-2", children: _jsx("p", { className: "text-xs font-semibold text-amber-800 text-center", children: groupTitle || '(Chưa nhập tiêu đề)' }) }), _jsxs("div", { className: "grid grid-cols-[40px_1fr_1fr_36px] bg-gradient-to-r from-amber-50/80 to-orange-50/80 border-b border-amber-100", children: [_jsx("div", { className: "px-2 py-2 text-[10px] font-bold text-amber-600 uppercase tracking-wider text-center", children: "#" }), _jsx("div", { className: "px-2 py-2 text-[10px] font-bold text-amber-600 uppercase tracking-wider", children: "Key" }), _jsx("div", { className: "px-2 py-2 text-[10px] font-bold text-amber-600 uppercase tracking-wider", children: "Value" }), _jsx("div", {})] }), _jsx("div", { className: "divide-y divide-gray-100", children: groupExaminerFields.map((field, index) => (_jsxs("div", { className: "grid grid-cols-[40px_1fr_1fr_36px] items-center bg-white hover:bg-amber-50/30 transition-colors group/row", children: [_jsx("div", { className: "px-2 py-2 flex justify-center", children: _jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-amber-100 text-amber-700 font-bold text-[11px]", children: index + 1 }) }), _jsx("div", { className: "px-1.5 py-1.5", children: _jsx(Input, { value: field.key, onChange: (e) => handleUpdateExaminerField(index, 'key', e.target.value), placeholder: "VD: Where", className: "h-8 text-sm border-gray-200 bg-white focus:border-amber-400 focus:ring-amber-200 transition-all" }) }), _jsx("div", { className: "px-1.5 py-1.5", children: _jsx(Input, { value: field.answer, onChange: (e) => handleUpdateExaminerField(index, 'answer', e.target.value), placeholder: "VD: King Road", className: "h-8 text-sm border-gray-200 bg-white focus:border-amber-400 focus:ring-amber-200 transition-all" }) }), _jsx("div", { className: "px-1 py-1.5 flex justify-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => handleRemoveExaminerField(index), disabled: groupExaminerFields.length <= 1, className: "h-7 w-7 text-gray-300 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/row:opacity-100 transition-all disabled:opacity-0 disabled:group-hover/row:opacity-30", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) })] }, index))) }), _jsx("div", { className: "border-t border-dashed border-amber-200 bg-amber-50/30", children: _jsxs("button", { type: "button", onClick: handleAddExaminerField, className: "w-full flex items-center justify-center gap-1.5 py-2.5 text-xs font-semibold text-amber-600 hover:text-amber-700 hover:bg-amber-50 transition-colors cursor-pointer", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm d\u00F2ng"] }) })] }) })] })] })] })) : (
|
|
381
|
-
/* Read-only view of the shared group data for non-first questions */
|
|
382
|
-
_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow shadow-teal-100/30", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-teal-400 via-emerald-400 to-cyan-400" }), _jsx(CardHeader, { className: "pb-3", 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-teal-100 shadow-sm", children: _jsx(Table2, { className: "h-4 w-4 text-teal-600" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Th\u00F4ng tin chung" }), _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 ", currentGroupNumber, " \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: _jsxs("div", { className: "space-y-3", children: [groupTitle && (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg bg-teal-50/50 border border-teal-100", children: [_jsx(MessageSquareText, { className: "h-4 w-4 text-teal-500 shrink-0" }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: groupTitle })] })), groupImageUrl && (_jsx(ImagePreview, { imageUrl: groupImageUrl, label: groupTitle || 'Hình ảnh minh hoạ' })), _jsxs("div", { className: "space-y-4", children: [groupCandidateFields.length > 0 && (_jsxs("div", { className: "rounded-xl overflow-hidden border border-teal-200 shadow-sm", children: [_jsx("div", { className: "bg-gradient-to-r from-teal-600 to-emerald-600 px-3 py-2.5", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded bg-white/20", children: _jsx(Table2, { className: "h-3 w-3 text-white" }) }), _jsx("span", { className: "text-xs font-bold text-white tracking-wide", children: "Candidate's Copy" })] }), _jsxs("span", { className: "text-[10px] font-semibold text-teal-100 bg-white/15 rounded-full px-1.5 py-0.5", children: [groupCandidateFields.length, " d\u00F2ng"] })] }) }), _jsx("div", { className: "bg-teal-50/60 text-center font-semibold text-xs py-2 border-b border-teal-100 text-teal-800", children: groupTitle || '(Chưa nhập tiêu đề)' }), _jsxs("div", { className: "grid grid-cols-2 bg-teal-50/40 border-b border-teal-100", children: [_jsx("div", { className: "px-3 py-1.5 text-[10px] font-bold text-teal-600 uppercase tracking-wider", children: "Key" }), _jsx("div", { className: "px-3 py-1.5 text-[10px] font-bold text-teal-600 uppercase tracking-wider", children: "Value" })] }), _jsx("div", { className: "divide-y divide-gray-100", children: groupCandidateFields.map((row, index) => (_jsxs("div", { className: "grid grid-cols-2 bg-white hover:bg-teal-50/20 transition-colors", children: [_jsx("div", { className: "px-3 py-2 text-sm font-medium text-gray-700", children: row.key || _jsx("span", { className: "text-gray-300 italic", children: '(Chưa nhập)' }) }), _jsx("div", { className: "px-3 py-2 text-sm text-gray-800", children: row.answer || _jsx("span", { className: "text-gray-300", children: '—' }) })] }, index))) })] })), groupExaminerFields.length > 0 && (_jsxs("div", { className: "rounded-xl overflow-hidden border border-amber-200 shadow-sm", children: [_jsx("div", { className: "bg-gradient-to-r from-amber-600 to-orange-600 px-3 py-2.5", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded bg-white/20", children: _jsx(Table2, { className: "h-3 w-3 text-white" }) }), _jsx("span", { className: "text-xs font-bold text-white tracking-wide", children: "Examiner's Copy" })] }), _jsxs("span", { className: "text-[10px] font-semibold text-amber-100 bg-white/15 rounded-full px-1.5 py-0.5", children: [groupExaminerFields.length, " d\u00F2ng"] })] }) }), _jsx("div", { className: "bg-amber-50/60 text-center font-semibold text-xs py-2 border-b border-amber-100 text-amber-800", children: groupTitle || '(Chưa nhập tiêu đề)' }), _jsxs("div", { className: "grid grid-cols-2 bg-amber-50/40 border-b border-amber-100", children: [_jsx("div", { className: "px-3 py-1.5 text-[10px] font-bold text-amber-600 uppercase tracking-wider", children: "Key" }), _jsx("div", { className: "px-3 py-1.5 text-[10px] font-bold text-amber-600 uppercase tracking-wider", children: "Value" })] }), _jsx("div", { className: "divide-y divide-gray-100", children: groupExaminerFields.map((row, index) => (_jsxs("div", { className: "grid grid-cols-2 bg-white hover:bg-amber-50/20 transition-colors", children: [_jsx("div", { className: "px-3 py-2 text-sm font-medium text-gray-700", children: row.key || _jsx("span", { className: "text-gray-300 italic", children: '(Chưa nhập)' }) }), _jsx("div", { className: "px-3 py-2 text-sm text-gray-800", children: row.answer || _jsx("span", { className: "text-gray-300", children: '—' }) })] }, index))) })] }))] }), groupCandidateFields.length === 0 && groupExaminerFields.length === 0 && (_jsx("div", { className: "text-center py-4 text-sm text-gray-400 italic", children: "Ch\u01B0a c\u00F3 d\u1EEF li\u1EC7u b\u1EA3ng th\u00F4ng tin" }))] }) })] })), _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-500 via-purple-500 to-pink-500" }), _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: [_jsxs(CardTitle, { className: "text-lg font-bold text-gray-900", children: ["C\u00E2u h\u1ECFi #", displayQuestionNumber] }), _jsx("p", { className: "text-sm text-gray-500", children: viewMode === 'ANSWER'
|
|
383
|
-
? 'Học sinh nghe câu hỏi của Examiner và trả lời'
|
|
384
|
-
: 'Học sinh đặt câu hỏi cho Examiner' })] })] }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsx("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: _jsx("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 }) }) }) }), _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" }), audioLabel, " ", _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-info-exchange-${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: "info-exchange" }) }), _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 shrink-0 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" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-lg font-bold text-gray-900", children: "\u0110\u00E1p \u00E1n mong mu\u1ED1n" }), _jsxs("p", { className: "text-sm text-gray-500", children: [watchedExpectedAnswers.filter(a => a.trim()).length, " \u0111\u00E1p \u00E1n \u0111\u00E3 nh\u1EADp"] })] })] }), _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 text-xs text-blue-700", children: [_jsx(Info, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: "Ch\u1EA5m th\u1EE7 c\u00F4ng: gi\u00E1o vi\u00EAn tham kh\u1EA3o \u0111\u00E1p \u00E1n \u0111\u1EC3 ch\u1EA5m \u00B7 Ch\u1EA5m t\u1EF1 \u0111\u1ED9ng: AI so s\u00E1nh gi\u1ECDng n\u00F3i h\u1ECDc sinh v\u1EDBi \u0111\u00E1p \u00E1n." })] }), _jsx(FormField, { control: control, name: "expectedAnswers", render: () => (_jsxs(FormItem, { children: [_jsx("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-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-green-100 text-green-700 text-sm font-bold mt-1", children: index + 1 }), _jsxs("div", { className: "flex-1", children: [_jsx(Input, { placeholder: answerPlaceholder, 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: "mt-0.5 h-8 w-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/answer:opacity-100 transition-opacity", "aria-label": `Xóa đáp án ${index + 1}`, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }, index))) }), form.formState.errors?.expectedAnswers?.message && (_jsx(FormMessage, {}))] })) })] })] }), _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 }) })] })] })] }));
|
|
385
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useEffect as r,useMemo as a,useRef as i,useState as l}from"react";import{Card as s,CardContent as n,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 x,FormField as b,FormItem as u,FormMessage as f}from"../../../../components/ui/form";import{FileUpload as N}from"../../../../components/ui/file-upload";import{PointsInput as v}from"../../../../components/ui/points-input";import{useInfoExchangeGroup as y}from"../../_shared/hooks/useInfoExchangeGroup";import{usePresignedFileUrl as w}from"../../../../shared/lib/hooks";import{listenAndSpeakInfoExchangeSchema as k}from"../../../../shared/lib/validation/listen-and-speak-info-exchange.validation";import{Plus as C,Trash2 as F,Mic as U,Volume2 as A,Table2 as S,MessageSquareText as D,ImageIcon as j,BookOpen as E,Eye as V,Pencil as T,Info as R,ListChecks as _}from"lucide-react";import{useReactHookForm as W}from"../../../../shared/lib/hooks/useReactHookForm";import{useDebouncedCallback as I}from"../../../../shared/lib/hooks/useDebouncedCallback";import{CreatorGuide as J}from"../../components/CreatorGuide";import{ListenAndSpeakInfoExchangeClient as K}from"./ListenAndSpeakInfoExchangeClient";function O({imageUrl:t,label:r}){const{previewUrl:a}=w(t);return a?e("div",{className:"mt-2 rounded-xl border border-gray-200 overflow-hidden bg-white shadow-sm",children:e("img",{src:a,alt:r,className:"max-h-80 w-full object-contain bg-gray-50 p-2"})}):null}const z=[{key:"",answer:""}];export function ListenAndSpeakInfoExchangeCreator({questionIndex:w,initialData:H,onChange:q,validationRef:G,onUnsavedChangesChange:P,isFirstInGroup:B,partId:L,groups:$,questionIndexInPart:M,viewMode:Q="ANSWER"}){const X=Boolean($&&$.length>0),Y=a(()=>{if(void 0!==B)return B;if(!$||!$.length||void 0===M)return!0;let e=0;for(const t of $){if(M===e)return!0;e+=t.questionCount}return!1},[B,$,M]),Z=a(()=>{if($&&$.length>0){if(void 0===M)return 1;let e=0;for(let t=0;t<$.length;t++)if(e+=$[t].questionCount,M<e)return t+1;return 1}const e=H;return e?.groupNumber??1},[$,M,H]),{title:ee,imageUrl:te,fields:re,candidateFields:ae,examinerFields:ie,updateGroupData:le}=y(L,Z),se=i(null);r(()=>{if(Y&&H){const e=JSON.stringify({title:H.title,imageUrl:H.imageUrl,candidateFields:H.candidateFields,examinerFields:H.examinerFields,fields:H.fields});if(se.current===e)return;se.current=e;const t=H.candidateFields&&H.candidateFields.length>0?H.candidateFields:H.fields&&H.fields.length>0?H.fields:z,r=H.examinerFields&&H.examinerFields.length>0?H.examinerFields:z;le({title:H.title||"",imageUrl:H.imageUrl||"",fields:t,candidateFields:t,examinerFields:r})}},[Y,H,le]);const ne=ee||H?.title||"",de=te||H?.imageUrl||"",ce=ae&&ae.length>0?ae:H?.candidateFields&&H.candidateFields.length>0?H.candidateFields:H?.fields&&H.fields.length>0?H.fields:z,oe=ie&&ie.length>0?ie:H?.examinerFields&&H.examinerFields.length>0?H.examinerFields:z,me=W({zodSchema:k,defaultValues:{title:ne,imageUrl:de,fields:ce,candidateFields:ce,examinerFields:oe,audioUrl:H?.audioUrl||"",expectedAnswers:H?.expectedAnswers?.length?H.expectedAnswers:[""],points:H?.points??1,explanation:H?.explanation||""}}),{control:he,watch:ge,setValue:pe,trigger:xe,clearErrors:be}=me,ue=ge("audioUrl"),fe=ge("expectedAnswers"),Ne=ge("points"),ve=ge("explanation"),ye=i(H),we=i(!1),ke=i(q),Ce=i("");r(()=>{ke.current=q},[q]);const Fe=I(e=>{ke.current?.(e)},300);r(()=>{P?.(me.formState.isDirty)},[me.formState.isDirty,P]),r(()=>{const e={title:ne,imageUrl:de,fields:ce,candidateFields:ce,examinerFields:oe,audioUrl:H?.audioUrl||"",expectedAnswers:H?.expectedAnswers?.length?H.expectedAnswers:[""],points:H?.points??1,explanation:H?.explanation||""};if(!(JSON.stringify(ye.current)!==JSON.stringify(H)))return;ye.current=H;const t={audioUrl:ue,expectedAnswers:fe,points:Ne,explanation:ve},r=t.points===e.points&&t.explanation===e.explanation&&t.audioUrl===e.audioUrl&&JSON.stringify(t.expectedAnswers)===JSON.stringify(e.expectedAnswers);if(r&&!0===H?.isCompleted)return;!r&&(e.audioUrl!==t.audioUrl||JSON.stringify(e.expectedAnswers)!==JSON.stringify(t.expectedAnswers))&&(we.current=!0,me.reset(e),setTimeout(()=>{we.current=!1},150))},[H,me,ue,fe,ve,Ne,ne,de,ce,oe]);const Ue=JSON.stringify(fe);r(()=>{if(we.current){const e=JSON.stringify({title:ne,imageUrl:de,fields:ce,candidateFields:ce,examinerFields:oe,audioUrl:ue,expectedAnswers:fe,points:Ne,explanation:ve,groupNumber:X?Z:void 0});return void(Ce.current=e)}const e={title:ne,imageUrl:de,fields:ce,candidateFields:ce,examinerFields:oe,audioUrl:ue,expectedAnswers:fe,points:Ne,explanation:ve,groupNumber:X?Z:void 0},t=JSON.stringify(e);Ce.current!==t&&(Ce.current=t,Fe(e))},[ue,Ue,Ne,ve,ne,de,ce,oe,X,Z,Fe]),r(()=>{G&&(G.current={triggerValidation:async()=>{pe("title",ne),pe("imageUrl",de),pe("fields",ce),pe("candidateFields",ce),pe("examinerFields",oe);return await xe()},getFormData:()=>({...me.getValues(),title:ne,imageUrl:de,fields:ce,candidateFields:ce,examinerFields:oe,groupNumber:X?Z:void 0})})},[xe,G,me,ne,de,ce,oe,X,Z,pe]),r(()=>{P?.(me.formState.isDirty)},[me.formState.isDirty,P]);const Ae=(e,t,r)=>{const a=ce.map((a,i)=>i===e?{...a,[t]:r}:a);le({candidateFields:a,fields:a}),pe("candidateFields",a,{shouldDirty:!0}),pe("fields",a,{shouldDirty:!0})},Se=(e,t,r)=>{const a=oe.map((a,i)=>i===e?{...a,[t]:r}:a);le({examinerFields:a}),pe("examinerFields",a,{shouldDirty:!0})},De="number"!=typeof w||isNaN(w)?"number"!=typeof M||isNaN(M)?"?":M+1:w+1,je="ANSWER"===Q?"Part 2a (Examiner hỏi - Candidate trả lời)":"Part 2b (Candidate hỏi - Examiner trả lời)",Ee="ANSWER"===Q?"Audio câu hỏi của Examiner":"Audio câu trả lời của Examiner",Ve="ANSWER"===Q?"VD: It's in King Road.":"VD: Where is Alex's house?",Te="ANSWER"===Q?{badge:"bg-blue-50 text-blue-700 border-blue-200",icon:"📣",gradientFrom:"from-blue-500",gradientTo:"to-cyan-500"}:{badge:"bg-purple-50 text-purple-700 border-purple-200",icon:"🎤",gradientFrom:"from-purple-500",gradientTo:"to-pink-500"},[Re,_e]=l(!1);return t("div",Re?{className:"space-y-6",children:[e(s,{children:e(d,{children:t("div",{className:"flex items-center justify-between",children:[e(c,{className:"text-lg font-bold",children:"👁️ Chế độ xem trước (Preview)"}),t(m,{variant:"outline",size:"sm",onClick:()=>_e(!1),children:[e(T,{className:"mr-2 h-4 w-4"}),"Quay lại chỉnh sửa"]})]})})}),e(K,{questionData:{audioUrl:ue,title:ne,imageUrl:de,fields:ce,candidateFields:ce,examinerFields:oe,expectedAnswers:fe,points:Ne,explanation:ve},viewMode:Q})]}:{className:"space-y-6",children:[e(J,{colorScheme:"blue",steps:[{text:"Nhập tiêu đề chung cho bảng thông tin (ví dụ: Shopping List, Holiday Plans)"},{text:"Upload hình ảnh minh họa nếu có"},{text:"Thiết lập bảng thông tin Candidate và Examiner — mỗi bảng có các cặp key/value"},{text:"Upload audio câu hỏi cho từng câu"},{text:"Chọn chế độ: ANSWER (trả lời) hoặc ASK (đặt câu hỏi)"},{text:"Nhập đáp án/câu hỏi mong muốn và giải thích"}],tips:['Trường có giá trị "?" là trường học sinh cần tìm câu trả lời']}),t(s,{className:"overflow-hidden border-0 bg-white shadow-lg shadow-blue-100/50",children:[e("div",{className:"h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500"}),e(d,{className:"pb-4",children:t("div",{className:"flex items-center justify-between",children:[t("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-blue-500 to-indigo-600 shadow-sm",children:e(_,{className:"h-4 w-4 text-white"})}),t("div",{children:[t(c,{className:"text-lg font-bold text-gray-900",children:["Trao đổi thông tin — Câu #",De]}),e("p",{className:"text-sm text-gray-500",children:je})]})]}),t(m,{type:"button",variant:"outline",size:"sm",onClick:()=>_e(!0),disabled:!ue,className:"gap-2 border-blue-200 text-blue-600 hover:bg-blue-50 disabled:opacity-50",children:[e(V,{className:"h-4 w-4"}),"Preview"]})]})})]}),t("div",{className:`inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semibold border ${Te.badge}`,children:[Te.icon," ","ANSWER"===Q?"Examiner → Candidate":"Candidate → Examiner",e("span",{className:"text-gray-400",children:"|"})," ",je]}),t(s,Y?{className:"overflow-hidden border-0 bg-white shadow-lg shadow-teal-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-teal-500 via-emerald-500 to-cyan-500"}),e(d,{className:"pb-4",children:t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-teal-500 to-emerald-600 shadow-md",children:e(S,{className:"h-5 w-5 text-white"})}),t("div",{className:"flex-1",children:[t("div",{className:"flex items-center gap-2",children:[e(c,{className:"text-lg font-bold text-gray-900",children:"Thông tin chung"}),X&&t("span",{className:"inline-flex items-center rounded-full border border-teal-300 bg-teal-100 px-2.5 py-0.5 text-xs font-semibold text-teal-700",children:["Nhóm ",Z]})]}),e("p",{className:"text-sm text-gray-500",children:"Dùng chung cho tất cả câu hỏi trong nhóm — Tiêu đề, hình ảnh, và 2 bảng thông tin"})]})]})}),t(n,{className:"space-y-5",children:[t("div",{className:"space-y-2",children:[t(o,{className:"flex items-center gap-2 text-sm font-semibold text-gray-800",children:[e(D,{className:"h-4 w-4 text-teal-500"}),"Tiêu đề ",e("span",{className:"text-red-500",children:"*"})]}),e(h,{value:ne,onChange:e=>{le({title:e.target.value}),pe("title",e.target.value,{shouldDirty:!0})},placeholder:"VD: Katy's house",className:"border-gray-200 bg-white transition-all focus:border-teal-300 focus:ring-teal-200"})]}),t("div",{className:"space-y-2",children:[t(o,{className:"flex items-center gap-2 text-sm font-semibold text-gray-800",children:[e(j,{className:"h-4 w-4 text-teal-500"}),"Hình ảnh minh hoạ"]}),e(N,{accept:"image/*",value:de,onChange:e=>{le({imageUrl:e}),pe("imageUrl",e,{shouldDirty:!0})},placeholder:"Upload hình ảnh",label:"",autoUpload:!0,prefix:"info-exchange"}),de&&e(O,{imageUrl:de,label:"Hình ảnh minh hoạ"})]}),t("div",{className:"space-y-5",children:[e("div",{className:"space-y-3",children:t("div",{className:"rounded-xl border border-teal-200 overflow-hidden shadow-sm bg-white",children:[e("div",{className:"bg-gradient-to-r from-teal-600 to-emerald-600 px-4 py-3",children:t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2.5",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-white/20 backdrop-blur-sm",children:e(S,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h4",{className:"text-sm font-bold text-white tracking-wide",children:"Candidate's Copy"}),e("p",{className:"text-[10px] text-teal-100 font-medium",children:"Bảng thông tin học sinh nhìn thấy"})]})]}),e("div",{className:"flex items-center gap-2",children:t("span",{className:"inline-flex items-center rounded-full bg-white/20 backdrop-blur-sm px-2 py-0.5 text-[10px] font-bold text-white",children:[ce.length," dòng"]})})]})}),e("div",{className:"bg-teal-50/60 border-b border-teal-100 px-4 py-2",children:e("p",{className:"text-xs font-semibold text-teal-800 text-center",children:ne||"(Chưa nhập tiêu đề)"})}),t("div",{className:"grid grid-cols-[40px_1fr_1fr_36px] bg-gradient-to-r from-teal-50/80 to-emerald-50/80 border-b border-teal-100",children:[e("div",{className:"px-2 py-2 text-[10px] font-bold text-teal-600 uppercase tracking-wider text-center",children:"#"}),e("div",{className:"px-2 py-2 text-[10px] font-bold text-teal-600 uppercase tracking-wider",children:"Key"}),e("div",{className:"px-2 py-2 text-[10px] font-bold text-teal-600 uppercase tracking-wider",children:"Value"}),e("div",{})]}),e("div",{className:"divide-y divide-gray-100",children:ce.map((r,a)=>t("div",{className:"grid grid-cols-[40px_1fr_1fr_36px] items-center bg-white hover:bg-teal-50/30 transition-colors group/row",children:[e("div",{className:"px-2 py-2 flex justify-center",children:e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-teal-100 text-teal-700 font-bold text-[11px]",children:a+1})}),e("div",{className:"px-1.5 py-1.5",children:e(h,{value:r.key,onChange:e=>Ae(a,"key",e.target.value),placeholder:"VD: Where",className:"h-8 text-sm border-gray-200 bg-white focus:border-teal-400 focus:ring-teal-200 transition-all"})}),e("div",{className:"px-1.5 py-1.5",children:e(h,{value:r.answer,onChange:e=>Ae(a,"answer",e.target.value),placeholder:"VD: King Road",className:"h-8 text-sm border-gray-200 bg-white focus:border-teal-400 focus:ring-teal-200 transition-all"})}),e("div",{className:"px-1 py-1.5 flex justify-center",children:e(m,{type:"button",variant:"ghost",size:"icon",onClick:()=>(e=>{const t=ce.filter((t,r)=>r!==e);le({candidateFields:t,fields:t}),pe("candidateFields",t,{shouldDirty:!0}),pe("fields",t,{shouldDirty:!0})})(a),disabled:ce.length<=1,className:"h-7 w-7 text-gray-300 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/row:opacity-100 transition-all disabled:opacity-0 disabled:group-hover/row:opacity-30",children:e(F,{className:"h-3.5 w-3.5"})})})]},a))}),e("div",{className:"border-t border-dashed border-teal-200 bg-teal-50/30",children:t("button",{type:"button",onClick:()=>{const e=[...ce,{key:"",answer:""}];le({candidateFields:e,fields:e}),pe("candidateFields",e,{shouldDirty:!0}),pe("fields",e,{shouldDirty:!0})},className:"w-full flex items-center justify-center gap-1.5 py-2.5 text-xs font-semibold text-teal-600 hover:text-teal-700 hover:bg-teal-50 transition-colors cursor-pointer",children:[e(C,{className:"h-3.5 w-3.5"}),"Thêm dòng"]})})]})}),e("div",{className:"space-y-3",children:t("div",{className:"rounded-xl border border-amber-200 overflow-hidden shadow-sm bg-white",children:[e("div",{className:"bg-gradient-to-r from-amber-600 to-orange-600 px-4 py-3",children:t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2.5",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-white/20 backdrop-blur-sm",children:e(S,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h4",{className:"text-sm font-bold text-white tracking-wide",children:"Examiner's Copy"}),e("p",{className:"text-[10px] text-amber-100 font-medium",children:"Bảng thông tin giám khảo nhìn thấy"})]})]}),e("div",{className:"flex items-center gap-2",children:t("span",{className:"inline-flex items-center rounded-full bg-white/20 backdrop-blur-sm px-2 py-0.5 text-[10px] font-bold text-white",children:[oe.length," dòng"]})})]})}),e("div",{className:"bg-amber-50/60 border-b border-amber-100 px-4 py-2",children:e("p",{className:"text-xs font-semibold text-amber-800 text-center",children:ne||"(Chưa nhập tiêu đề)"})}),t("div",{className:"grid grid-cols-[40px_1fr_1fr_36px] bg-gradient-to-r from-amber-50/80 to-orange-50/80 border-b border-amber-100",children:[e("div",{className:"px-2 py-2 text-[10px] font-bold text-amber-600 uppercase tracking-wider text-center",children:"#"}),e("div",{className:"px-2 py-2 text-[10px] font-bold text-amber-600 uppercase tracking-wider",children:"Key"}),e("div",{className:"px-2 py-2 text-[10px] font-bold text-amber-600 uppercase tracking-wider",children:"Value"}),e("div",{})]}),e("div",{className:"divide-y divide-gray-100",children:oe.map((r,a)=>t("div",{className:"grid grid-cols-[40px_1fr_1fr_36px] items-center bg-white hover:bg-amber-50/30 transition-colors group/row",children:[e("div",{className:"px-2 py-2 flex justify-center",children:e("div",{className:"flex h-6 w-6 items-center justify-center rounded-md bg-amber-100 text-amber-700 font-bold text-[11px]",children:a+1})}),e("div",{className:"px-1.5 py-1.5",children:e(h,{value:r.key,onChange:e=>Se(a,"key",e.target.value),placeholder:"VD: Where",className:"h-8 text-sm border-gray-200 bg-white focus:border-amber-400 focus:ring-amber-200 transition-all"})}),e("div",{className:"px-1.5 py-1.5",children:e(h,{value:r.answer,onChange:e=>Se(a,"answer",e.target.value),placeholder:"VD: King Road",className:"h-8 text-sm border-gray-200 bg-white focus:border-amber-400 focus:ring-amber-200 transition-all"})}),e("div",{className:"px-1 py-1.5 flex justify-center",children:e(m,{type:"button",variant:"ghost",size:"icon",onClick:()=>(e=>{const t=oe.filter((t,r)=>r!==e);le({examinerFields:t}),pe("examinerFields",t,{shouldDirty:!0})})(a),disabled:oe.length<=1,className:"h-7 w-7 text-gray-300 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/row:opacity-100 transition-all disabled:opacity-0 disabled:group-hover/row:opacity-30",children:e(F,{className:"h-3.5 w-3.5"})})})]},a))}),e("div",{className:"border-t border-dashed border-amber-200 bg-amber-50/30",children:t("button",{type:"button",onClick:()=>{const e=[...oe,{key:"",answer:""}];le({examinerFields:e}),pe("examinerFields",e,{shouldDirty:!0})},className:"w-full flex items-center justify-center gap-1.5 py-2.5 text-xs font-semibold text-amber-600 hover:text-amber-700 hover:bg-amber-50 transition-colors cursor-pointer",children:[e(C,{className:"h-3.5 w-3.5"}),"Thêm dòng"]})})]})})]})]})]}:{className:"overflow-hidden border-0 bg-white shadow shadow-teal-100/30",children:[e("div",{className:"h-1 bg-gradient-to-r from-teal-400 via-emerald-400 to-cyan-400"}),e(d,{className:"pb-3",children:t("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-teal-100 shadow-sm",children:e(S,{className:"h-4 w-4 text-teal-600"})}),t("div",{className:"flex-1",children:[t("div",{className:"flex items-center gap-2",children:[e(c,{className:"text-base font-semibold text-gray-800",children:"Thông tin chung"}),t("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 ",Z," — 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(n,{className:"pt-0",children:t("div",{className:"space-y-3",children:[ne&&t("div",{className:"flex items-center gap-2 px-3 py-2 rounded-lg bg-teal-50/50 border border-teal-100",children:[e(D,{className:"h-4 w-4 text-teal-500 shrink-0"}),e("span",{className:"text-sm font-medium text-gray-700",children:ne})]}),de&&e(O,{imageUrl:de,label:ne||"Hình ảnh minh hoạ"}),t("div",{className:"space-y-4",children:[ce.length>0&&t("div",{className:"rounded-xl overflow-hidden border border-teal-200 shadow-sm",children:[e("div",{className:"bg-gradient-to-r from-teal-600 to-emerald-600 px-3 py-2.5",children:t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-5 w-5 items-center justify-center rounded bg-white/20",children:e(S,{className:"h-3 w-3 text-white"})}),e("span",{className:"text-xs font-bold text-white tracking-wide",children:"Candidate's Copy"})]}),t("span",{className:"text-[10px] font-semibold text-teal-100 bg-white/15 rounded-full px-1.5 py-0.5",children:[ce.length," dòng"]})]})}),e("div",{className:"bg-teal-50/60 text-center font-semibold text-xs py-2 border-b border-teal-100 text-teal-800",children:ne||"(Chưa nhập tiêu đề)"}),t("div",{className:"grid grid-cols-2 bg-teal-50/40 border-b border-teal-100",children:[e("div",{className:"px-3 py-1.5 text-[10px] font-bold text-teal-600 uppercase tracking-wider",children:"Key"}),e("div",{className:"px-3 py-1.5 text-[10px] font-bold text-teal-600 uppercase tracking-wider",children:"Value"})]}),e("div",{className:"divide-y divide-gray-100",children:ce.map((r,a)=>t("div",{className:"grid grid-cols-2 bg-white hover:bg-teal-50/20 transition-colors",children:[e("div",{className:"px-3 py-2 text-sm font-medium text-gray-700",children:r.key||e("span",{className:"text-gray-300 italic",children:"(Chưa nhập)"})}),e("div",{className:"px-3 py-2 text-sm text-gray-800",children:r.answer||e("span",{className:"text-gray-300",children:"—"})})]},a))})]}),oe.length>0&&t("div",{className:"rounded-xl overflow-hidden border border-amber-200 shadow-sm",children:[e("div",{className:"bg-gradient-to-r from-amber-600 to-orange-600 px-3 py-2.5",children:t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"flex h-5 w-5 items-center justify-center rounded bg-white/20",children:e(S,{className:"h-3 w-3 text-white"})}),e("span",{className:"text-xs font-bold text-white tracking-wide",children:"Examiner's Copy"})]}),t("span",{className:"text-[10px] font-semibold text-amber-100 bg-white/15 rounded-full px-1.5 py-0.5",children:[oe.length," dòng"]})]})}),e("div",{className:"bg-amber-50/60 text-center font-semibold text-xs py-2 border-b border-amber-100 text-amber-800",children:ne||"(Chưa nhập tiêu đề)"}),t("div",{className:"grid grid-cols-2 bg-amber-50/40 border-b border-amber-100",children:[e("div",{className:"px-3 py-1.5 text-[10px] font-bold text-amber-600 uppercase tracking-wider",children:"Key"}),e("div",{className:"px-3 py-1.5 text-[10px] font-bold text-amber-600 uppercase tracking-wider",children:"Value"})]}),e("div",{className:"divide-y divide-gray-100",children:oe.map((r,a)=>t("div",{className:"grid grid-cols-2 bg-white hover:bg-amber-50/20 transition-colors",children:[e("div",{className:"px-3 py-2 text-sm font-medium text-gray-700",children:r.key||e("span",{className:"text-gray-300 italic",children:"(Chưa nhập)"})}),e("div",{className:"px-3 py-2 text-sm text-gray-800",children:r.answer||e("span",{className:"text-gray-300",children:"—"})})]},a))})]})]}),0===ce.length&&0===oe.length&&e("div",{className:"text-center py-4 text-sm text-gray-400 italic",children:"Chưa có dữ liệu bảng thông tin"})]})})]}),t(p,{...me,children:[t(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-500 via-purple-500 to-pink-500"}),e(d,{className:"pb-4",children:t("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(U,{className:"h-4 w-4 text-white"})}),t("div",{children:[t(c,{className:"text-lg font-bold text-gray-900",children:["Câu hỏi #",De]}),e("p",{className:"text-sm text-gray-500",children:"ANSWER"===Q?"Học sinh nghe câu hỏi của Examiner và trả lời":"Học sinh đặt câu hỏi cho Examiner"})]})]})}),t(n,{className:"space-y-6",children:[e("div",{className:"rounded-xl border border-gray-200 bg-gray-50 p-4",children:e("div",{className:"flex flex-col md:flex-row gap-6",children:e("div",{className:"flex-1",children:e(v,{...me.register("points",{valueAsNumber:!0}),error:me.formState.errors?.points?.message})})})}),t("div",{className:"space-y-2",children:[t(o,{className:"flex items-center gap-2 text-sm font-semibold text-gray-800",children:[e(A,{className:"h-4 w-4 text-indigo-500"}),Ee," ",e("span",{className:"text-red-500",children:"*"})]}),e(b,{control:he,name:"audioUrl",render:({field:r})=>t(u,{children:[e(x,{children:e(N,{id:`audio-info-exchange-${De}`,label:"",accept:"audio/*",value:r.value,onChange:e=>{r.onChange(e),be("audioUrl")},maxSize:10,placeholder:"Tải lên file âm thanh (mp3, wav...)",autoUpload:!0,prefix:"info-exchange"})}),e(f,{})]})})]})]})]}),t(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:t("div",{className:"flex items-center justify-between",children:[t("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-green-500 to-emerald-600 shadow-sm",children:e(D,{className:"h-4 w-4 text-white"})}),t("div",{children:[e(c,{className:"text-lg font-bold text-gray-900",children:"Đáp án mong muốn"}),t("p",{className:"text-sm text-gray-500",children:[fe.filter(e=>e.trim()).length," đáp án đã nhập"]})]})]}),t(m,{type:"button",variant:"outline",size:"sm",onClick:()=>{pe("expectedAnswers",[...fe,""],{shouldValidate:!0,shouldDirty:!0})},className:"gap-2 border-green-200 text-green-600 hover:bg-green-50",children:[e(C,{className:"h-4 w-4"}),"Thêm đáp án"]})]})}),t(n,{className:"space-y-4",children:[t("div",{className:"flex items-start gap-2 rounded-lg border border-blue-100 bg-blue-50/60 px-3 py-2 text-xs text-blue-700",children:[e(R,{className:"mt-0.5 h-3.5 w-3.5 shrink-0"}),e("span",{children:"Chấm thủ công: giáo viên tham khảo đáp án để chấm · Chấm tự động: AI so sánh giọng nói học sinh với đáp án."})]}),e(b,{control:he,name:"expectedAnswers",render:()=>t(u,{children:[e("div",{className:"space-y-3",children:fe.map((r,a)=>t("div",{className:"flex items-start gap-3 group/answer",children:[e("div",{className:"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-green-100 text-green-700 text-sm font-bold mt-1",children:a+1}),t("div",{className:"flex-1",children:[e(h,{placeholder:Ve,value:r||"",onChange:e=>((e,t)=>{const r=[...fe];r[e]=t,pe("expectedAnswers",r,{shouldValidate:!0,shouldDirty:!0})})(a,e.target.value),className:"border-gray-200 focus:border-green-300 focus:ring-green-200"}),me.formState.errors?.expectedAnswers?.[a]?.message&&e("p",{className:"mt-1 text-sm text-red-500",children:me.formState.errors.expectedAnswers[a]?.message})]}),fe.length>1&&e(m,{type:"button",variant:"ghost",size:"sm",onClick:()=>(e=>{if(fe.length>1){const t=fe.filter((t,r)=>r!==e);pe("expectedAnswers",t,{shouldValidate:!0,shouldDirty:!0})}})(a),className:"mt-0.5 h-8 w-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 opacity-0 group-hover/answer:opacity-100 transition-opacity","aria-label":`Xóa đáp án ${a+1}`,children:e(F,{className:"h-4 w-4"})})]},a))}),me.formState.errors?.expectedAnswers?.message&&e(f,{})]})})]})]}),t(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:t("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(E,{className:"h-4 w-4 text-white"})}),t("div",{children:[t("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(n,{children:e(g,{...me.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})})]})]})]})}
|