@tinyweb_dev/oe-exam-sdk 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api/contest-rooms.js +54 -1
- package/dist/api/contest-rooms.types.js +1 -1
- package/dist/api/exam-create.js +179 -1
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-entry.js +68 -1
- package/dist/api/exam-entry.types.js +1 -1
- package/dist/api/exam-questions.js +149 -1
- package/dist/api/exam-questions.types.js +1 -1
- package/dist/api/exam-taking.js +84 -1
- package/dist/api/exam-taking.types.d.ts +7 -0
- package/dist/api/exam-taking.types.js +1 -1
- package/dist/api/index.js +8 -1
- package/dist/api/result-review.js +62 -1
- package/dist/api/result-review.types.js +16 -1
- package/dist/components/CueCardEditor.js +58 -1
- package/dist/components/common/ResolvedImage.js +15 -1
- package/dist/components/exams/CompletionDialog.js +8 -1
- package/dist/components/exams/CreateExamInfoForm.js +84 -1
- package/dist/components/exams/CreateExamPageContainer.js +110 -1
- package/dist/components/exams/ExamCreator.js +287 -1
- package/dist/components/exams/ExamPartTabs.js +138 -1
- package/dist/components/exams/ExamPreviewDialog.d.ts +6 -7
- package/dist/components/exams/ExamPreviewDialog.js +121 -1
- package/dist/components/exams/ExamPreviewPartContent.d.ts +18 -0
- package/dist/components/exams/ExamPreviewPartContent.js +27 -0
- package/dist/components/exams/ExamPreviewSidebar.d.ts +17 -0
- package/dist/components/exams/ExamPreviewSidebar.js +37 -0
- package/dist/components/exams/ExamQuestionGrid.js +37 -1
- package/dist/components/exams/ExamQuestionsPageContainer.js +112 -1
- package/dist/components/exams/ExamViewDialog.js +61 -1
- package/dist/components/exams/PartAudioUploader.js +9 -1
- package/dist/components/exams/TemplateSelector.js +76 -1
- package/dist/components/exams/UnsavedChangesDialog.js +7 -1
- package/dist/components/exams/api.js +3 -1
- package/dist/components/exams/create-exam.texts.js +1 -1
- package/dist/components/exams/create-exam.validation.js +18 -1
- package/dist/components/exams/entry/EntryRestartDialog.js +11 -1
- package/dist/components/exams/entry/StudentExamEntryPageContainer.js +358 -1
- package/dist/components/exams/entry/exam-entry.states.js +26 -1
- package/dist/components/exams/entry/exam-entry.utils.js +55 -1
- package/dist/components/exams/entry/fullscreen-loading.js +18 -1
- package/dist/components/exams/entry/index.js +4 -1
- package/dist/components/exams/exam-preview.types.d.ts +30 -0
- package/dist/components/exams/exam-preview.types.js +62 -0
- package/dist/components/exams/exam-question-status.js +45 -1
- package/dist/components/exams/exam-template.utils.js +18 -1
- package/dist/components/exams/index.d.ts +1 -0
- package/dist/components/exams/index.js +19 -1
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +183 -1
- package/dist/components/exams/rooms/contest-rooms.utils.js +93 -1
- package/dist/components/exams/rooms/index.js +2 -1
- package/dist/components/exams/take/ExamTakingApiContext.js +25 -1
- package/dist/components/exams/take/ExamTakingPageContainer.js +955 -1
- package/dist/components/exams/take/GenericQuestionRenderer.js +14 -1
- package/dist/components/exams/take/components/QuestionRenderer.js +207 -1
- package/dist/components/exams/take/components/index.js +11 -1
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +8 -1
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +30 -1
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +63 -1
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +136 -1
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +379 -1
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.d.ts +20 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +141 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +131 -1
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +76 -1
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +68 -1
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +42 -1
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +131 -1
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +76 -1
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +68 -1
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +62 -1
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +51 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +171 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +198 -1
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderAndMatchGroupRenderer.d.ts +15 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderAndMatchGroupRenderer.js +36 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +92 -1
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +11 -1
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +56 -1
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +25 -1
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +85 -1
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +10 -1
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +15 -1
- package/dist/components/exams/take/components/speaking/SpeakingHeader.js +6 -1
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +8 -1
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +59 -1
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +7 -1
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +5 -1
- package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +120 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +73 -1
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +8 -1
- package/dist/components/exams/take/components/speaking/SpeakingSpontaneousQA.module.css +344 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +32 -1
- package/dist/components/exams/take/components/speaking/index.js +1 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +63 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +95 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +35 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +147 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +232 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +193 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +155 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +212 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +77 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +64 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +69 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +64 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +92 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +202 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +339 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +76 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +56 -1
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQACambridgeLayout.d.ts +24 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQACambridgeLayout.js +120 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQAInlineAudioPlayer.d.ts +8 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQAInlineAudioPlayer.js +94 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQASummerSkyAudioPlayer.d.ts +8 -0
- package/dist/components/exams/take/components/speaking/renderers/SpontaneousQASummerSkyAudioPlayer.js +101 -0
- package/dist/components/exams/take/components/speaking/renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/renderers/index.js +14 -1
- package/dist/components/exams/take/exam-taking.utils.d.ts +13 -0
- package/dist/components/exams/take/exam-taking.utils.js +203 -1
- package/dist/components/exams/take/hooks/index.js +2 -1
- package/dist/components/exams/take/hooks/useAnswerAutosave.js +71 -1
- package/dist/components/exams/take/hooks/useAttemptSubmit.js +38 -1
- package/dist/components/exams/take/hooks/useAutoSave.js +83 -1
- package/dist/components/exams/take/hooks/useExamCountdown.js +35 -1
- package/dist/components/exams/take/hooks/useExamTimer.js +97 -1
- package/dist/components/exams/take/hooks/useProctoringCamera.js +118 -1
- package/dist/components/exams/take/hooks/useProctoringSnapshot.js +76 -1
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +178 -1
- package/dist/components/exams/take/hooks/useSpeakingNavigation.js +51 -1
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +406 -1
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +136 -1
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRecorder.d.ts +11 -0
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRecorder.js +352 -0
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.d.ts +42 -0
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +166 -0
- package/dist/components/exams/take/hooks/useToeicAudio.js +35 -1
- package/dist/components/exams/take/index.js +6 -1
- package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -1
- package/dist/components/exams/take/types/speaking.types.js +7 -1
- package/dist/components/exams/take/types.js +4 -1
- package/dist/components/exams/take/utils/answer-transformers.js +452 -1
- package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.d.ts +21 -0
- package/dist/components/exams/take/utils/cambridge-yle-inline-speaking.js +44 -0
- package/dist/components/exams/take/utils/coloring-assignments.js +40 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.d.ts +21 -0
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +83 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +43 -2
- package/dist/components/exams/take/utils/question-transformers.js +1137 -1
- package/dist/components/features/exam-entry/entry-state-cards.js +19 -1
- package/dist/components/features/exam-entry/index.js +2 -1
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +121 -1
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +37 -1
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +15 -1
- package/dist/components/features/exam-entry/themed/cambridge-yle-waiting-parts.js +80 -1
- package/dist/components/features/exam-entry/themed/components/CloseButton.js +6 -1
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +17 -1
- package/dist/components/features/exam-entry/themed/components/ModeSelector.js +6 -1
- package/dist/components/features/exam-entry/themed/components/StartButton.js +12 -1
- package/dist/components/features/exam-entry/themed/components/index.js +4 -1
- package/dist/components/features/exam-entry/themed/index.js +3 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +43 -1
- package/dist/components/questions/_shared/config/question-types.config.js +362 -1
- package/dist/components/questions/_shared/constants/color-palette.js +21 -1
- package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +105 -1
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +222 -1
- package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +140 -1
- package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +123 -1
- package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +50 -1
- package/dist/components/questions/_shared/hooks/useQuestionFormState.js +54 -1
- package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +123 -1
- package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +121 -1
- package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +70 -1
- package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +194 -1
- package/dist/components/questions/_shared/index.js +7 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
- package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
- package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +3 -1
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/color-region.config.js +1 -1
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +2 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
- package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
- package/dist/components/questions/_shared/types/index.d.ts +1 -0
- package/dist/components/questions/_shared/types/index.js +14 -1
- package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +3 -1
- package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +3 -1
- package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
- package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
- package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/multiple-choice.type.d.ts +5 -0
- package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
- package/dist/components/questions/_shared/types/question-group.type.js +11 -1
- package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
- package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
- package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
- package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +37 -0
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.js +1 -0
- package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
- package/dist/components/questions/_shared/types/true-false-group.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.d.ts +20 -11
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
- package/dist/components/questions/_shared/types/word-order-and-match-group.type.d.ts +38 -0
- package/dist/components/questions/_shared/types/word-order-and-match-group.type.js +1 -0
- package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
- package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
- package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +3 -1
- package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
- package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/utils/answer-builder.js +79 -1
- package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +39 -1
- package/dist/components/questions/_shared/utils/id-generator.js +3 -1
- package/dist/components/questions/_shared/utils/parseFillBlankStem.d.ts +30 -0
- package/dist/components/questions/_shared/utils/parseFillBlankStem.js +69 -0
- package/dist/components/questions/_shared/utils/question-validation.js +21 -1
- package/dist/components/questions/components/ColorNode.js +85 -1
- package/dist/components/questions/components/ColorRegionClient.js +545 -1
- package/dist/components/questions/components/ColorRegionCreator.js +420 -1
- package/dist/components/questions/components/ColorRegionNode.js +59 -1
- package/dist/components/questions/components/CreatorGuide.js +103 -1
- package/dist/components/questions/components/NameNode.js +26 -1
- package/dist/components/questions/components/QuestionBankOption.js +46 -1
- package/dist/components/questions/components/QuestionBasicInfoForm.js +9 -1
- package/dist/components/questions/components/QuestionCommonFields.js +33 -1
- package/dist/components/questions/components/QuestionSearchDropdown.js +258 -1
- package/dist/components/questions/components/QuestionTypeSelector.js +32 -1
- package/dist/components/questions/components/QuestionTypeSpecificForms.js +6 -1
- package/dist/components/questions/components/RegionNode.js +94 -1
- package/dist/components/questions/components/index.js +14 -1
- package/dist/components/questions/creator/QuestionCreator.js +90 -1
- package/dist/components/questions/creator/QuestionGroupCreator.js +119 -1
- package/dist/components/questions/creator/dedicated-component-router.js +79 -1
- package/dist/components/questions/creator/index.js +3 -1
- package/dist/components/questions/creator/question-type-registry.js +90 -1
- package/dist/components/questions/groups/ArticlesGroupCard.js +121 -1
- package/dist/components/questions/groups/BasicQuestionGroupCard.js +50 -1
- package/dist/components/questions/groups/DocumentGroupCard.js +114 -1
- package/dist/components/questions/groups/MatchAnswerGroupCard.js +25 -1
- package/dist/components/questions/index.d.ts +1 -0
- package/dist/components/questions/index.js +11 -1
- package/dist/components/questions/question-bank/EditQuestionBankDialog.d.ts +2 -0
- package/dist/components/questions/question-bank/EditQuestionBankDialog.js +114 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +23 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.js +75 -0
- package/dist/components/questions/question-bank/difficulty.d.ts +15 -0
- package/dist/components/questions/question-bank/difficulty.js +33 -0
- package/dist/components/questions/question-bank/editor-adapters/choice-adapters.d.ts +5 -0
- package/dist/components/questions/question-bank/editor-adapters/choice-adapters.js +101 -0
- package/dist/components/questions/question-bank/editor-adapters/index.d.ts +5 -0
- package/dist/components/questions/question-bank/editor-adapters/index.js +4 -0
- package/dist/components/questions/question-bank/editor-adapters/media-adapters.d.ts +6 -0
- package/dist/components/questions/question-bank/editor-adapters/media-adapters.js +140 -0
- package/dist/components/questions/question-bank/editor-adapters/types.d.ts +1 -0
- package/dist/components/questions/question-bank/editor-adapters/types.js +1 -0
- package/dist/components/questions/question-bank/editor-adapters/unsupported-fallback.d.ts +8 -0
- package/dist/components/questions/question-bank/editor-adapters/unsupported-fallback.js +6 -0
- package/dist/components/questions/question-bank/editor-adapters/writing-adapters.d.ts +5 -0
- package/dist/components/questions/question-bank/editor-adapters/writing-adapters.js +114 -0
- package/dist/components/questions/question-bank/index.d.ts +4 -0
- package/dist/components/questions/question-bank/index.js +3 -0
- package/dist/components/questions/question-bank/types.d.ts +59 -0
- package/dist/components/questions/question-bank/types.js +1 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +37 -1
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +351 -1
- package/dist/components/questions/types/answer-the-question/register.js +67 -1
- package/dist/components/questions/types/answer-the-question/transform.js +36 -1
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +141 -1
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +118 -1
- package/dist/components/questions/types/arrange-letters-into-words/index.js +2 -1
- package/dist/components/questions/types/arrange-letters-into-words/register.js +4 -1
- package/dist/components/questions/types/arrange-letters-into-words/transform.js +24 -1
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +74 -1
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +169 -1
- package/dist/components/questions/types/choose-correct-adjective/index.js +2 -1
- package/dist/components/questions/types/choose-correct-adjective/register.js +55 -1
- package/dist/components/questions/types/choose-correct-adjective/transform.js +16 -1
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +149 -1
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +568 -1
- package/dist/components/questions/types/choose-the-correct-answer/index.js +3 -1
- package/dist/components/questions/types/choose-the-correct-answer/normalize.d.ts +9 -0
- package/dist/components/questions/types/choose-the-correct-answer/normalize.js +88 -1
- package/dist/components/questions/types/choose-the-correct-answer/register.js +99 -1
- package/dist/components/questions/types/choose-the-correct-answer/transform.js +70 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +22 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +163 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/index.js +3 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +94 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/register.js +36 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +55 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +27 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +231 -1
- package/dist/components/questions/types/cross-out-word-group/index.js +5 -1
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +110 -1
- package/dist/components/questions/types/cross-out-word-group/register.js +36 -1
- package/dist/components/questions/types/cross-out-word-group/transform.js +67 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +275 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +464 -1
- package/dist/components/questions/types/fill-in-blank/index.js +2 -1
- package/dist/components/questions/types/fill-in-blank/register.js +148 -1
- package/dist/components/questions/types/fill-in-blank/transform.js +88 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +117 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +671 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/index.js +2 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/register.js +37 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +15 -1
- package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +262 -1
- package/dist/components/questions/types/gn-speaking-interview/register.js +121 -1
- package/dist/components/questions/types/gn-speaking-interview/transform.js +48 -1
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +16 -1
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +27 -1
- package/dist/components/questions/types/image-object-matching/index.js +2 -1
- package/dist/components/questions/types/image-object-matching/register.js +3 -1
- package/dist/components/questions/types/image-object-matching/transform.js +2 -1
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +102 -1
- package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +344 -1
- package/dist/components/questions/types/label-the-picture/index.js +2 -1
- package/dist/components/questions/types/label-the-picture/register.js +54 -1
- package/dist/components/questions/types/label-the-picture/transform.js +28 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +78 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +232 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +2 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +82 -1
- package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +27 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +70 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +386 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +2 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +100 -1
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +33 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +106 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +457 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +2 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +110 -1
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +35 -1
- package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +8 -1
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +22 -1
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +291 -1
- package/dist/components/questions/types/listen-and-speak-answer/index.js +2 -1
- package/dist/components/questions/types/listen-and-speak-answer/register.js +84 -1
- package/dist/components/questions/types/listen-and-speak-answer/transform.js +27 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +32 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +295 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/register.js +79 -1
- package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +27 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +34 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +285 -1
- package/dist/components/questions/types/listen-and-speak-image-group/register.js +81 -1
- package/dist/components/questions/types/listen-and-speak-image-group/transform.js +26 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +38 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +385 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +87 -1
- package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +32 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +26 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +247 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +59 -1
- package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +18 -1
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +26 -1
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +256 -1
- package/dist/components/questions/types/listen-and-speak-question-list/register.js +81 -1
- package/dist/components/questions/types/listen-and-speak-question-list/transform.js +26 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +26 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +230 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +56 -1
- package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +16 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +72 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +230 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +66 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +42 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +96 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +238 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +2 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +76 -1
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +19 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +134 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +617 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/index.js +2 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/register.js +82 -1
- package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +29 -1
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +38 -1
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +359 -1
- package/dist/components/questions/types/match-by-writing-answer/index.js +3 -1
- package/dist/components/questions/types/match-by-writing-answer/register.js +58 -1
- package/dist/components/questions/types/match-by-writing-answer/transform.js +61 -1
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +16 -1
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +23 -1
- package/dist/components/questions/types/read-and-color-objects/index.js +2 -1
- package/dist/components/questions/types/read-and-color-objects/register.js +3 -1
- package/dist/components/questions/types/read-and-color-objects/transform.js +36 -1
- package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +125 -1
- package/dist/components/questions/types/read-displayed-content/register.js +64 -1
- package/dist/components/questions/types/read-displayed-content/transform.js +12 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +65 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +511 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/index.js +2 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/register.js +58 -1
- package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +28 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +373 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +41 -1
- package/dist/components/questions/types/speaking-conversation/register.js +110 -1
- package/dist/components/questions/types/speaking-conversation/transform.js +34 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +74 -1
- package/dist/components/questions/types/speaking-cue-card/register.js +52 -1
- package/dist/components/questions/types/speaking-cue-card/transform.js +12 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +10 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +161 -1
- package/dist/components/questions/types/speaking-describe-image/index.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/register.js +69 -1
- package/dist/components/questions/types/speaking-describe-image/transform.js +16 -1
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +234 -1
- package/dist/components/questions/types/spontaneous-qa/register.js +93 -1
- package/dist/components/questions/types/spontaneous-qa/transform.js +34 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +9 -0
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +179 -0
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.d.ts +5 -0
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +43 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +14 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +69 -0
- package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/index.js +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/register.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/register.js +139 -0
- package/dist/components/questions/types/spontaneous-qa-group/transform.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +52 -0
- package/dist/components/questions/types/true-false/TrueFalseClient.js +75 -1
- package/dist/components/questions/types/true-false/TrueFalseCreator.js +244 -1
- package/dist/components/questions/types/true-false/index.js +2 -1
- package/dist/components/questions/types/true-false/register.js +77 -1
- package/dist/components/questions/types/true-false/transform.js +32 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +25 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupCreator.js +129 -1
- package/dist/components/questions/types/true-false-group/index.js +5 -1
- package/dist/components/questions/types/true-false-group/map-true-false-group-data.js +69 -1
- package/dist/components/questions/types/true-false-group/register.js +36 -1
- package/dist/components/questions/types/true-false-group/transform.js +43 -1
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +233 -1
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +631 -1
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +28 -1
- package/dist/components/questions/types/word-fill-paragraph/index.js +2 -1
- package/dist/components/questions/types/word-fill-paragraph/register.js +136 -1
- package/dist/components/questions/types/word-fill-paragraph/transform.js +56 -1
- package/dist/components/questions/types/word-fill-structured-form/WfsfWordBankEditor.js +86 -1
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +246 -1
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.d.ts +2 -0
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +484 -1
- package/dist/components/questions/types/word-fill-structured-form/index.js +3 -1
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +113 -1
- package/dist/components/questions/types/word-fill-structured-form/register.js +36 -1
- package/dist/components/questions/types/word-fill-structured-form/transform.js +77 -1
- package/dist/components/questions/types/word-fill-structured-form/wfsf-word-bank.utils.js +110 -1
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.d.ts +3 -0
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +190 -0
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.d.ts +3 -0
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupCreator.js +182 -0
- package/dist/components/questions/types/word-order-and-match-group/index.d.ts +4 -0
- package/dist/components/questions/types/word-order-and-match-group/index.js +4 -0
- package/dist/components/questions/types/word-order-and-match-group/register.d.ts +2 -0
- package/dist/components/questions/types/word-order-and-match-group/register.js +121 -0
- package/dist/components/questions/types/word-order-and-match-group/transform.d.ts +2 -0
- package/dist/components/questions/types/word-order-and-match-group/transform.js +70 -0
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +84 -1
- package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +192 -1
- package/dist/components/questions/types/word-ordering/index.js +2 -1
- package/dist/components/questions/types/word-ordering/register.js +46 -1
- package/dist/components/questions/types/word-ordering/transform.js +24 -1
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +99 -1
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +320 -1
- package/dist/components/questions/types/write-a-short-letter/index.js +2 -1
- package/dist/components/questions/types/write-a-short-letter/register.js +68 -1
- package/dist/components/questions/types/write-a-short-letter/transform.js +25 -1
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +61 -1
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +196 -1
- package/dist/components/questions/types/write-correct-verb-form/index.js +2 -1
- package/dist/components/questions/types/write-correct-verb-form/register.js +62 -1
- package/dist/components/questions/types/write-correct-verb-form/transform.js +17 -1
- package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +22 -1
- package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +174 -1
- package/dist/components/questions/types/write-sentences/index.js +2 -1
- package/dist/components/questions/types/write-sentences/register.js +58 -1
- package/dist/components/questions/types/write-sentences/transform.js +28 -1
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +96 -1
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +161 -1
- package/dist/components/questions/types/write-short-paragraph/index.js +2 -1
- package/dist/components/questions/types/write-short-paragraph/register.js +4 -1
- package/dist/components/questions/types/write-short-paragraph/transform.js +21 -1
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +11 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +8 -1
- package/dist/components/questions/viewer/QuestionGroupViewer.js +36 -1
- package/dist/components/questions/viewer/QuestionViewer.js +692 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +2 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +82 -1
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +11 -1
- package/dist/components/questions/viewer/WordFillStructuredFormViewer.js +14 -1
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +95 -1
- package/dist/components/questions/viewer/index.js +8 -1
- package/dist/components/results/ResultReviewPageContainer.js +220 -1
- package/dist/components/results/ReviewQuestionRenderer.js +55 -1
- package/dist/components/results/hooks/useResultReviewPolling.js +76 -1
- package/dist/components/results/index.js +22 -1
- package/dist/components/results/renderers/QuestionExplanations.js +19 -1
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +94 -1
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +75 -1
- package/dist/components/results/renderers/ReviewColoringRenderer.js +110 -1
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +48 -1
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +87 -1
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +104 -1
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +123 -1
- package/dist/components/results/renderers/ReviewListeningHeader.js +104 -1
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +116 -1
- package/dist/components/results/renderers/ReviewSpeakingMedia.js +48 -1
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +42 -1
- package/dist/components/results/renderers/ReviewSpeakingRenderer.js +12 -1
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +85 -1
- package/dist/components/results/renderers/review-question-body-dedicated.js +104 -1
- package/dist/components/results/renderers/review-question-body-movers.js +148 -1
- package/dist/components/results/renderers/review-speaking-map.js +226 -1
- package/dist/components/results/renderers/review-spontaneous-qa-group.d.ts +4 -0
- package/dist/components/results/renderers/review-spontaneous-qa-group.js +74 -0
- package/dist/components/results/report/BadgeTitle.js +5 -1
- package/dist/components/results/report/ExamSkillRow.js +20 -1
- package/dist/components/results/report/OceanEduExamReport.js +58 -1
- package/dist/components/results/report/OceanEduSpeakingReport.js +62 -1
- package/dist/components/results/report/OverallAssessmentSection.js +17 -1
- package/dist/components/results/report/ResultErrorState.js +18 -1
- package/dist/components/results/report/ResultLoadingState.js +10 -1
- package/dist/components/results/report/ResultNotPublicState.js +27 -1
- package/dist/components/results/report/SpeakingReportParts.js +14 -1
- package/dist/components/results/report/SpeakingReportShared.js +86 -1
- package/dist/components/results/report/SpeakingRubricSection.js +171 -1
- package/dist/components/results/report/speaking-report.utils.js +198 -1
- package/dist/components/results/result-review.utils.js +6 -1
- package/dist/components/results/review-data.js +222 -1
- package/dist/components/results/review-renderer.utils.js +97 -1
- package/dist/components/results/review-status.utils.js +36 -1
- package/dist/components/results/review-types.js +1 -1
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +77 -1
- package/dist/components/results/speaking/SpeakingReviewResult.js +48 -1
- package/dist/components/results/utils/review-score.utils.js +72 -1
- package/dist/components/results/utils/speaking-summary.js +224 -1
- package/dist/components/results/utils/transform-speaking-review.js +183 -1
- package/dist/components/shared/RichTextEditor.d.ts +9 -0
- package/dist/components/shared/RichTextEditor.js +47 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.js +100 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +140 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +10 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.d.ts +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +18 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +11 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +11 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.d.ts +2 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +19 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +13 -1
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.d.ts +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +3 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +6 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +5 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +9 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.d.ts +2 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +61 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.d.ts +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +25 -1
- package/dist/components/themes/cambridge-yle/index.js +13 -1
- package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +25 -1
- package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +89 -1
- package/dist/components/themes/english-certification/EcWordFillParagraph.js +56 -1
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +192 -1
- package/dist/components/themes/english-certification/EcWritingTask.js +60 -1
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +108 -1
- package/dist/components/themes/english-certification/EnglishCertificationBrand.js +11 -1
- package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +34 -1
- package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +15 -1
- package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +19 -1
- package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +166 -1
- package/dist/components/themes/english-certification/EnglishCertificationHeader.js +31 -1
- package/dist/components/themes/english-certification/EnglishCertificationInstructionBanner.js +79 -1
- package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +71 -1
- package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +79 -1
- package/dist/components/themes/english-certification/EnglishCertificationPartHeader.js +47 -1
- package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +77 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +25 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +36 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +81 -1
- package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +95 -1
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +18 -1
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +32 -1
- package/dist/components/themes/english-certification/english-cert-scroll.js +163 -1
- package/dist/components/themes/english-certification/english-certification-theme.js +58 -1
- package/dist/components/themes/english-certification/grouped-question-utils.js +243 -1
- package/dist/components/themes/english-certification/index.js +24 -1
- package/dist/components/themes/index.js +25 -1
- package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +170 -1
- package/dist/components/themes/summer-sky/SummerSkyExitModal.js +21 -1
- package/dist/components/themes/summer-sky/SummerSkyLayout.js +221 -1
- package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +18 -1
- package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +193 -1
- package/dist/components/themes/types.js +1 -1
- package/dist/components/ui/alert-dialog.js +26 -1
- package/dist/components/ui/alert.js +22 -1
- package/dist/components/ui/avatar.js +12 -1
- package/dist/components/ui/badge.js +38 -1
- package/dist/components/ui/button.js +35 -1
- package/dist/components/ui/calendar.js +76 -1
- package/dist/components/ui/card.js +16 -1
- package/dist/components/ui/carousel.js +91 -1
- package/dist/components/ui/checkbox.js +9 -1
- package/dist/components/ui/collapsible.js +13 -1
- package/dist/components/ui/command.js +34 -1
- package/dist/components/ui/confirm-dialog.js +49 -1
- package/dist/components/ui/dialog.js +23 -1
- package/dist/components/ui/drawer.js +35 -1
- package/dist/components/ui/dropdown-menu.js +33 -1
- package/dist/components/ui/file-upload.js +155 -1
- package/dist/components/ui/form.js +62 -1
- package/dist/components/ui/hover-card.js +10 -1
- package/dist/components/ui/image-preview.js +29 -1
- package/dist/components/ui/input.js +8 -1
- package/dist/components/ui/label.js +9 -1
- package/dist/components/ui/language-switcher.js +15 -1
- package/dist/components/ui/multi-select.js +90 -1
- package/dist/components/ui/points-input.js +33 -1
- package/dist/components/ui/popover.js +26 -1
- package/dist/components/ui/progress.js +16 -1
- package/dist/components/ui/radio-group.js +12 -1
- package/dist/components/ui/scroll-area.js +11 -1
- package/dist/components/ui/select.js +38 -1
- package/dist/components/ui/separator.js +8 -1
- package/dist/components/ui/sheet.js +40 -1
- package/dist/components/ui/single-select.js +76 -1
- package/dist/components/ui/skeleton.js +6 -1
- package/dist/components/ui/switch.js +8 -1
- package/dist/components/ui/table.js +20 -1
- package/dist/components/ui/tabs.js +17 -1
- package/dist/components/ui/textarea.js +8 -1
- package/dist/components/ui/tooltip.js +11 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +24 -1
- package/dist/shared/constants/ApiConstant.d.ts +1 -0
- package/dist/shared/constants/ApiConstant.js +278 -1
- package/dist/shared/constants/QueryKeyConstant.js +112 -1
- package/dist/shared/constants/exam-level.enum.js +41 -1
- package/dist/shared/constants/landings/placement-test.constants.js +431 -1
- package/dist/shared/constants/question-skills.js +277 -1
- package/dist/shared/constants/routes.js +81 -1
- package/dist/shared/index.js +10 -1
- package/dist/shared/lib/hooks/index.js +10 -1
- package/dist/shared/lib/hooks/useAlertDialog.js +57 -1
- package/dist/shared/lib/hooks/useAudioPlayCounter.js +30 -1
- package/dist/shared/lib/hooks/useAudioPreview.js +81 -1
- package/dist/shared/lib/hooks/useAudioUpload.js +24 -1
- package/dist/shared/lib/hooks/useConfirmDialog.js +89 -1
- package/dist/shared/lib/hooks/useDebouncedCallback.js +32 -1
- package/dist/shared/lib/hooks/useDelayedAutoPlay.js +12 -1
- package/dist/shared/lib/hooks/useFileUpload.js +24 -1
- package/dist/shared/lib/hooks/useImageUrl.js +5 -1
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +108 -1
- package/dist/shared/lib/hooks/useReactHookForm.js +31 -1
- package/dist/shared/lib/hooks/useToast.js +62 -1
- package/dist/shared/lib/html-content.js +22 -1
- package/dist/shared/lib/i18n/index.js +54 -1
- package/dist/shared/lib/i18n/messages/en/admin.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/en/admin.js +2111 -1
- package/dist/shared/lib/i18n/messages/en/auth.js +86 -1
- package/dist/shared/lib/i18n/messages/en/common.js +140 -1
- package/dist/shared/lib/i18n/messages/en/file-upload.js +18 -1
- package/dist/shared/lib/i18n/messages/en/student.js +133 -1
- package/dist/shared/lib/i18n/messages/en/terms.js +37 -1
- package/dist/shared/lib/i18n/messages/en.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/en.js +15 -1
- package/dist/shared/lib/i18n/messages/vi/admin.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/vi/admin.js +2111 -1
- package/dist/shared/lib/i18n/messages/vi/auth.js +86 -1
- package/dist/shared/lib/i18n/messages/vi/common.js +140 -1
- package/dist/shared/lib/i18n/messages/vi/file-upload.js +18 -1
- package/dist/shared/lib/i18n/messages/vi/student.js +133 -1
- package/dist/shared/lib/i18n/messages/vi/terms.js +37 -1
- package/dist/shared/lib/i18n/messages/vi.d.ts +11 -4
- package/dist/shared/lib/i18n/messages/vi.js +15 -1
- package/dist/shared/lib/i18n/types.js +1 -1
- package/dist/shared/lib/stores/examQuestionStore.js +212 -1
- package/dist/shared/lib/stores/examTakingStore.js +240 -1
- package/dist/shared/lib/stores/gradingTransientStore.js +39 -1
- package/dist/shared/lib/stores/localeStore.js +10 -1
- package/dist/shared/lib/themes/config/starters.config.js +71 -1
- package/dist/shared/lib/themes/index.js +49 -1
- package/dist/shared/lib/themes/types.js +2 -1
- package/dist/shared/lib/toast.js +43 -1
- package/dist/shared/lib/utils/public-page.js +44 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +438 -1
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform-validation.js +194 -1
- package/dist/shared/lib/utils/question-transform.js +199 -1
- package/dist/shared/lib/utils.js +168 -1
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +111 -1
- package/dist/shared/lib/validation/label-the-picture.validation.js +19 -1
- package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +46 -1
- package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +21 -1
- package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +34 -1
- package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +40 -1
- package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +45 -1
- package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +19 -1
- package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +39 -1
- package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +32 -1
- package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +19 -1
- package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +28 -1
- package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +12 -1
- package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +47 -1
- package/dist/shared/lib/validation/read-and-color-objects.validation.js +29 -1
- package/dist/shared/lib/validation/read-displayed-content.validation.js +28 -1
- package/dist/shared/lib/validation/speaking-conversation.validation.js +75 -1
- package/dist/shared/lib/validation/speaking-describe-image.validation.js +31 -1
- package/dist/shared/lib/validation/spontaneous-qa.validation.js +48 -1
- package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +38 -1
- package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +39 -1
- package/dist/shared/lib/validation/word-fill-paragraph.validation.js +43 -1
- package/dist/shared/lib/validation/word-ordering.validation.js +33 -1
- package/dist/shared/types/branch.types.js +6 -1
- package/dist/shared/types/common.types.js +49 -1
- package/dist/shared/types/entities/exam-schedule.types.js +5 -1
- package/dist/shared/types/entities/exam.types.js +1 -1
- package/dist/shared/types/entities/question.types.js +4 -1
- package/dist/shared/types/entities/result.types.js +4 -1
- package/dist/shared/types/entities/user.types.js +4 -1
- package/dist/shared/types/exam-taking.types.js +2 -1
- package/dist/shared/types/history.types.js +34 -1
- package/dist/shared/types/index.js +1 -1
- package/dist/shared/types/questions/choose-correct-adjective.js +4 -1
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
- package/dist/shared/types/questions/choose-the-correct-answer-group.js +1 -1
- package/dist/shared/types/questions/choose-the-correct-answer.js +9 -1
- package/dist/shared/types/questions/cross-out-word-group.d.ts +2 -0
- package/dist/shared/types/questions/cross-out-word-group.js +1 -1
- package/dist/shared/types/questions/fill-missing-words-in-grid.js +11 -1
- package/dist/shared/types/questions/index.d.ts +1 -0
- package/dist/shared/types/questions/index.js +46 -1
- package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-info-exchange.js +3 -1
- package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +3 -1
- package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-with-story-images.js +3 -1
- package/dist/shared/types/questions/look-picture-choose-correct-answer.js +5 -1
- package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +9 -1
- package/dist/shared/types/questions/look-picture-fill-word-hint.js +7 -1
- package/dist/shared/types/questions/read-displayed-content.js +1 -1
- package/dist/shared/types/questions/speaking-conversation.js +7 -1
- package/dist/shared/types/questions/speaking-describe-image.js +1 -1
- package/dist/shared/types/questions/spontaneous-qa.js +1 -1
- package/dist/shared/types/questions/word-order-and-match-group.d.ts +33 -0
- package/dist/shared/types/questions/word-order-and-match-group.js +1 -0
- package/dist/shared/types/questions/write-correct-verb-form.js +12 -1
- package/dist/shared/types/role.types.js +1 -1
- package/dist/shared/types/structure-management.js +28 -1
- package/dist/shared/types/student.types.js +57 -1
- package/dist/shared/types/teacher-dashboard-rooms.js +2 -1
- package/package.json +184 -166
- package/dist/mcp/config.d.ts +0 -12
- package/dist/mcp/config.js +0 -1
- package/dist/mcp/http-client.d.ts +0 -6
- package/dist/mcp/http-client.js +0 -1
- package/dist/mcp/index.d.ts +0 -6
- package/dist/mcp/index.js +0 -2
- package/dist/mcp/tools/_helpers.d.ts +0 -4
- package/dist/mcp/tools/_helpers.js +0 -1
- package/dist/mcp/tools/exams.d.ts +0 -2
- package/dist/mcp/tools/exams.js +0 -1
- package/dist/mcp/tools/index.d.ts +0 -2
- package/dist/mcp/tools/index.js +0 -1
- package/dist/mcp/tools/results.d.ts +0 -2
- package/dist/mcp/tools/results.js +0 -1
|
@@ -1 +1,1137 @@
|
|
|
1
|
-
export function groupQuestionsByPart(t,e){const n=new Map,o=new Map;for(const e of t){const t=o.get(e.part_id)||[];t.push(e),o.set(e.part_id,t)}for(const[t,r]of o.entries()){const o=[...r].sort((t,e)=>t.question_number-e.question_number),i=o[0];if(!i)continue;const s=e?.find(e=>String(e.id)===String(t)),a=void 0!==s?.id?Number(s.id):void 0,u=s?.name?.match(/\d+/),c=u?parseInt(u[0],10):i.part_no,l=s?.group,m=Boolean(s?.isGroup)&&"DOCUMENT"===l?.type,p=s?.sections??[],d=new Set(p.map(t=>t.questionType).filter(Boolean)),g=p.length>1&&d.size>1;if(m||g){const e=`${t}_part`;n.set(e,{partId:t,partNo:c,questionType:i.question_type,questions:o,name:s?.name,instructions:s?.instructions,audioUrl:s?.audioUrl,templateOrder:a,category:s?.category,points:s?.points,isGroup:!0,group:l,sections:s?.sections,startIndex:s?.startIndex,endIndex:s?.endIndex});continue}if(!s?.sections?.length){const e=`${t}_part`;n.set(e,{partId:t,partNo:c,questionType:i.question_type,questions:o,name:s?.name,instructions:s?.instructions,audioUrl:s?.audioUrl,templateOrder:a,category:s?.category,points:s?.points,isGroup:s?.isGroup,optionMode:s?.optionMode,optionCount:s?.optionCount,viewMode:s?.viewMode,defaultValue:s?.defaultValue,multipleAnswers:s?.multipleAnswers,questionConfig:s?.questionConfig,wordBankConfig:s?.wordBankConfig,group:s?.group,startIndex:s?.startIndex,endIndex:s?.endIndex});continue}for(const[e,r]of s.sections.entries()){const i=o.filter(t=>{const e=t.question_number-1;return e>=r.startIndex&&e<=r.endIndex});if(!i.length)continue;const u=`${t}_section_${e}`,l=Boolean(r.isGroup||r.group?.type);n.set(u,{partId:t,partNo:c,questionType:r.questionType||i[0].question_type,questions:i,name:s.name,instructions:r.instructions||r.name||s.instructions,audioUrl:s.audioUrl,templateOrder:a,category:s.category,points:s.points,sectionId:r.sectionId,isGroup:l,optionMode:r.optionMode,optionCount:r.optionCount,viewMode:r.viewMode,defaultValue:r.defaultValue,multipleAnswers:r.multipleAnswers,questionConfig:r.questionConfig,wordBankConfig:r.wordBankConfig,group:r.group,startIndex:r.startIndex,endIndex:r.endIndex})}}return Array.from(n.values()).sort((t,e)=>{const n=void 0===t.templateOrder||isNaN(t.templateOrder)?1/0:t.templateOrder,o=void 0===e.templateOrder||isNaN(e.templateOrder)?1/0:e.templateOrder;return n!==o?n-o:(t.questions[0]?.question_number||0)-(e.questions[0]?.question_number||0)})}export function transformFillInBlank(t){const e=t[0],n=e?.content,o=e?.questionGroup?.payload,r=[];for(const e of t){const t=e.content,n=e.is_example||!1,o=t.question||t.blanks?.[0]?.text||"";if([...o.matchAll(/\{(\d+)\}/g)].length>1){const i=o.split("\n").filter(t=>t.trim());for(const o of i){const i=o.match(/\{(\d+)\}/);if(!i)continue;const s=parseInt(i[1],10),a=o.split(/\{\d+\}/),u=a[0]?.trim()||"",c=a[1]?.trim()||"";r.push({id:`${e.id}-blank-${s}`,questionNumber:e.question_number,textBefore:u,textAfter:c,imageUrl:t.imageUrl,hints:t.hints,isExample:n,correctAnswer:n?e.correct_answer?.answers?.[s]:void 0})}}else{const i=o.match(/^([\s\S]*?)\{0\}([\s\S]*)$/),s=i?i[1].trim():o,a=i?i[2].trim():"";r.push({id:e.id,questionNumber:e.question_number,textBefore:s,textAfter:a,imageUrl:t.imageUrl,hints:t.hints,isExample:n,correctAnswer:n?e.correct_answer?.answers?.[0]:void 0})}}return{questions:r,title:n?.title||o?.title||"",imageUrl:o?n?.imageUrl:void 0,audioUrl:n?.audioUrl,exampleText:n?.exampleText,exampleHighlight:n?.exampleHighlight,instruction:n?.instruction||"LISTEN AND WRITE",showHints:n?.showHints,groupContent:o?.content||o?.groupContent,groupTitle:o?.title,groupImageUrl:o?.mainImageUrl||o?.imageUrl}}export function transformWriteCorrectVerbForm(t){const e=t[0],n=e?.content;return{questions:t.map(t=>{const e=t.content,n=e.sentence||"",o=n.match(/^([\s\S]*?)\{0\}([\s\S]*)$/),r=o?o[1].trim():n,i=o?o[2].trim():"";return{id:t.id,questionNumber:t.question_number,textBefore:r,textAfter:i,verbHint:e.verbHint}}),title:"WRITE THE CORRECT VERB FORM",instruction:n?.instruction||"Write the correct form of the verb in parentheses."}}const t=["A","B","C","D","E","F"];export function transformChooseCorrectAnswer(e){const n=e[0]?.content,o=e[0]?.questionGroup?.payload,r=e.find(t=>t.is_example),i=e.filter(t=>!t.is_example);let s;if(r){const e=r.content,n=(e.options||[]).map((e,n)=>{const o="object"==typeof e&&null!==e,i=o&&e.id?String(e.id):`${r.id}-opt-${n}`,s=o?e.text??e.content??"":String(e),a=o?e.imageUrl:void 0;return{id:i,label:t[n]||String(n+1),text:s,...a?{imageUrl:a}:{}}}),o=r.correct_answer?.answer??r.correct_answer?.optionId;let i="";if("string"==typeof o&&o){const t=n.find(t=>t.id===o);i=t?t.id:o}else"number"==typeof o&&o>=0&&o<n.length&&(i=n[o]?.id||"");s={questionNumber:r.question_number,questionText:e.question||"",options:n,correctOptionId:i||n[0]?.id||"",optionType:e.optionType||"text",imageUrl:e.imageUrl,audioUrl:e.audioUrl}}const a=i.map(e=>{const n=e.content,o=n.optionType||"text",r=(n.options||[]).map((n,o)=>{const r="object"==typeof n&&null!==n,i=r&&n.id?String(n.id):`${e.id}-opt-${o}`,s=r?n.text??n.content??"":String(n),a=r?n.imageUrl:void 0;return{id:i,label:t[o]||String(o+1),text:s,...a?{imageUrl:a}:{}}}),i=e.correct_answer?.answer??e.correct_answer?.optionId??e.correct_answer?.optionIds,s=t=>{if(null==t)return;const e=String(t),n=r.find(t=>t.id===e);if(n)return n.id;const o=e.match(/-opt-(\d+)$/);if(o){const t=parseInt(o[1],10);if(t>=0&&t<r.length)return r[t]?.id}if("number"==typeof t&&t>=0&&t<r.length)return r[t]?.id;const i=parseInt(e,10);return!isNaN(i)&&i>=0&&i<r.length&&!e.startsWith("opt-")?r[i]?.id:e};let a=[];if(Array.isArray(i))a=i.map(s).filter(t=>Boolean(t));else{const t=s(i);t&&(a=[t])}return{id:e.id,questionNumber:e.question_number,questionText:n.question||"",options:r,correctOptionId:a[0],correctOptionIds:a,imageUrl:n.imageUrl,audioUrl:n.audioUrl,optionType:o,multipleAnswers:Boolean(n.multipleAnswers||Array.isArray(i)),explanation:e.explanation}}),u=a.some(t=>!!t.audioUrl?.trim())||!!s?.audioUrl?.trim();return{questions:a,example:s,imageUrl:n?.imageUrl,groupImageUrl:o?.mainImageUrl||o?.imageUrl,groupTitle:o?.title,groupContent:o?.content,articles:o?.articles,audioUrl:u?void 0:n?.audioUrl,instruction:n?.instruction||"CHOOSE THE CORRECT ANSWER"}}export function transformChooseTheCorrectAnswerGroup(e){const n=e[0],o=n?.content,r=Array.isArray(o?.items)?o.items:[],i=Array.isArray(n?.correct_answer?.answer)?n.correct_answer.answer:[],s=r.find(t=>t.isExample),a=r.filter(t=>!t.isExample),u=(e,n)=>(e||[]).map((e,o)=>{const r="object"==typeof e&&null!==e;return{id:r&&e.id?String(e.id):`${n}-opt-${o+1}`,label:t[o]||String(o+1),text:String(r?e.text??"":e),...r&&e.imageUrl?{imageUrl:e.imageUrl}:{}}});let c;if(s){const t=r.indexOf(s),e=u(s.content?.options,`${n.id}-ex`),o=s.correctAnswer?.answer||i[t]||e[0]?.id||"";c={questionNumber:s.questionNumber||t+1,questionText:s.content?.question||"",options:e,correctOptionId:o,optionType:s.content?.optionType||"text"}}return{questions:a.map(t=>{const e=r.indexOf(t),o=u(t.content?.options,`${n.id}-item-${e}`),s=t.correctAnswer?.answer||i[e]||"";return{id:`${n.id}-item-${e}`,questionNumber:t.questionNumber||e+1,questionText:t.content?.question||"",options:o,correctOptionId:s,correctOptionIds:s?[s]:[],optionType:t.content?.optionType||"text"}}),example:c,instruction:o?.meta?.instruction||"Choose the correct answer."}}export function transformChooseCorrectAdjective(e){return{questions:e.map(e=>{const n=e.content,o=n.sentence||"",r=o.match(/^([\s\S]*?)\{0\}([\s\S]*)$/),i=r?r[1]:o,s=r?r[2]:"";return{id:e.id,questionNumber:e.question_number,sentenceBefore:i,sentenceAfter:s,options:(n.options||[]).map((n,o)=>({id:`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:n}))}}),instruction:"CHOOSE THE CORRECT ADJECTIVE"}}export function transformWriteShortLetter(t){const e=t.content;if(e.instructions?.mainInstruction||e.informations?.length)return{prompt:e.instructions?.mainInstruction||"",requirements:e.informations,minWords:e.minWords,maxWords:e.maxWords,guideImages:e.guideImages};return{prompt:e.contexts?.filter(t=>"text"===t.type).map(t=>t.text).join("\n")||"",minWords:e.minWords,maxWords:e.maxWords,guideImages:e.guideImages}}export function transformColorObjects(t){const e=t[0],n=e?.content,o=n?.colorNodes?.map(t=>({id:t.id,color:t.color,label:t.label}))||n?.hintItems||[],r=n?.regionNodes?.map(t=>({id:t.id,x:t.x,y:t.y,width:t.width,height:t.height,correctHintId:t.linkedColorNodeId}))||n?.regions||[],i=n?.instructions;return{instruction:(i&&i.length>0?i.join(" "):n?.instruction)||"READ AND COLOR THE OBJECTS",instructions:i,imageUrl:n?.backgroundImageUrl||n?.imageUrl||"",audioUrl:n?.audioUrl,hintItems:o,regions:r}}export function transformPictureChoose(e){const n=e[0]?.content;return{subQuestions:e.map(e=>{const n=e.content;return{id:e.id,questionNumber:e.question_number,imageUrl:n.imageUrl||"",questionText:n.questionText,options:(n.options||[]).map((n,o)=>({id:n.id||`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:"string"==typeof n?n:n.text}))}}),instruction:n?.instruction||"LOOK AT THE PICTURE AND CHOOSE THE CORRECT ANSWER"}}export function transformPictureFillBlankChoose(e){const n=e[0]?.content;return{subQuestions:e.map(e=>{const n=e.content;return{id:e.id,questionNumber:e.question_number,imageUrl:n.imageUrl||"",question:n.question||"",options:(n.options||[]).map((n,o)=>({id:n.id||`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:"string"==typeof n?n:n.text}))}}),instruction:n?.instruction||"LOOK AT THE PICTURE, FILL IN THE BLANK AND CHOOSE THE ANSWER"}}export function transformReadingPassage(e){if(function(t){const e=t[0];if(!e)return!1;const n=e.content;return void 0!==n?.question&&!n?.questions&&void 0!==e.questionGroup?.payload?.passage}(e)){const n=e[0],o=n.questionGroup?.payload||{},r=e.map(e=>{const n=e.content;return{id:e.id,questionNumber:e.question_number,questionText:n?.question||"",options:(n?.options||[]).map((n,o)=>({id:`${e.id}-opt-${o}`,label:t[o]||String(o+1),text:"string"==typeof n?n:String(n)}))}});return{passage:o.passage||"",passageTitle:o.passageTitle,passageImageUrl:o.passageImageUrl||o.mainImageUrl,instruction:o.instructions||"READ THE PASSAGE AND ANSWER QUESTIONS",questions:r}}const n=e[0],o=n?.content,r=o?.questions?.map((e,o)=>({id:e.id||`${n.id}-sq-${o}`,questionNumber:o+1,questionText:e.question,options:(e.options||[]).map((o,r)=>({id:`${e.id||n.id}-opt-${r}`,label:t[r]||String(r+1),text:o}))}))||[];return{passage:o?.passage||"",passageTitle:o?.passageTitle,passageImageUrl:o?.passageImageUrl||o?.mainImageUrl,instruction:o?.instructions?.mainInstruction||"READ THE PASSAGE AND ANSWER QUESTIONS",questions:r}}export function transformLabelPicture(t){const e=t[0],n=e?.content;return{imageUrl:n?.imageUrl||"",wordBank:n?.wordBank||[],labels:n?.labels?.map(t=>({id:t.id,number:t.number,isPreFilled:t.isPreFilled||!1,correctAnswer:t.correctAnswer}))||[],instruction:n?.instruction||"LABEL THE PICTURE"}}function e(t){return t&&"string"==typeof t.word&&!t.subQuestions}export function transformFillWordHint(t){const n=t[0],o=n?.content;if(t.length>1&&t.every(t=>e(t.content))){const e=n.questionGroup?.payload?.hintLetters||[];return{subQuestions:t.map(t=>{const e=t.content,n=e.word.replace(/\{\d+\}/,"_");return{id:t.id,questionNumber:t.question_number,imageUrl:e.imageUrl||"",word:n}}),hintLetters:e,instruction:o?.instruction||"LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER"}}if(e(o)){const t=o,e=n.questionGroup?.payload?.hintLetters||[];return{subQuestions:function(t){const e=[],n=t.split(",").map(t=>t.trim());for(const t of n){let n;const o=/\{(\d+)\}/g;for(;null!==(n=o.exec(t));){const o=parseInt(n[1],10),r=t.replace(n[0],"_");e.push({index:o,displayWord:r})}}return e.sort((t,e)=>t.index-e.index),e}(t.word).map(e=>({id:`${n.id}-blank-${e.index}`,questionNumber:n.question_number,imageUrl:t.imageUrl||"",word:e.displayWord,blankIndex:e.index})),hintLetters:e,instruction:t.instruction||"LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER"}}const r=o;return{subQuestions:r?.subQuestions?.map((t,e)=>({id:t.id,questionNumber:n.question_number+e,imageUrl:t.imageUrl||"",word:t.word||""}))||[],hintLetters:r?.hintLetters||n.questionGroup?.payload?.hintLetters||[],instruction:r?.instruction||"LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER"}}export function transformGridFill(t){const e=t[0],n=e?.content;return{grid:n?.grid||[],rows:n?.rows||0,columns:n?.columns||0,instruction:n?.instruction||"FILL IN THE MISSING WORDS",disabledCells:n?.disabledCells||[]}}export function transformAnswerTheQuestion(t){const e=t[0],n=e?.questionGroup?.payload,o=e?.content;return{questions:t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1;return{id:t.id,questionNumber:t.question_number,question:(e.question||"").replace(/\{\d+\}/g,"______"),imageUrl:e.imageUrl,prefixText:e.prefixText,suffixText:e.suffixText,suggestedAnswers:e.suggestedAnswers,isExample:n,exampleAnswer:n?t.correct_answer?.answer:void 0}}),instruction:"ANSWER THE QUESTION",imageUrl:n?.mainImageUrl||n?.imageUrl||o?.imageUrl,title:o?.title,groupContent:n?.groupContent||o?.groupContent}}export function transformTrueFalse(t){const e=t[0]?.questionGroup?.payload;return{questions:t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1,o=n&&"boolean"==typeof t.correct_answer?.answer?t.correct_answer.answer:void 0;return{id:t.id,questionNumber:t.question_number,question:e.question||"",imageUrl:e.imageUrl,isExample:n,exampleCorrectAnswer:o}}),instruction:"READ AND CHOOSE TRUE OR FALSE",imageUrl:e?.mainImageUrl||e?.imageUrl,groupContent:e?.content||e?.groupContent}}export function transformWordOrdering(t){return{questions:t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1,o=Array.isArray(t.correct_answer?.answer)?t.correct_answer.answer:Array.isArray(e.answer)?e.answer:void 0;return{id:t.id,questionNumber:t.question_number,words:e.words||[],imageUrl:e.imageUrl,isExample:n,exampleAnswer:n?o:void 0}}),instruction:"ARRANGE THE LETTERS TO MAKE A WORD"}}export function transformWordFillParagraph(t){const e=t[0],n=e?.questionGroup?.payload,o=e?.content,r=o?.title||n?.title||t.find(t=>t.content?.title)?.content?.title,i=o?.wordBank||[],s=i.map(t=>"string"==typeof t?{word:t}:t),a=o?.viewMode,u=void 0!==o?.wordBank?.[0]?.options;let c;c="EMAIL"===a?"EMAIL":a||!o?.emailFrom&&!o?.emailTo?"DEFAULT"===a?"DEFAULT":"WITH_OPTIONS"===a||u?"WITH_OPTIONS":a||0!==i.length?"WITH_WORD_BANK":"DEFAULT":"EMAIL";const l=o?.emailFrom||o?.metadata?.emailFrom,m=o?.emailTo||o?.metadata?.emailTo;return{questionId:e?.id||"",paragraph:o?.paragraph||"",title:r,imageUrl:o?.imageUrl,wordBank:s,viewMode:c,instruction:"READ AND FILL IN THE BLANKS",exampleAnswer:o?.exampleAnswer,emailFrom:l,emailTo:m}}export function transformMatchByWritingAnswer(t){const e=t[0]?.questionGroup?.payload,n=t.some(t=>!!t.content?.askerName),o=t.map(t=>{const e=t.content,n=t.is_example||e.isExample||!1;return{id:t.id,questionNumber:t.question_number,question:e.question||"",imageUrl:e.imageUrl,isExample:n,exampleAnswer:n?t.correct_answer?.answer:void 0,askerName:e.askerName,answererName:e.answererName}}),r=e?.options?.map(t=>({answer:t.answer,imageUrl:t.imageUrl||"",content:t.content||""}))||[],i=n?t[0]?.content:void 0;return{questions:o,options:r,instruction:"LOOK AND WRITE THE ANSWER",viewMode:n?"CONVERSATION":"DEFAULT",groupImageUrl:e?.groupImageUrl||"",askerName:i?.askerName,answererName:i?.answererName}}export function transformWriteSentences(t){const e=t[0],n=e?.content,o=e?.questionGroup?.payload;return{questions:t.map(t=>({id:t.id,questionNumber:t.question_number})),instruction:n?.instruction||"WRITE SENTENCES ABOUT THE PICTURE",imageUrl:o?.mainImageUrl||o?.imageUrl||n?.imageUrl,title:n?.title,groupContent:o?.groupContent||n?.groupContent}}export function transformWordFillStructuredForm(t){const e=t[0],n=e?.questionGroup?.payload;return{questions:t.map(t=>{const e=t.content,n=t.correct_answer||{};let o=e?.content||"";const r=e?.blocks;return!o&&r?.length&&(o=r.map(t=>(t.heading?`<h3>${t.heading}</h3>`:"")+(t.content||"")).join("")),{id:t.id,questionNumber:t.question_number,title:e?.title||"",imageUrl:e?.imageUrl,content:o,answers:n?.answers||{},caseSensitive:n?.caseSensitive??e?.caseSensitive??!1}}),instruction:n?.instructions||"COMPLETE THE FORM BELOW",groupTitle:n?.title,groupContent:n?.content,groupImageUrl:n?.imageUrl,audioUrl:e?.audio_url||n?.audioUrl}}export function transformListenChooseObjects(t){const e=t[0],n=e?.content,o=e?.questionGroup?.payload||{};return{audioUrl:n?.audioUrl||"",backgroundImageUrl:o.backgroundImageUrl||"",objectNodes:o.objectNodes||[],correctObjectId:e?.correctAnswer?.objectId||e?.correct_answer?.objectId||"",isBackup:n?.isBackup||!1,instruction:"LISTEN AND CHOOSE THE CORRECT OBJECT"}}export function transformListenSpeakAnswer(t){return{questions:t.map(t=>{const e=t.content,n=t.questionGroup?.payload||{};return{id:t.id,questionNumber:t.question_number,audioUrl:e?.audioUrl||"",arrowNode:e?.arrowNode,backgroundImageUrl:n.backgroundImageUrl||n.imageUrl||"",isBackup:e?.isBackup||!1}}),instruction:"LISTEN AND SPEAK YOUR ANSWER"}}
|
|
1
|
+
/**
|
|
2
|
+
* Question Transformers - Transform API response to renderer props
|
|
3
|
+
*
|
|
4
|
+
* Maps backend question types to frontend renderer props.
|
|
5
|
+
* Each transformer handles specific question type data structure.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Group questions by part_id AND questionGroup.id
|
|
9
|
+
* This ensures questions with different passages (different questionGroups)
|
|
10
|
+
* within the same part are properly separated.
|
|
11
|
+
*
|
|
12
|
+
* Handles template sections: parts with `sections` array are split into
|
|
13
|
+
* multiple QuestionPart entries with section-specific questionTypes.
|
|
14
|
+
* Used for Movers P8 (2 sections) and P11 (3 sections).
|
|
15
|
+
*
|
|
16
|
+
* @param questions - API questions to group
|
|
17
|
+
* @param templateParts - Optional template parts with name/instructions metadata
|
|
18
|
+
*/
|
|
19
|
+
export function groupQuestionsByPart(questions, templateParts) {
|
|
20
|
+
const partMap = new Map();
|
|
21
|
+
const questionsByPart = new Map();
|
|
22
|
+
for (const q of questions) {
|
|
23
|
+
const existing = questionsByPart.get(q.part_id) || [];
|
|
24
|
+
existing.push(q);
|
|
25
|
+
questionsByPart.set(q.part_id, existing);
|
|
26
|
+
}
|
|
27
|
+
for (const [partId, partQuestions] of questionsByPart.entries()) {
|
|
28
|
+
const sortedPartQuestions = [...partQuestions].sort((a, b) => a.question_number - b.question_number);
|
|
29
|
+
const firstQuestion = sortedPartQuestions[0];
|
|
30
|
+
if (!firstQuestion)
|
|
31
|
+
continue;
|
|
32
|
+
// Find template part to get name/instructions metadata
|
|
33
|
+
const templatePart = templateParts?.find((tp) => String(tp.id) === String(partId));
|
|
34
|
+
const templateOrder = templatePart?.id !== undefined ? Number(templatePart.id) : undefined;
|
|
35
|
+
// Extract partNo from template name (e.g. "Part 8") or fallback to question's part_no
|
|
36
|
+
const partNameMatch = templatePart?.name?.match(/\d+/);
|
|
37
|
+
const parsedPartNo = partNameMatch ? parseInt(partNameMatch[0], 10) : firstQuestion.part_no;
|
|
38
|
+
// IELTS Reading: a part carries a part-level DOCUMENT group (the shared
|
|
39
|
+
// passage) AND sections with mixed sub-types. Render it like TOEIC Part 7 —
|
|
40
|
+
// a single screen with the passage on the left and ALL questions on the
|
|
41
|
+
// right — so keep it whole and pass the sections through for the renderer.
|
|
42
|
+
const partLevelGroup = templatePart?.group;
|
|
43
|
+
const hasDocumentGroup = Boolean(templatePart?.isGroup) && partLevelGroup?.type === 'DOCUMENT';
|
|
44
|
+
// Mixed multi-section part (e.g. Listening SECTION 2: CHOOSE + MATCH +
|
|
45
|
+
// WORD_FILL): keep it whole and pass the sections through so the grouped
|
|
46
|
+
// renderer can stack each sub-section on one screen. Without this the part
|
|
47
|
+
// would be split into one screen per section and only the first sub-section
|
|
48
|
+
// would be reachable.
|
|
49
|
+
const templateSections = (templatePart?.sections ?? []);
|
|
50
|
+
const distinctSectionTypes = new Set(templateSections.map((s) => s.questionType).filter(Boolean));
|
|
51
|
+
const isMixedMultiSection = templateSections.length > 1 && distinctSectionTypes.size > 1;
|
|
52
|
+
if (hasDocumentGroup || isMixedMultiSection) {
|
|
53
|
+
const key = `${partId}_part`;
|
|
54
|
+
partMap.set(key, {
|
|
55
|
+
partId,
|
|
56
|
+
partNo: parsedPartNo,
|
|
57
|
+
questionType: firstQuestion.question_type,
|
|
58
|
+
questions: sortedPartQuestions,
|
|
59
|
+
name: templatePart?.name,
|
|
60
|
+
instructions: templatePart?.instructions,
|
|
61
|
+
audioUrl: templatePart?.audioUrl,
|
|
62
|
+
templateOrder,
|
|
63
|
+
category: templatePart?.category,
|
|
64
|
+
points: templatePart?.points,
|
|
65
|
+
isGroup: true,
|
|
66
|
+
group: partLevelGroup,
|
|
67
|
+
sections: templatePart?.sections,
|
|
68
|
+
startIndex: templatePart?.startIndex,
|
|
69
|
+
endIndex: templatePart?.endIndex,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Parts without sections should render as exactly one screen per part
|
|
74
|
+
if (!templatePart?.sections?.length) {
|
|
75
|
+
const key = `${partId}_part`;
|
|
76
|
+
partMap.set(key, {
|
|
77
|
+
partId,
|
|
78
|
+
partNo: parsedPartNo,
|
|
79
|
+
questionType: firstQuestion.question_type,
|
|
80
|
+
questions: sortedPartQuestions,
|
|
81
|
+
name: templatePart?.name,
|
|
82
|
+
instructions: templatePart?.instructions,
|
|
83
|
+
audioUrl: templatePart?.audioUrl,
|
|
84
|
+
templateOrder,
|
|
85
|
+
category: templatePart?.category,
|
|
86
|
+
points: templatePart?.points,
|
|
87
|
+
isGroup: templatePart?.isGroup,
|
|
88
|
+
optionMode: templatePart?.optionMode,
|
|
89
|
+
optionCount: templatePart?.optionCount,
|
|
90
|
+
viewMode: templatePart?.viewMode,
|
|
91
|
+
defaultValue: templatePart?.defaultValue,
|
|
92
|
+
multipleAnswers: templatePart?.multipleAnswers,
|
|
93
|
+
questionConfig: templatePart?.questionConfig,
|
|
94
|
+
wordBankConfig: templatePart?.wordBankConfig,
|
|
95
|
+
group: templatePart?.group,
|
|
96
|
+
startIndex: templatePart?.startIndex,
|
|
97
|
+
endIndex: templatePart?.endIndex,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Parts with sections should render as exactly one screen per section.
|
|
102
|
+
// Template startIndex/endIndex are stored as zero-based exam-order positions,
|
|
103
|
+
// while API question_number is one-based. Convert question_number → zero-based
|
|
104
|
+
// position before matching, otherwise mixed-section parts (e.g. IELTS Part 3)
|
|
105
|
+
// can render the wrong heading and only a tail subset of questions.
|
|
106
|
+
for (const [sectionIdx, section] of templatePart.sections.entries()) {
|
|
107
|
+
const sectionQuestions = sortedPartQuestions.filter((question) => {
|
|
108
|
+
const templatePosition = question.question_number - 1;
|
|
109
|
+
return templatePosition >= section.startIndex && templatePosition <= section.endIndex;
|
|
110
|
+
});
|
|
111
|
+
if (!sectionQuestions.length)
|
|
112
|
+
continue;
|
|
113
|
+
const key = `${partId}_section_${sectionIdx}`;
|
|
114
|
+
// A section is a grouped question when the template marks it isGroup OR it
|
|
115
|
+
// carries a nested group payload (e.g. IELTS SECTION 2 DOCUMENT/MATCH_ANSWER).
|
|
116
|
+
const sectionIsGroup = Boolean(section.isGroup || section.group?.type);
|
|
117
|
+
partMap.set(key, {
|
|
118
|
+
partId,
|
|
119
|
+
partNo: parsedPartNo,
|
|
120
|
+
questionType: section.questionType || sectionQuestions[0].question_type,
|
|
121
|
+
questions: sectionQuestions,
|
|
122
|
+
name: templatePart.name,
|
|
123
|
+
instructions: section.instructions || section.name || templatePart.instructions,
|
|
124
|
+
audioUrl: templatePart.audioUrl,
|
|
125
|
+
templateOrder,
|
|
126
|
+
category: templatePart.category,
|
|
127
|
+
points: templatePart.points,
|
|
128
|
+
sectionId: section.sectionId,
|
|
129
|
+
isGroup: sectionIsGroup,
|
|
130
|
+
optionMode: section.optionMode,
|
|
131
|
+
optionCount: section.optionCount,
|
|
132
|
+
viewMode: section.viewMode,
|
|
133
|
+
defaultValue: section.defaultValue,
|
|
134
|
+
multipleAnswers: section.multipleAnswers,
|
|
135
|
+
questionConfig: section.questionConfig,
|
|
136
|
+
wordBankConfig: section.wordBankConfig,
|
|
137
|
+
group: section.group,
|
|
138
|
+
startIndex: section.startIndex,
|
|
139
|
+
endIndex: section.endIndex,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Sort by templateOrder, then by question_number of first question in group
|
|
144
|
+
return Array.from(partMap.values()).sort((a, b) => {
|
|
145
|
+
const orderA = a.templateOrder !== undefined && !isNaN(a.templateOrder) ? a.templateOrder : Infinity;
|
|
146
|
+
const orderB = b.templateOrder !== undefined && !isNaN(b.templateOrder) ? b.templateOrder : Infinity;
|
|
147
|
+
if (orderA !== orderB)
|
|
148
|
+
return orderA - orderB;
|
|
149
|
+
return (a.questions[0]?.question_number || 0) - (b.questions[0]?.question_number || 0);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export function transformFillInBlank(questions) {
|
|
153
|
+
const firstQuestion = questions[0];
|
|
154
|
+
const content = firstQuestion?.content;
|
|
155
|
+
const groupPayload = firstQuestion?.questionGroup?.payload;
|
|
156
|
+
const transformed = [];
|
|
157
|
+
/** Take UI shows plain textBefore/textAfter — strip `**` so markers never leak. */
|
|
158
|
+
const stripStemBoldMarkers = (s) => s.replace(/\*\*/g, '');
|
|
159
|
+
for (const q of questions) {
|
|
160
|
+
const qContent = q.content;
|
|
161
|
+
const isExample = q.is_example || false;
|
|
162
|
+
// Support both: content.question (new) and content.blanks[0].text (legacy)
|
|
163
|
+
const text = qContent.question || qContent.blanks?.[0]?.text || '';
|
|
164
|
+
// Check for multiple placeholders: {0}, {1}, etc.
|
|
165
|
+
const placeholderMatches = [...text.matchAll(/\{(\d+)\}/g)];
|
|
166
|
+
if (placeholderMatches.length > 1) {
|
|
167
|
+
// Multi-placeholder: split into separate lines by newline or by placeholder
|
|
168
|
+
const lines = text.split('\n').filter((l) => l.trim());
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const lineMatch = line.match(/\{(\d+)\}/);
|
|
171
|
+
if (!lineMatch)
|
|
172
|
+
continue;
|
|
173
|
+
const blankIndex = parseInt(lineMatch[1], 10);
|
|
174
|
+
const parts = line.split(/\{\d+\}/);
|
|
175
|
+
const textBefore = stripStemBoldMarkers(parts[0]?.trim() || '');
|
|
176
|
+
const textAfter = stripStemBoldMarkers(parts[1]?.trim() || '');
|
|
177
|
+
transformed.push({
|
|
178
|
+
id: `${q.id}-blank-${blankIndex}`,
|
|
179
|
+
questionNumber: q.question_number,
|
|
180
|
+
textBefore,
|
|
181
|
+
textAfter,
|
|
182
|
+
imageUrl: qContent.imageUrl,
|
|
183
|
+
hints: qContent.hints,
|
|
184
|
+
isExample,
|
|
185
|
+
correctAnswer: isExample ? q.correct_answer?.answers?.[blankIndex] : undefined,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Single placeholder or no placeholder.
|
|
191
|
+
// Use [\s\S] so stems with newlines still match (`.` does not cross `\n`).
|
|
192
|
+
const match = text.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
|
|
193
|
+
const textBefore = stripStemBoldMarkers(match ? match[1].trim() : text);
|
|
194
|
+
const textAfter = stripStemBoldMarkers(match ? match[2].trim() : '');
|
|
195
|
+
transformed.push({
|
|
196
|
+
id: q.id,
|
|
197
|
+
questionNumber: q.question_number,
|
|
198
|
+
textBefore,
|
|
199
|
+
textAfter,
|
|
200
|
+
imageUrl: qContent.imageUrl,
|
|
201
|
+
hints: qContent.hints,
|
|
202
|
+
isExample,
|
|
203
|
+
correctAnswer: isExample ? q.correct_answer?.answers?.[0] : undefined,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
questions: transformed,
|
|
209
|
+
// No hardcoded fallback: the part instruction banner already states the task,
|
|
210
|
+
// so an extra "FILL IN THE BLANK" heading is redundant.
|
|
211
|
+
title: content?.title || groupPayload?.title || '',
|
|
212
|
+
// Shared header image: only meaningful for grouped parts (legacy data had the group
|
|
213
|
+
// payload merged into content). For non-grouped parts each question renders its own
|
|
214
|
+
// content.imageUrl below, so lifting the first one here would duplicate it.
|
|
215
|
+
imageUrl: groupPayload ? content?.imageUrl : undefined,
|
|
216
|
+
audioUrl: content?.audioUrl,
|
|
217
|
+
exampleText: content?.exampleText,
|
|
218
|
+
exampleHighlight: content?.exampleHighlight,
|
|
219
|
+
instruction: content?.instruction || 'LISTEN AND WRITE',
|
|
220
|
+
showHints: content?.showHints,
|
|
221
|
+
groupContent: groupPayload?.content || groupPayload?.groupContent,
|
|
222
|
+
groupTitle: groupPayload?.title,
|
|
223
|
+
groupImageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export function transformWriteCorrectVerbForm(questions) {
|
|
227
|
+
const firstQuestion = questions[0];
|
|
228
|
+
const content = firstQuestion?.content;
|
|
229
|
+
const transformed = questions.map((q) => {
|
|
230
|
+
const qContent = q.content;
|
|
231
|
+
const sentence = qContent.sentence || '';
|
|
232
|
+
// Parse "John {0} his homework." -> textBefore: "John", textAfter: "his homework."
|
|
233
|
+
// [\s\S] allows instruction text with newlines before the blank.
|
|
234
|
+
const match = sentence.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
|
|
235
|
+
const textBefore = match ? match[1].trim() : sentence;
|
|
236
|
+
const textAfter = match ? match[2].trim() : '';
|
|
237
|
+
return {
|
|
238
|
+
id: q.id,
|
|
239
|
+
questionNumber: q.question_number,
|
|
240
|
+
textBefore,
|
|
241
|
+
textAfter,
|
|
242
|
+
verbHint: qContent.verbHint, // Pass verbHint per question
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
questions: transformed,
|
|
247
|
+
title: 'WRITE THE CORRECT VERB FORM',
|
|
248
|
+
instruction: content?.instruction ||
|
|
249
|
+
'Write the correct form of the verb in parentheses.',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// ============================================
|
|
253
|
+
// Deterministic Shuffle Utility
|
|
254
|
+
// ============================================
|
|
255
|
+
/**
|
|
256
|
+
* Simple hash function to create a numeric seed from a string.
|
|
257
|
+
* Used for deterministic (repeatable) shuffling based on questionId.
|
|
258
|
+
*/
|
|
259
|
+
function simpleHash(str) {
|
|
260
|
+
let hash = 0;
|
|
261
|
+
for (let i = 0; i < str.length; i++) {
|
|
262
|
+
const char = str.charCodeAt(i);
|
|
263
|
+
hash = ((hash << 5) - hash) + char;
|
|
264
|
+
hash |= 0; // Convert to 32-bit integer
|
|
265
|
+
}
|
|
266
|
+
return Math.abs(hash);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Fisher-Yates shuffle with a deterministic seed.
|
|
270
|
+
* Same seed always produces the same shuffled order.
|
|
271
|
+
* Uses a Linear Congruential Generator (LCG) for pseudo-random numbers.
|
|
272
|
+
*/
|
|
273
|
+
function seededShuffle(array, seed) {
|
|
274
|
+
const shuffled = [...array];
|
|
275
|
+
let currentSeed = simpleHash(seed);
|
|
276
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
277
|
+
currentSeed = (currentSeed * 1664525 + 1013904223) & 0x7fffffff; // LCG
|
|
278
|
+
const j = currentSeed % (i + 1);
|
|
279
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
280
|
+
}
|
|
281
|
+
return shuffled;
|
|
282
|
+
}
|
|
283
|
+
const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
|
|
284
|
+
export function transformChooseCorrectAnswer(questions, optionsConfig) {
|
|
285
|
+
const shouldShuffleOptions = optionsConfig?.shuffleOptions ?? true;
|
|
286
|
+
const firstContent = questions[0]?.content;
|
|
287
|
+
const groupPayload = questions[0]?.questionGroup?.payload;
|
|
288
|
+
// Separate example from regular questions
|
|
289
|
+
const exampleQuestion = questions.find((q) => q.is_example);
|
|
290
|
+
const regularQuestions = questions.filter((q) => !q.is_example);
|
|
291
|
+
// Build example object if an example question exists
|
|
292
|
+
let example;
|
|
293
|
+
if (exampleQuestion) {
|
|
294
|
+
const exContent = exampleQuestion.content;
|
|
295
|
+
const exOptions = (exContent.options || []).map((opt, idx) => {
|
|
296
|
+
const isObj = typeof opt === 'object' && opt !== null;
|
|
297
|
+
const optId = isObj && opt.id ? String(opt.id) : `${exampleQuestion.id}-opt-${idx}`;
|
|
298
|
+
const optText = isObj ? (opt.text ?? opt.content ?? '') : String(opt);
|
|
299
|
+
const optImage = isObj ? opt.imageUrl : undefined;
|
|
300
|
+
return {
|
|
301
|
+
id: optId,
|
|
302
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
303
|
+
text: optText,
|
|
304
|
+
...(optImage ? { imageUrl: optImage } : {}),
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
const exRawAns = exampleQuestion.correct_answer?.answer ?? exampleQuestion.correct_answer?.optionId;
|
|
308
|
+
let exCorrectId = '';
|
|
309
|
+
if (typeof exRawAns === 'string' && exRawAns) {
|
|
310
|
+
const match = exOptions.find((o) => o.id === exRawAns);
|
|
311
|
+
exCorrectId = match ? match.id : exRawAns;
|
|
312
|
+
}
|
|
313
|
+
else if (typeof exRawAns === 'number' && exRawAns >= 0 && exRawAns < exOptions.length) {
|
|
314
|
+
exCorrectId = exOptions[exRawAns]?.id || '';
|
|
315
|
+
}
|
|
316
|
+
example = {
|
|
317
|
+
questionNumber: exampleQuestion.question_number,
|
|
318
|
+
questionText: exContent.question || '',
|
|
319
|
+
options: exOptions,
|
|
320
|
+
correctOptionId: exCorrectId || exOptions[0]?.id || '',
|
|
321
|
+
optionType: (exContent.optionType || 'text'),
|
|
322
|
+
imageUrl: exContent.imageUrl,
|
|
323
|
+
audioUrl: exContent.audioUrl,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const transformed = regularQuestions.map((q) => {
|
|
327
|
+
const content = q.content;
|
|
328
|
+
const optionType = content.optionType || 'text';
|
|
329
|
+
const options = (content.options || []).map((opt, idx) => {
|
|
330
|
+
const isObj = typeof opt === 'object' && opt !== null;
|
|
331
|
+
const optId = isObj && opt.id ? String(opt.id) : `${q.id}-opt-${idx}`;
|
|
332
|
+
const optText = isObj ? (opt.text ?? opt.content ?? '') : String(opt);
|
|
333
|
+
const optImage = isObj ? opt.imageUrl : undefined;
|
|
334
|
+
return {
|
|
335
|
+
id: optId,
|
|
336
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
337
|
+
text: optText,
|
|
338
|
+
...(optImage ? { imageUrl: optImage } : {}),
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
const rawAns = q.correct_answer?.answer ?? q.correct_answer?.optionId ?? q.correct_answer?.optionIds;
|
|
342
|
+
const findOptionId = (val) => {
|
|
343
|
+
if (val === undefined || val === null)
|
|
344
|
+
return undefined;
|
|
345
|
+
const strVal = String(val);
|
|
346
|
+
const matchedById = options.find((o) => o.id === strVal);
|
|
347
|
+
if (matchedById)
|
|
348
|
+
return matchedById.id;
|
|
349
|
+
// Handle legacy synthetic option IDs like "questionId-opt-0"
|
|
350
|
+
const optMatch = strVal.match(/-opt-(\d+)$/);
|
|
351
|
+
if (optMatch) {
|
|
352
|
+
const optIdx = parseInt(optMatch[1], 10);
|
|
353
|
+
if (optIdx >= 0 && optIdx < options.length) {
|
|
354
|
+
return options[optIdx]?.id;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (typeof val === 'number' && val >= 0 && val < options.length) {
|
|
358
|
+
return options[val]?.id;
|
|
359
|
+
}
|
|
360
|
+
const num = parseInt(strVal, 10);
|
|
361
|
+
if (!isNaN(num) && num >= 0 && num < options.length && !strVal.startsWith('opt-')) {
|
|
362
|
+
return options[num]?.id;
|
|
363
|
+
}
|
|
364
|
+
return strVal;
|
|
365
|
+
};
|
|
366
|
+
let correctOptionIds = [];
|
|
367
|
+
if (Array.isArray(rawAns)) {
|
|
368
|
+
correctOptionIds = rawAns.map(findOptionId).filter((id) => Boolean(id));
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const singleId = findOptionId(rawAns);
|
|
372
|
+
if (singleId)
|
|
373
|
+
correctOptionIds = [singleId];
|
|
374
|
+
}
|
|
375
|
+
// Shuffle options deterministically using questionId as seed when enabled.
|
|
376
|
+
// Since answers use optionId (not index), shuffling display order is safe.
|
|
377
|
+
const finalOptions = shouldShuffleOptions
|
|
378
|
+
? seededShuffle(options, q.id).map((opt, idx) => ({
|
|
379
|
+
...opt,
|
|
380
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
381
|
+
}))
|
|
382
|
+
: options;
|
|
383
|
+
return {
|
|
384
|
+
id: q.id,
|
|
385
|
+
questionNumber: q.question_number,
|
|
386
|
+
questionText: content.question || '',
|
|
387
|
+
options: finalOptions,
|
|
388
|
+
correctOptionId: correctOptionIds[0],
|
|
389
|
+
correctOptionIds,
|
|
390
|
+
imageUrl: content.imageUrl, // Per-question image
|
|
391
|
+
audioUrl: content.audioUrl, // Per-question audio (questionConfig includes "audio")
|
|
392
|
+
optionType: optionType,
|
|
393
|
+
multipleAnswers: Boolean(content.multipleAnswers || Array.isArray(rawAns)),
|
|
394
|
+
explanation: q.explanation,
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
// Each question can carry its own audio (questionConfig "audio"). Only fall back to the
|
|
398
|
+
// first question's audio as a part-level player when nothing has per-question audio —
|
|
399
|
+
// otherwise question 1's clip would be lifted to the top and the rest would be lost.
|
|
400
|
+
const hasPerQuestionAudio = transformed.some((q) => !!q.audioUrl?.trim()) || !!example?.audioUrl?.trim();
|
|
401
|
+
return {
|
|
402
|
+
questions: transformed,
|
|
403
|
+
example,
|
|
404
|
+
imageUrl: firstContent?.imageUrl,
|
|
405
|
+
groupImageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
|
|
406
|
+
groupTitle: groupPayload?.title,
|
|
407
|
+
groupContent: groupPayload?.content,
|
|
408
|
+
articles: groupPayload?.articles,
|
|
409
|
+
audioUrl: hasPerQuestionAudio ? undefined : firstContent?.audioUrl,
|
|
410
|
+
instruction: firstContent?.instruction || 'CHOOSE THE CORRECT ANSWER',
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
export function transformChooseTheCorrectAnswerGroup(questions) {
|
|
414
|
+
const parent = questions[0];
|
|
415
|
+
const content = parent?.content;
|
|
416
|
+
const items = Array.isArray(content?.items) ? content.items : [];
|
|
417
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
418
|
+
? parent.correct_answer.answer
|
|
419
|
+
: [];
|
|
420
|
+
const exampleItem = items.find((item) => item.isExample);
|
|
421
|
+
const regularItems = items.filter((item) => !item.isExample);
|
|
422
|
+
const mapOptions = (options, prefix) => (options || []).map((opt, idx) => {
|
|
423
|
+
const isObj = typeof opt === 'object' && opt !== null;
|
|
424
|
+
return {
|
|
425
|
+
id: isObj && opt.id ? String(opt.id) : `${prefix}-opt-${idx + 1}`,
|
|
426
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
427
|
+
text: isObj ? String(opt.text ?? '') : String(opt),
|
|
428
|
+
...(isObj && opt.imageUrl ? { imageUrl: opt.imageUrl } : {}),
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
let example;
|
|
432
|
+
if (exampleItem) {
|
|
433
|
+
const exampleIndex = items.indexOf(exampleItem);
|
|
434
|
+
const exOptions = mapOptions(exampleItem.content?.options, `${parent.id}-ex`);
|
|
435
|
+
const exCorrect = exampleItem.correctAnswer?.answer || parentCorrect[exampleIndex] || exOptions[0]?.id || '';
|
|
436
|
+
example = {
|
|
437
|
+
questionNumber: exampleItem.questionNumber || exampleIndex + 1,
|
|
438
|
+
questionText: exampleItem.content?.question || '',
|
|
439
|
+
options: exOptions,
|
|
440
|
+
correctOptionId: exCorrect,
|
|
441
|
+
optionType: (exampleItem.content?.optionType || 'text'),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const transformed = regularItems.map((item) => {
|
|
445
|
+
const itemIndex = items.indexOf(item);
|
|
446
|
+
const options = mapOptions(item.content?.options, `${parent.id}-item-${itemIndex}`);
|
|
447
|
+
const correctId = item.correctAnswer?.answer || parentCorrect[itemIndex] || '';
|
|
448
|
+
return {
|
|
449
|
+
id: `${parent.id}-item-${itemIndex}`,
|
|
450
|
+
questionNumber: item.questionNumber || itemIndex + 1,
|
|
451
|
+
questionText: item.content?.question || '',
|
|
452
|
+
options,
|
|
453
|
+
correctOptionId: correctId,
|
|
454
|
+
correctOptionIds: correctId ? [correctId] : [],
|
|
455
|
+
optionType: (item.content?.optionType || 'text'),
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
questions: transformed,
|
|
460
|
+
example,
|
|
461
|
+
instruction: content?.meta?.instruction || 'Choose the correct answer.',
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
export function transformChooseCorrectAdjective(questions) {
|
|
465
|
+
const transformed = questions.map((q) => {
|
|
466
|
+
const content = q.content;
|
|
467
|
+
const sentence = content.sentence || '';
|
|
468
|
+
// Parse sentence with {0} placeholder ([\s\S] crosses newlines)
|
|
469
|
+
const match = sentence.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
|
|
470
|
+
const sentenceBefore = match ? match[1] : sentence;
|
|
471
|
+
const sentenceAfter = match ? match[2] : '';
|
|
472
|
+
return {
|
|
473
|
+
id: q.id,
|
|
474
|
+
questionNumber: q.question_number,
|
|
475
|
+
sentenceBefore,
|
|
476
|
+
sentenceAfter,
|
|
477
|
+
options: (content.options || []).map((opt, idx) => ({
|
|
478
|
+
id: `${q.id}-opt-${idx}`,
|
|
479
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
480
|
+
text: opt,
|
|
481
|
+
})),
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
questions: transformed,
|
|
486
|
+
instruction: 'CHOOSE THE CORRECT ADJECTIVE',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
export function transformWriteShortLetter(question) {
|
|
490
|
+
const content = question.content;
|
|
491
|
+
// New structure: use instructions.mainInstruction as prompt, informations as requirements
|
|
492
|
+
if (content.instructions?.mainInstruction || content.informations?.length) {
|
|
493
|
+
return {
|
|
494
|
+
prompt: content.instructions?.mainInstruction || '',
|
|
495
|
+
requirements: content.informations,
|
|
496
|
+
minWords: content.minWords,
|
|
497
|
+
maxWords: content.maxWords,
|
|
498
|
+
guideImages: content.guideImages,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// Legacy structure: build prompt from contexts array
|
|
502
|
+
const prompt = content.contexts
|
|
503
|
+
?.filter((c) => c.type === 'text')
|
|
504
|
+
.map((c) => c.text)
|
|
505
|
+
.join('\n') || '';
|
|
506
|
+
return {
|
|
507
|
+
prompt,
|
|
508
|
+
minWords: content.minWords,
|
|
509
|
+
maxWords: content.maxWords,
|
|
510
|
+
guideImages: content.guideImages,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
export function transformColorObjects(questions) {
|
|
514
|
+
const firstQuestion = questions[0];
|
|
515
|
+
const content = firstQuestion?.content;
|
|
516
|
+
// Map colorNodes to hintItems format
|
|
517
|
+
const hintItems = content?.colorNodes?.map((node) => ({
|
|
518
|
+
id: node.id,
|
|
519
|
+
color: node.color,
|
|
520
|
+
label: node.label,
|
|
521
|
+
})) ||
|
|
522
|
+
content?.hintItems ||
|
|
523
|
+
[];
|
|
524
|
+
// Map regionNodes to regions format
|
|
525
|
+
const regions = content?.regionNodes?.map((node) => ({
|
|
526
|
+
id: node.id,
|
|
527
|
+
x: node.x,
|
|
528
|
+
y: node.y,
|
|
529
|
+
width: node.width,
|
|
530
|
+
height: node.height,
|
|
531
|
+
correctHintId: node.linkedColorNodeId,
|
|
532
|
+
})) ||
|
|
533
|
+
content?.regions ||
|
|
534
|
+
[];
|
|
535
|
+
// Get raw instructions array for numbered list display
|
|
536
|
+
const instructionsArray = content?.instructions;
|
|
537
|
+
// Get instruction text (joined or single)
|
|
538
|
+
const instruction = (instructionsArray && instructionsArray.length > 0
|
|
539
|
+
? instructionsArray.join(' ')
|
|
540
|
+
: content?.instruction) || 'READ AND COLOR THE OBJECTS';
|
|
541
|
+
return {
|
|
542
|
+
instruction,
|
|
543
|
+
instructions: instructionsArray,
|
|
544
|
+
imageUrl: content?.backgroundImageUrl || content?.imageUrl || '',
|
|
545
|
+
audioUrl: content?.audioUrl,
|
|
546
|
+
hintItems,
|
|
547
|
+
regions,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
export function transformPictureChoose(questions) {
|
|
551
|
+
const firstContent = questions[0]?.content;
|
|
552
|
+
const subQuestions = questions.map((q) => {
|
|
553
|
+
const content = q.content;
|
|
554
|
+
return {
|
|
555
|
+
id: q.id,
|
|
556
|
+
questionNumber: q.question_number,
|
|
557
|
+
imageUrl: content.imageUrl || '',
|
|
558
|
+
questionText: content.questionText,
|
|
559
|
+
options: (content.options || []).map((opt, idx) => ({
|
|
560
|
+
id: opt.id || `${q.id}-opt-${idx}`,
|
|
561
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
562
|
+
text: typeof opt === 'string' ? opt : opt.text,
|
|
563
|
+
})),
|
|
564
|
+
};
|
|
565
|
+
});
|
|
566
|
+
return {
|
|
567
|
+
subQuestions,
|
|
568
|
+
instruction: firstContent?.instruction || 'LOOK AT THE PICTURE AND CHOOSE THE CORRECT ANSWER',
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
export function transformPictureFillBlankChoose(questions) {
|
|
572
|
+
const firstContent = questions[0]?.content;
|
|
573
|
+
const subQuestions = questions.map((q) => {
|
|
574
|
+
const content = q.content;
|
|
575
|
+
return {
|
|
576
|
+
id: q.id,
|
|
577
|
+
questionNumber: q.question_number,
|
|
578
|
+
imageUrl: content.imageUrl || '',
|
|
579
|
+
question: content.question || '',
|
|
580
|
+
options: (content.options || []).map((opt, idx) => ({
|
|
581
|
+
id: opt.id || `${q.id}-opt-${idx}`,
|
|
582
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
583
|
+
text: typeof opt === 'string' ? opt : opt.text,
|
|
584
|
+
})),
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
subQuestions,
|
|
589
|
+
instruction: firstContent?.instruction ||
|
|
590
|
+
'LOOK AT THE PICTURE, FILL IN THE BLANK AND CHOOSE THE ANSWER',
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Detect if data uses new format (separate questions with questionGroup)
|
|
595
|
+
* New format: content has 'question' field (not 'questions' array)
|
|
596
|
+
* AND questionGroup.payload has passage
|
|
597
|
+
*/
|
|
598
|
+
function isNewReadingPassageFormat(questions) {
|
|
599
|
+
const first = questions[0];
|
|
600
|
+
if (!first)
|
|
601
|
+
return false;
|
|
602
|
+
const content = first.content;
|
|
603
|
+
return (content?.question !== undefined &&
|
|
604
|
+
!content?.questions &&
|
|
605
|
+
first.questionGroup?.payload?.passage !== undefined);
|
|
606
|
+
}
|
|
607
|
+
export function transformReadingPassage(questions) {
|
|
608
|
+
if (isNewReadingPassageFormat(questions)) {
|
|
609
|
+
// NEW FORMAT: Multiple questions with shared questionGroup.payload
|
|
610
|
+
const firstQuestion = questions[0];
|
|
611
|
+
const groupPayload = firstQuestion.questionGroup?.payload || {};
|
|
612
|
+
const transformedQuestions = questions.map((q) => {
|
|
613
|
+
const content = q.content;
|
|
614
|
+
return {
|
|
615
|
+
id: q.id,
|
|
616
|
+
questionNumber: q.question_number,
|
|
617
|
+
questionText: content?.question || '',
|
|
618
|
+
options: (content?.options || []).map((opt, idx) => ({
|
|
619
|
+
id: `${q.id}-opt-${idx}`,
|
|
620
|
+
label: OPTION_LABELS[idx] || String(idx + 1),
|
|
621
|
+
text: typeof opt === 'string' ? opt : String(opt),
|
|
622
|
+
})),
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
return {
|
|
626
|
+
passage: groupPayload.passage || '',
|
|
627
|
+
passageTitle: groupPayload.passageTitle,
|
|
628
|
+
passageImageUrl: groupPayload.passageImageUrl || groupPayload.mainImageUrl,
|
|
629
|
+
instruction: groupPayload.instructions || 'READ THE PASSAGE AND ANSWER QUESTIONS',
|
|
630
|
+
questions: transformedQuestions,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
// OLD FORMAT: Single question with embedded sub-questions in content.questions[]
|
|
634
|
+
const firstQuestion = questions[0];
|
|
635
|
+
const content = firstQuestion?.content;
|
|
636
|
+
const subQuestions = content?.questions?.map((sq, idx) => ({
|
|
637
|
+
id: sq.id || `${firstQuestion.id}-sq-${idx}`,
|
|
638
|
+
questionNumber: idx + 1,
|
|
639
|
+
questionText: sq.question,
|
|
640
|
+
options: (sq.options || []).map((opt, optIdx) => ({
|
|
641
|
+
id: `${sq.id || firstQuestion.id}-opt-${optIdx}`,
|
|
642
|
+
label: OPTION_LABELS[optIdx] || String(optIdx + 1),
|
|
643
|
+
text: opt,
|
|
644
|
+
})),
|
|
645
|
+
})) || [];
|
|
646
|
+
return {
|
|
647
|
+
passage: content?.passage || '',
|
|
648
|
+
passageTitle: content?.passageTitle,
|
|
649
|
+
passageImageUrl: content?.passageImageUrl || content?.mainImageUrl,
|
|
650
|
+
instruction: content?.instructions?.mainInstruction || 'READ THE PASSAGE AND ANSWER QUESTIONS',
|
|
651
|
+
questions: subQuestions,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
export function transformLabelPicture(questions) {
|
|
655
|
+
const firstQuestion = questions[0];
|
|
656
|
+
const content = firstQuestion?.content;
|
|
657
|
+
return {
|
|
658
|
+
imageUrl: content?.imageUrl || '',
|
|
659
|
+
wordBank: content?.wordBank || [],
|
|
660
|
+
labels: content?.labels?.map((l) => ({
|
|
661
|
+
id: l.id,
|
|
662
|
+
number: l.number,
|
|
663
|
+
isPreFilled: l.isPreFilled || false,
|
|
664
|
+
correctAnswer: l.correctAnswer, // From questionContent for preFilled labels
|
|
665
|
+
})) || [],
|
|
666
|
+
instruction: content?.instruction || 'LABEL THE PICTURE',
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Detect if content uses new format (single word with {n} placeholders)
|
|
671
|
+
*/
|
|
672
|
+
function isNewFillWordHintFormat(content) {
|
|
673
|
+
return content && typeof content.word === 'string' && !content.subQuestions;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Parse word string with numbered placeholders like "c{0}t, {1}lephan"
|
|
677
|
+
* Returns array of parsed blanks with their segments
|
|
678
|
+
*/
|
|
679
|
+
function parseNumberedPlaceholders(word) {
|
|
680
|
+
const results = [];
|
|
681
|
+
// Split by comma to handle multiple words like "c{0}t, {1}lephan"
|
|
682
|
+
const wordSegments = word.split(',').map((s) => s.trim());
|
|
683
|
+
for (const segment of wordSegments) {
|
|
684
|
+
// Find all placeholders in this segment
|
|
685
|
+
let match;
|
|
686
|
+
const segmentRegex = /\{(\d+)\}/g;
|
|
687
|
+
while ((match = segmentRegex.exec(segment)) !== null) {
|
|
688
|
+
const placeholderIndex = parseInt(match[1], 10);
|
|
689
|
+
// Replace {n} with _ for display
|
|
690
|
+
const displayWord = segment.replace(match[0], '_');
|
|
691
|
+
results.push({
|
|
692
|
+
index: placeholderIndex,
|
|
693
|
+
displayWord,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Sort by index to ensure correct order
|
|
698
|
+
results.sort((a, b) => a.index - b.index);
|
|
699
|
+
return results;
|
|
700
|
+
}
|
|
701
|
+
export function transformFillWordHint(questions) {
|
|
702
|
+
const firstQuestion = questions[0];
|
|
703
|
+
const content = firstQuestion?.content;
|
|
704
|
+
// MULTI-QUESTION FORMAT: Multiple separate questions with same group_id
|
|
705
|
+
// Each has own id, imageUrl, and single {0} placeholder
|
|
706
|
+
if (questions.length > 1 && questions.every((q) => isNewFillWordHintFormat(q.content))) {
|
|
707
|
+
const hintLetters = firstQuestion.questionGroup?.payload?.hintLetters || [];
|
|
708
|
+
const subQuestions = questions.map((q) => {
|
|
709
|
+
const qContent = q.content;
|
|
710
|
+
// Replace {0} with underscore for display
|
|
711
|
+
const displayWord = qContent.word.replace(/\{\d+\}/, '_');
|
|
712
|
+
return {
|
|
713
|
+
id: q.id, // Use actual question ID (not synthetic)
|
|
714
|
+
questionNumber: q.question_number,
|
|
715
|
+
imageUrl: qContent.imageUrl || '',
|
|
716
|
+
word: displayWord,
|
|
717
|
+
// No blankIndex → triggers OLD FORMAT rendering (grid with individual images)
|
|
718
|
+
};
|
|
719
|
+
});
|
|
720
|
+
return {
|
|
721
|
+
subQuestions,
|
|
722
|
+
hintLetters,
|
|
723
|
+
instruction: content?.instruction ||
|
|
724
|
+
'LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER',
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
// Detect format and transform accordingly
|
|
728
|
+
if (isNewFillWordHintFormat(content)) {
|
|
729
|
+
// NEW FORMAT: Single word with {0}, {1} placeholders
|
|
730
|
+
const newContent = content;
|
|
731
|
+
// Get hintLetters from questionGroup.payload (new location)
|
|
732
|
+
const hintLetters = firstQuestion.questionGroup?.payload?.hintLetters || [];
|
|
733
|
+
// Parse the word string to extract placeholders
|
|
734
|
+
const placeholders = parseNumberedPlaceholders(newContent.word);
|
|
735
|
+
// Create sub-questions for each placeholder
|
|
736
|
+
const subQuestions = placeholders.map((p) => ({
|
|
737
|
+
id: `${firstQuestion.id}-blank-${p.index}`,
|
|
738
|
+
questionNumber: firstQuestion.question_number,
|
|
739
|
+
imageUrl: newContent.imageUrl || '',
|
|
740
|
+
word: p.displayWord,
|
|
741
|
+
blankIndex: p.index,
|
|
742
|
+
}));
|
|
743
|
+
return {
|
|
744
|
+
subQuestions,
|
|
745
|
+
hintLetters,
|
|
746
|
+
instruction: newContent.instruction ||
|
|
747
|
+
'LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER',
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
// OLD FORMAT: subQuestions array with individual images
|
|
751
|
+
const oldContent = content;
|
|
752
|
+
const subQuestions = oldContent?.subQuestions?.map((sq, idx) => ({
|
|
753
|
+
id: sq.id,
|
|
754
|
+
questionNumber: firstQuestion.question_number + idx,
|
|
755
|
+
imageUrl: sq.imageUrl || '',
|
|
756
|
+
word: sq.word || '',
|
|
757
|
+
})) || [];
|
|
758
|
+
// Try hintLetters from content first, then from questionGroup.payload
|
|
759
|
+
const hintLetters = oldContent?.hintLetters ||
|
|
760
|
+
firstQuestion.questionGroup?.payload?.hintLetters ||
|
|
761
|
+
[];
|
|
762
|
+
return {
|
|
763
|
+
subQuestions,
|
|
764
|
+
hintLetters,
|
|
765
|
+
instruction: oldContent?.instruction ||
|
|
766
|
+
'LOOK AT THE PICTURE AND FILL IN THE MISSING LETTER',
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
export function transformGridFill(questions) {
|
|
770
|
+
const firstQuestion = questions[0];
|
|
771
|
+
const content = firstQuestion?.content;
|
|
772
|
+
return {
|
|
773
|
+
grid: content?.grid || [],
|
|
774
|
+
rows: content?.rows || 0,
|
|
775
|
+
columns: content?.columns || 0,
|
|
776
|
+
instruction: content?.instruction || 'FILL IN THE MISSING WORDS',
|
|
777
|
+
disabledCells: content?.disabledCells || [],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
export function transformAnswerTheQuestion(questions) {
|
|
781
|
+
const firstQuestion = questions[0];
|
|
782
|
+
const groupPayload = firstQuestion?.questionGroup?.payload;
|
|
783
|
+
const firstContent = firstQuestion?.content;
|
|
784
|
+
const transformed = questions.map((q) => {
|
|
785
|
+
const content = q.content;
|
|
786
|
+
const isExample = q.is_example || content.isExample || false;
|
|
787
|
+
return {
|
|
788
|
+
id: q.id,
|
|
789
|
+
questionNumber: q.question_number,
|
|
790
|
+
question: (content.question || '').replace(/\{\d+\}/g, '______'),
|
|
791
|
+
imageUrl: content.imageUrl,
|
|
792
|
+
prefixText: content.prefixText,
|
|
793
|
+
suffixText: content.suffixText,
|
|
794
|
+
suggestedAnswers: content.suggestedAnswers,
|
|
795
|
+
isExample,
|
|
796
|
+
exampleAnswer: isExample ? q.correct_answer?.answer : undefined,
|
|
797
|
+
};
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
questions: transformed,
|
|
801
|
+
instruction: 'ANSWER THE QUESTION',
|
|
802
|
+
imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl || firstContent?.imageUrl,
|
|
803
|
+
title: firstContent?.title,
|
|
804
|
+
groupContent: groupPayload?.groupContent || firstContent?.groupContent,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
export function transformTrueFalse(questions) {
|
|
808
|
+
const groupPayload = questions[0]?.questionGroup?.payload;
|
|
809
|
+
const transformed = questions.map((q) => {
|
|
810
|
+
const content = q.content;
|
|
811
|
+
const isExample = q.is_example || content.isExample || false;
|
|
812
|
+
const exampleCorrectAnswer = isExample && typeof q.correct_answer?.answer === 'boolean'
|
|
813
|
+
? q.correct_answer.answer
|
|
814
|
+
: undefined;
|
|
815
|
+
return {
|
|
816
|
+
id: q.id,
|
|
817
|
+
questionNumber: q.question_number,
|
|
818
|
+
question: content.question || '',
|
|
819
|
+
imageUrl: content.imageUrl,
|
|
820
|
+
isExample,
|
|
821
|
+
exampleCorrectAnswer,
|
|
822
|
+
};
|
|
823
|
+
});
|
|
824
|
+
return {
|
|
825
|
+
questions: transformed,
|
|
826
|
+
instruction: 'READ AND CHOOSE TRUE OR FALSE',
|
|
827
|
+
imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
|
|
828
|
+
groupContent: groupPayload?.content || groupPayload?.groupContent,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
// ============================================
|
|
832
|
+
// TRUE_FALSE_GROUP Transformer
|
|
833
|
+
// ============================================
|
|
834
|
+
export function transformTrueFalseGroup(questions) {
|
|
835
|
+
const parent = questions[0];
|
|
836
|
+
const content = parent?.content;
|
|
837
|
+
const items = Array.isArray(content?.items) ? content.items : [];
|
|
838
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
839
|
+
? parent.correct_answer.answer
|
|
840
|
+
: [];
|
|
841
|
+
const transformed = items.map((item, index) => {
|
|
842
|
+
const isExample = item.isExample === true;
|
|
843
|
+
const exampleCorrectAnswer = isExample && typeof (item.correctAnswer?.answer ?? parentCorrect[index]) === 'boolean'
|
|
844
|
+
? (item.correctAnswer?.answer ?? parentCorrect[index])
|
|
845
|
+
: undefined;
|
|
846
|
+
return {
|
|
847
|
+
id: `${parent.id}-item-${index}`,
|
|
848
|
+
questionNumber: item.questionNumber || index + 1,
|
|
849
|
+
question: item.content?.question || '',
|
|
850
|
+
isExample,
|
|
851
|
+
exampleCorrectAnswer,
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
return {
|
|
855
|
+
questions: transformed,
|
|
856
|
+
instruction: content?.meta?.instruction || 'READ AND CHOOSE TRUE OR FALSE',
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
export function transformWordOrdering(questions) {
|
|
860
|
+
const transformed = questions.map((q) => {
|
|
861
|
+
const content = q.content;
|
|
862
|
+
const isExample = q.is_example || content.isExample || false;
|
|
863
|
+
const exampleAnswer = Array.isArray(q.correct_answer?.answer)
|
|
864
|
+
? q.correct_answer.answer
|
|
865
|
+
: Array.isArray(content.answer)
|
|
866
|
+
? content.answer
|
|
867
|
+
: undefined;
|
|
868
|
+
return {
|
|
869
|
+
id: q.id,
|
|
870
|
+
questionNumber: q.question_number,
|
|
871
|
+
words: content.words || [],
|
|
872
|
+
imageUrl: content.imageUrl,
|
|
873
|
+
isExample,
|
|
874
|
+
exampleAnswer: isExample ? exampleAnswer : undefined,
|
|
875
|
+
};
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
questions: transformed,
|
|
879
|
+
instruction: 'ARRANGE THE LETTERS TO MAKE A WORD',
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
export function transformWordFillParagraph(questions) {
|
|
883
|
+
const firstQuestion = questions[0];
|
|
884
|
+
const groupPayload = firstQuestion?.questionGroup?.payload;
|
|
885
|
+
const content = firstQuestion?.content;
|
|
886
|
+
// Try to find title in content of any question or fallback to group payload
|
|
887
|
+
const title = content?.title || groupPayload?.title || questions.find(q => q.content?.title)?.content?.title;
|
|
888
|
+
// Normalize wordBank: handle both string[] (legacy DB format) and WordBankItem[] formats
|
|
889
|
+
const rawWordBank = content?.wordBank || [];
|
|
890
|
+
const wordBank = rawWordBank.map((item) => typeof item === 'string' ? { word: item } : item);
|
|
891
|
+
const storedViewMode = content?.viewMode;
|
|
892
|
+
const hasOptions = content?.wordBank?.[0]?.options !== undefined;
|
|
893
|
+
let viewMode;
|
|
894
|
+
if (storedViewMode === 'EMAIL') {
|
|
895
|
+
viewMode = 'EMAIL';
|
|
896
|
+
}
|
|
897
|
+
else if (!storedViewMode && (content?.emailFrom || content?.emailTo)) {
|
|
898
|
+
// Defensive fallback: if viewMode is null/undefined but email fields are present, treat as EMAIL
|
|
899
|
+
viewMode = 'EMAIL';
|
|
900
|
+
}
|
|
901
|
+
else if (storedViewMode === 'DEFAULT') {
|
|
902
|
+
viewMode = 'DEFAULT';
|
|
903
|
+
}
|
|
904
|
+
else if (storedViewMode === 'WITH_OPTIONS' || hasOptions) {
|
|
905
|
+
viewMode = 'WITH_OPTIONS';
|
|
906
|
+
}
|
|
907
|
+
else if (!storedViewMode && rawWordBank.length === 0) {
|
|
908
|
+
viewMode = 'DEFAULT';
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
viewMode = 'WITH_WORD_BANK';
|
|
912
|
+
}
|
|
913
|
+
const emailFrom = content?.emailFrom || content?.metadata?.emailFrom;
|
|
914
|
+
const emailTo = content?.emailTo || content?.metadata?.emailTo;
|
|
915
|
+
return {
|
|
916
|
+
questionId: firstQuestion?.id || '',
|
|
917
|
+
paragraph: content?.paragraph || '',
|
|
918
|
+
title,
|
|
919
|
+
imageUrl: content?.imageUrl,
|
|
920
|
+
wordBank,
|
|
921
|
+
viewMode,
|
|
922
|
+
instruction: 'READ AND FILL IN THE BLANKS',
|
|
923
|
+
exampleAnswer: content?.exampleAnswer,
|
|
924
|
+
emailFrom,
|
|
925
|
+
emailTo,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
export function transformMatchByWritingAnswer(questions) {
|
|
929
|
+
const groupPayload = questions[0]?.questionGroup?.payload;
|
|
930
|
+
const isConversation = questions.some((q) => !!q.content?.askerName);
|
|
931
|
+
const transformed = questions.map((q) => {
|
|
932
|
+
const content = q.content;
|
|
933
|
+
const isExample = q.is_example || content.isExample || false;
|
|
934
|
+
return {
|
|
935
|
+
id: q.id,
|
|
936
|
+
questionNumber: q.question_number,
|
|
937
|
+
question: content.question || '',
|
|
938
|
+
imageUrl: content.imageUrl,
|
|
939
|
+
isExample,
|
|
940
|
+
exampleAnswer: isExample ? q.correct_answer?.answer : undefined,
|
|
941
|
+
askerName: content.askerName,
|
|
942
|
+
answererName: content.answererName,
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
const options = groupPayload?.options?.map((opt) => ({
|
|
946
|
+
answer: opt.answer,
|
|
947
|
+
imageUrl: opt.imageUrl || '',
|
|
948
|
+
content: opt.content || '',
|
|
949
|
+
})) || [];
|
|
950
|
+
const firstConversationQ = isConversation
|
|
951
|
+
? questions[0]?.content
|
|
952
|
+
: undefined;
|
|
953
|
+
return {
|
|
954
|
+
questions: transformed,
|
|
955
|
+
options,
|
|
956
|
+
instruction: 'LOOK AND WRITE THE ANSWER',
|
|
957
|
+
viewMode: isConversation ? 'CONVERSATION' : 'DEFAULT',
|
|
958
|
+
groupImageUrl: groupPayload?.groupImageUrl || '',
|
|
959
|
+
askerName: firstConversationQ?.askerName,
|
|
960
|
+
answererName: firstConversationQ?.answererName,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
export function transformWriteSentences(questions) {
|
|
964
|
+
const firstQuestion = questions[0];
|
|
965
|
+
const content = firstQuestion?.content;
|
|
966
|
+
const groupPayload = firstQuestion?.questionGroup?.payload;
|
|
967
|
+
return {
|
|
968
|
+
questions: questions.map((q) => ({
|
|
969
|
+
id: q.id,
|
|
970
|
+
questionNumber: q.question_number,
|
|
971
|
+
})),
|
|
972
|
+
instruction: content?.instruction || 'WRITE SENTENCES ABOUT THE PICTURE',
|
|
973
|
+
imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl || content?.imageUrl,
|
|
974
|
+
title: content?.title,
|
|
975
|
+
groupContent: groupPayload?.groupContent || content?.groupContent,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function normalizeWfsfBlankStyle(raw) {
|
|
979
|
+
const v = String(raw || '').toLowerCase();
|
|
980
|
+
if (v === 'letter' || v === 'box')
|
|
981
|
+
return 'letter';
|
|
982
|
+
if (v === 'phrase' || v === 'sentence')
|
|
983
|
+
return 'phrase';
|
|
984
|
+
return 'word';
|
|
985
|
+
}
|
|
986
|
+
function normalizeWfsfWordBank(raw) {
|
|
987
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
988
|
+
return undefined;
|
|
989
|
+
const bank = raw.map((entry) => {
|
|
990
|
+
if (entry == null)
|
|
991
|
+
return null;
|
|
992
|
+
if (typeof entry !== 'object' || Array.isArray(entry))
|
|
993
|
+
return null;
|
|
994
|
+
const rec = entry;
|
|
995
|
+
if (Array.isArray(rec.options)) {
|
|
996
|
+
const options = rec.options
|
|
997
|
+
.map((o) => String(o ?? '').trim())
|
|
998
|
+
.filter((o) => o.length > 0);
|
|
999
|
+
return options.length >= 2 ? { options } : null;
|
|
1000
|
+
}
|
|
1001
|
+
const word = String(rec.word ?? '').trim();
|
|
1002
|
+
if (word) {
|
|
1003
|
+
const w = rec.isExample
|
|
1004
|
+
? { word, isExample: true }
|
|
1005
|
+
: { word };
|
|
1006
|
+
return w;
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
});
|
|
1010
|
+
return bank.some((e) => e != null) ? bank : undefined;
|
|
1011
|
+
}
|
|
1012
|
+
export function transformWordFillStructuredForm(questions) {
|
|
1013
|
+
const firstQuestion = questions[0];
|
|
1014
|
+
const groupPayload = firstQuestion?.questionGroup?.payload;
|
|
1015
|
+
const transformed = questions.map((q) => {
|
|
1016
|
+
const content = q.content;
|
|
1017
|
+
const correctAnswer = q.correct_answer || {};
|
|
1018
|
+
// Resolve content: prefer single `content` string, fallback to merging old `blocks[]`
|
|
1019
|
+
let resolvedContent = content?.content || '';
|
|
1020
|
+
const blocks = content?.blocks;
|
|
1021
|
+
if (!resolvedContent && blocks?.length) {
|
|
1022
|
+
resolvedContent = blocks
|
|
1023
|
+
.map((b) => {
|
|
1024
|
+
const heading = b.heading ? `<h3>${b.heading}</h3>` : '';
|
|
1025
|
+
return heading + (b.content || '');
|
|
1026
|
+
})
|
|
1027
|
+
.join('');
|
|
1028
|
+
}
|
|
1029
|
+
const wordBank = normalizeWfsfWordBank(content?.wordBank);
|
|
1030
|
+
const hasOptions = Boolean(wordBank?.some((e) => e != null && 'options' in e && Array.isArray(e.options)));
|
|
1031
|
+
const hasWords = Boolean(wordBank?.some((e) => e != null && 'word' in e && e.word));
|
|
1032
|
+
const viewMode = content?.viewMode ||
|
|
1033
|
+
(hasOptions ? 'WITH_OPTIONS' : hasWords ? 'WITH_WORD_BANK' : undefined);
|
|
1034
|
+
return {
|
|
1035
|
+
id: q.id,
|
|
1036
|
+
questionNumber: q.question_number,
|
|
1037
|
+
title: content?.title || '',
|
|
1038
|
+
imageUrl: content?.imageUrl,
|
|
1039
|
+
content: resolvedContent,
|
|
1040
|
+
answers: correctAnswer?.answers || {},
|
|
1041
|
+
caseSensitive: correctAnswer?.caseSensitive ?? content?.caseSensitive ?? false,
|
|
1042
|
+
blankStyle: normalizeWfsfBlankStyle(content?.blankStyle),
|
|
1043
|
+
...(viewMode ? { viewMode } : {}),
|
|
1044
|
+
...(wordBank ? { wordBank } : {}),
|
|
1045
|
+
};
|
|
1046
|
+
});
|
|
1047
|
+
return {
|
|
1048
|
+
questions: transformed,
|
|
1049
|
+
instruction: groupPayload?.instructions || 'COMPLETE THE FORM BELOW',
|
|
1050
|
+
groupTitle: groupPayload?.title,
|
|
1051
|
+
groupContent: groupPayload?.content,
|
|
1052
|
+
groupImageUrl: groupPayload?.imageUrl,
|
|
1053
|
+
audioUrl: firstQuestion?.audio_url || groupPayload?.audioUrl,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
export function transformListenChooseObjects(questions) {
|
|
1057
|
+
const firstQuestion = questions[0];
|
|
1058
|
+
const content = firstQuestion?.content;
|
|
1059
|
+
const groupPayload = firstQuestion?.questionGroup?.payload || {};
|
|
1060
|
+
return {
|
|
1061
|
+
audioUrl: content?.audioUrl || '',
|
|
1062
|
+
backgroundImageUrl: groupPayload.backgroundImageUrl || '',
|
|
1063
|
+
objectNodes: (groupPayload.objectNodes || []),
|
|
1064
|
+
// Backend returns correctAnswer in review mode (snake_case or camelCase)
|
|
1065
|
+
correctObjectId: firstQuestion?.correctAnswer?.objectId ||
|
|
1066
|
+
firstQuestion?.correct_answer?.objectId ||
|
|
1067
|
+
'',
|
|
1068
|
+
isBackup: content?.isBackup || false,
|
|
1069
|
+
instruction: 'LISTEN AND CHOOSE THE CORRECT OBJECT',
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
export function transformListenSpeakAnswer(questions) {
|
|
1073
|
+
const transformed = questions.map((q) => {
|
|
1074
|
+
const content = q.content;
|
|
1075
|
+
const groupPayload = q.questionGroup?.payload || {};
|
|
1076
|
+
return {
|
|
1077
|
+
id: q.id,
|
|
1078
|
+
questionNumber: q.question_number,
|
|
1079
|
+
audioUrl: content?.audioUrl || '',
|
|
1080
|
+
arrowNode: content?.arrowNode,
|
|
1081
|
+
backgroundImageUrl: groupPayload.backgroundImageUrl || groupPayload.imageUrl || '',
|
|
1082
|
+
isBackup: content?.isBackup || false,
|
|
1083
|
+
};
|
|
1084
|
+
});
|
|
1085
|
+
return {
|
|
1086
|
+
questions: transformed,
|
|
1087
|
+
instruction: 'LISTEN AND SPEAK YOUR ANSWER',
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
export function transformWordOrderAndMatchGroup(questions) {
|
|
1091
|
+
const parent = questions[0];
|
|
1092
|
+
const rawContent = (parent?.content || {});
|
|
1093
|
+
const rawList = parent?.wordOrderMatchList || rawContent.wordOrderMatchList || rawContent;
|
|
1094
|
+
const meta = rawList.meta || rawContent.meta || {};
|
|
1095
|
+
const items = Array.isArray(rawList.items)
|
|
1096
|
+
? rawList.items
|
|
1097
|
+
: Array.isArray(rawContent.items)
|
|
1098
|
+
? rawContent.items
|
|
1099
|
+
: [];
|
|
1100
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
1101
|
+
? parent.correct_answer.answer
|
|
1102
|
+
: [];
|
|
1103
|
+
const matchOptions = Array.isArray(meta.matchOptions)
|
|
1104
|
+
? meta.matchOptions.map((opt) => String(opt || '').trim().toUpperCase()).filter(Boolean)
|
|
1105
|
+
: ['A', 'B', 'C', 'D', 'E', 'F'];
|
|
1106
|
+
const transformed = items.map((item, index) => {
|
|
1107
|
+
const rawItemContent = item?.content || {};
|
|
1108
|
+
const scrambledWords = Array.isArray(rawItemContent.scrambledWords)
|
|
1109
|
+
? rawItemContent.scrambledWords.map((w) => String(w || '').trim()).filter(Boolean)
|
|
1110
|
+
: [];
|
|
1111
|
+
const correctAnswerRaw = item?.correctAnswer || item?.correct_answer || {};
|
|
1112
|
+
const answerObj = correctAnswerRaw.answer && typeof correctAnswerRaw.answer === 'object'
|
|
1113
|
+
? correctAnswerRaw.answer
|
|
1114
|
+
: correctAnswerRaw && typeof correctAnswerRaw === 'object' && (correctAnswerRaw.orderedPhrase || correctAnswerRaw.matchLabel)
|
|
1115
|
+
? correctAnswerRaw
|
|
1116
|
+
: parentCorrect[index] || {};
|
|
1117
|
+
const orderedPhrase = typeof answerObj.orderedPhrase === 'string' ? answerObj.orderedPhrase : '';
|
|
1118
|
+
const matchLabel = typeof answerObj.matchLabel === 'string' ? answerObj.matchLabel.toUpperCase() : 'A';
|
|
1119
|
+
const isExample = item?.isExample === true;
|
|
1120
|
+
return {
|
|
1121
|
+
id: `${parent?.id || 'q'}-item-${index}`,
|
|
1122
|
+
questionNumber: item.questionNumber || index + 1,
|
|
1123
|
+
scrambledWords,
|
|
1124
|
+
orderedPhrase,
|
|
1125
|
+
matchLabel,
|
|
1126
|
+
isExample,
|
|
1127
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
1128
|
+
};
|
|
1129
|
+
});
|
|
1130
|
+
return {
|
|
1131
|
+
questions: transformed,
|
|
1132
|
+
instruction: meta.instruction ||
|
|
1133
|
+
'Put the useful phrases in order, then match them to parts A–F in Exercise 4.',
|
|
1134
|
+
matchOptions,
|
|
1135
|
+
explanation: parent?.explanation,
|
|
1136
|
+
};
|
|
1137
|
+
}
|