@tinyweb_dev/oe-exam-sdk 0.2.8 → 0.2.10

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.
Files changed (104) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +7 -3
  3. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -4
  4. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
  6. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  7. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  8. package/dist/components/exams/take/utils/answer-transformers.js +8 -1
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
  10. package/dist/components/exams/take/utils/question-transformers.js +49 -4
  11. package/dist/components/index.d.ts +1 -0
  12. package/dist/components/index.js +1 -0
  13. package/dist/components/questions/_shared/config/question-types.config.js +18 -0
  14. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
  15. package/dist/components/questions/_shared/types/index.d.ts +1 -0
  16. package/dist/components/questions/_shared/types/index.js +1 -0
  17. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
  18. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
  19. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
  20. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
  21. package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
  22. package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
  23. package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
  24. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  25. package/dist/components/questions/creator/question-type-registry.js +6 -0
  26. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +6 -4
  27. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +16 -4
  28. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -3
  29. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +6 -4
  30. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
  31. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  32. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
  33. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
  34. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
  35. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
  36. package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
  37. package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
  38. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
  39. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
  40. package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
  41. package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
  42. package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
  43. package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
  44. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
  45. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
  46. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
  47. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
  48. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
  49. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
  50. package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
  51. package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
  52. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
  53. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
  54. package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
  55. package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
  56. package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
  57. package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
  58. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
  59. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
  60. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
  61. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +106 -0
  62. package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
  63. package/dist/components/questions/types/word-ordering-group/index.js +5 -0
  64. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
  65. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
  66. package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
  67. package/dist/components/questions/types/word-ordering-group/register.js +36 -0
  68. package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
  69. package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
  70. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
  71. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
  72. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
  73. package/dist/components/questions/viewer/QuestionViewer.js +6 -0
  74. package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
  75. package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
  76. package/dist/components/questions/viewer/index.d.ts +2 -0
  77. package/dist/components/questions/viewer/index.js +2 -0
  78. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +3 -2
  79. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  80. package/dist/components/results/renderers/review-question-body-movers.js +6 -2
  81. package/dist/components/results/review-data.js +13 -0
  82. package/dist/components/shared/RichTextEditor.d.ts +4 -2
  83. package/dist/components/shared/RichTextEditor.js +34 -11
  84. package/dist/components/shared/RichTextHtml.d.ts +8 -0
  85. package/dist/components/shared/RichTextHtml.js +9 -0
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/shared/constants/question-skills.js +6 -0
  89. package/dist/shared/lib/rich-text.d.ts +3 -0
  90. package/dist/shared/lib/rich-text.js +24 -0
  91. package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
  92. package/dist/shared/lib/utils/question-transform.js +6 -0
  93. package/dist/shared/lib/validation/choose-correct-answer.validation.js +4 -2
  94. package/dist/shared/types/common.types.d.ts +1 -1
  95. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  96. package/dist/shared/types/questions/index.d.ts +3 -0
  97. package/dist/shared/types/questions/index.js +6 -0
  98. package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
  99. package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
  100. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
  101. package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
  102. package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
  103. package/dist/shared/types/questions/word-ordering-group.js +1 -0
  104. package/package.json +1 -1
@@ -637,6 +637,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
637
637
  map.set(`${question.id}-item-${index}`, question.id);
638
638
  });
639
639
  }
640
+ if (part.questionType === 'WORD_ORDERING_GROUP' &&
641
+ Array.isArray(content?.items)) {
642
+ content.items.forEach((_, index) => {
643
+ map.set(`${question.id}-item-${index}`, question.id);
644
+ });
645
+ }
640
646
  });
641
647
  });
642
648
  return map;
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * 2. Transforms data to renderer-specific props
9
9
  * 3. Renders the correct component for each question type
10
10
  */
11
- import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
11
+ import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
12
12
  import { SpeakingQuestionRenderer } from './speaking/renderers';
13
- import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
13
+ import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
14
14
  import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
15
15
  import { normalizeColoringAssignments } from '../utils/coloring-assignments';
16
16
  export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
@@ -55,7 +55,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
55
55
  }
56
56
  case 'CHOOSE_THE_CORRECT_ANSWER_GROUP': {
57
57
  const data = transformChooseTheCorrectAnswerGroup(questions);
58
- return (_jsx(MoversChooseBestAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, example: data.example, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
58
+ return (_jsx(MoversChooseBestAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, imageUrl: data.imageUrl, example: data.example, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
59
59
  }
60
60
  case 'CHOOSE_CORRECT_ADJECTIVE': {
61
61
  const data = transformChooseCorrectAdjective(questions);
@@ -181,6 +181,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
181
181
  const data = transformTrueFalse(questions);
182
182
  return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
183
183
  }
184
+ case 'WORD_ORDERING_GROUP': {
185
+ const data = transformWordOrderingGroupTake(questions);
186
+ return (_jsx(MoversWordOrderingGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: data.questions.filter((q) => !q.isExample).length, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
187
+ }
184
188
  case 'WORD_ORDERING': {
185
189
  const data = transformWordOrdering(questions);
186
190
  return (_jsx(MoversWordOrderingRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../../../shared/lib/utils';
4
4
  import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
5
+ import { RichTextHtml } from '../../../../../components/shared/RichTextHtml';
5
6
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
6
7
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
7
8
  import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
@@ -42,14 +43,14 @@ function normalizeRenderableText(value) {
42
43
  }
43
44
  // Example Section Component
44
45
  function ExampleSection({ example }) {
45
- return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [example.questionNumber, ". ", normalizeRenderableText(example.questionText)] }), example.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: example.audioUrl, compact: true, dense: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: example.imageUrl, className: "mb-3" }), example.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${example.options.length}, minmax(0, 1fr))` }, children: example.options.map((option) => {
46
+ return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("div", { className: "mb-3 flex items-start gap-1.5 text-[19px] font-bold text-[#001590]", children: [_jsxs("span", { children: [example.questionNumber, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(example.questionText), className: "flex-1 font-bold text-[#001590]" })] }), example.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: example.audioUrl, compact: true, dense: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: example.imageUrl, className: "mb-3" }), example.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${example.options.length}, minmax(0, 1fr))` }, children: example.options.map((option) => {
46
47
  const isCorrect = option.id === example.correctOptionId;
47
48
  return (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-40 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", isCorrect
48
49
  ? "bg-green-500 border-green-500 text-white"
49
50
  : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id));
50
51
  }) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: example.options.map((option) => {
51
52
  const isCorrect = option.id === example.correctOptionId;
52
- return (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: normalizeRenderableText(option.text) }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 rounded-full ${isCorrect ? 'border-green-500 bg-green-500' : 'border-gray-300 bg-white'}`, children: isCorrect && (_jsx("div", { className: "h-2 w-2 rounded-full bg-white" })) })] }, option.id));
53
+ return (_jsxs("div", { className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(option.text), className: "flex-1 text-sm text-gray-700" }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 rounded-full ${isCorrect ? 'border-green-500 bg-green-500' : 'border-gray-300 bg-white'}`, children: isCorrect && (_jsx("div", { className: "h-2 w-2 rounded-full bg-white" })) })] }, option.id));
53
54
  }) }))] }));
54
55
  }
55
56
  // Single Question Component
@@ -99,9 +100,9 @@ function QuestionItem({ question, selectedOptionIds, onSelect, isReviewMode, id,
99
100
  }
100
101
  return selectedOptionIds.includes(optionId) || correctOptionIds.includes(optionId);
101
102
  };
102
- return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [question.questionNumber, ". ", normalizeRenderableText(question.questionText)] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), question.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), question.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-35 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", showIndicatorMark(option.id)
103
+ return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("div", { className: "mb-3 flex items-start gap-1.5 text-[19px] font-bold text-[#001590]", children: [_jsxs("span", { children: [question.questionNumber, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(question.questionText), className: "flex-1 font-bold text-[#001590]" })] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), question.audioUrl && (_jsx("div", { className: "mb-3", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), question.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-35 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", showIndicatorMark(option.id)
103
104
  ? "bg-blue-600 border-blue-600 text-white"
104
- : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id))) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: normalizeRenderableText(option.text) }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 ${isMultipleAnswers ? 'rounded-md' : 'rounded-full'} ${getIndicatorStyle(option.id)}`, children: showIndicatorMark(option.id) && (_jsx("div", { className: `${isMultipleAnswers ? 'h-2.5 w-2.5 rounded-sm' : 'h-2 w-2 rounded-full'} bg-white` })) })] }, option.id))) }))] }));
105
+ : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id))) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx(RichTextHtml, { html: normalizeRenderableText(option.text), className: "flex-1 text-sm text-gray-700" }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 ${isMultipleAnswers ? 'rounded-md' : 'rounded-full'} ${getIndicatorStyle(option.id)}`, children: showIndicatorMark(option.id) && (_jsx("div", { className: `${isMultipleAnswers ? 'h-2.5 w-2.5 rounded-sm' : 'h-2 w-2 rounded-full'} bg-white` })) })] }, option.id))) }))] }));
105
106
  }
106
107
  // Article Card for ARTICLES groupType (e.g., GN QE RWL Part 2)
107
108
  function ArticleCard({ article, label, }) {
@@ -0,0 +1,13 @@
1
+ import type { WordOrderingGroupPartData } from '../../utils/question-transformers';
2
+ interface MoversWordOrderingGroupRendererProps {
3
+ partNumber: number;
4
+ partName?: string;
5
+ questionCount: number;
6
+ instruction: string;
7
+ data: WordOrderingGroupPartData;
8
+ answers: Record<string, string[] | undefined>;
9
+ onAnswerChange: (questionId: string, value: string[]) => void;
10
+ isReviewMode?: boolean;
11
+ }
12
+ export declare function MoversWordOrderingGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: MoversWordOrderingGroupRendererProps): import("react").JSX.Element;
13
+ export {};
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
4
+ import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
5
+ import { WordOrderingGroupClient } from '../../../../questions/types/word-ordering-group/WordOrderingGroupClient';
6
+ export function MoversWordOrderingGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
7
+ const userAnswers = data.questions.map((question) => {
8
+ if (question.isExample)
9
+ return question.answer;
10
+ return Array.isArray(answers[question.id]) ? answers[question.id] || [] : [];
11
+ });
12
+ return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(WordOrderingGroupClient, { questionData: {
13
+ instruction: data.instruction,
14
+ items: data.questions.map((question) => ({
15
+ words: question.words,
16
+ answer: question.answer,
17
+ isExample: question.isExample,
18
+ points: question.points,
19
+ questionNumber: question.questionNumber,
20
+ })),
21
+ explanation: data.explanation,
22
+ }, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: (next) => {
23
+ next.forEach((value, index) => {
24
+ const target = data.questions[index];
25
+ if (target && !target.isExample) {
26
+ onAnswerChange(target.id, value);
27
+ }
28
+ });
29
+ } }) })] }));
30
+ }
@@ -16,6 +16,7 @@ export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRender
16
16
  export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
17
17
  export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
18
18
  export { MoversWordOrderingRenderer } from './MoversWordOrderingRenderer';
19
+ export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRenderer';
19
20
  export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
20
21
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
21
22
  export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
@@ -20,6 +20,7 @@ export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRender
20
20
  export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
21
21
  export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
22
22
  export { MoversWordOrderingRenderer } from './MoversWordOrderingRenderer';
23
+ export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRenderer';
23
24
  export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
24
25
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
25
26
  export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
@@ -92,7 +92,8 @@ function findParentQuestion(subQuestionId, parts) {
92
92
  part.questionType === 'MATCHING_WITH_LINES_GROUP' ||
93
93
  part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
94
94
  part.questionType === 'SPONTANEOUS_QA_GROUP' ||
95
- part.questionType === 'ANSWER_THE_QUESTION_GROUP') {
95
+ part.questionType === 'ANSWER_THE_QUESTION_GROUP' ||
96
+ part.questionType === 'WORD_ORDERING_GROUP') {
96
97
  const parent = part.questions.find((question) => subQuestionId.startsWith(`${question.id}-item-`));
97
98
  if (parent) {
98
99
  return {
@@ -397,6 +398,12 @@ export function transformAnswersForSubmission(answers, parts) {
397
398
  }
398
399
  return { answers: [] };
399
400
  }
401
+ if (parentType === 'WORD_ORDERING_GROUP') {
402
+ if (Array.isArray(raw)) {
403
+ return raw.filter((token) => typeof token === 'string');
404
+ }
405
+ return [];
406
+ }
400
407
  if (parentType === 'WORD_ORDER_AND_MATCH_GROUP') {
401
408
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
402
409
  const obj = raw;
@@ -5,7 +5,7 @@
5
5
  * Each transformer handles specific question type data structure.
6
6
  */
7
7
  import type { FillBlankQuestion, ChooseBestAnswerQuestion, PictureChooseSubQuestion, PictureFillBlankSubQuestion, ReadingPassageQuestion, LabelThePictureLabel, FillWordHintSubQuestion, ColoringHintItem, ColoringRegion } from '../types';
8
- export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
8
+ export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
9
9
  export interface ApiQuestion {
10
10
  id: string;
11
11
  content: any;
@@ -162,6 +162,7 @@ export declare function transformChooseTheCorrectAnswerGroup(questions: ApiQuest
162
162
  example?: import('../types').ChooseBestAnswerExample;
163
163
  instruction: string;
164
164
  audioUrl?: string;
165
+ imageUrl?: string;
165
166
  };
166
167
  export interface ChooseAdjectiveOption {
167
168
  id: string;
@@ -550,3 +551,17 @@ export interface MatchingWithLinesGroupPartData {
550
551
  explanation?: string;
551
552
  }
552
553
  export declare function transformMatchingWithLinesGroup(questions: ApiQuestion[]): MatchingWithLinesGroupPartData;
554
+ export interface WordOrderingGroupQuestionItem {
555
+ id: string;
556
+ questionNumber: number;
557
+ words: string[];
558
+ answer: string[];
559
+ isExample: boolean;
560
+ points: number;
561
+ }
562
+ export interface WordOrderingGroupPartData {
563
+ questions: WordOrderingGroupQuestionItem[];
564
+ instruction: string;
565
+ explanation?: string;
566
+ }
567
+ export declare function transformWordOrderingGroupTake(questions: ApiQuestion[]): WordOrderingGroupPartData;
@@ -376,6 +376,16 @@ export function transformChooseCorrectAnswer(questions, optionsConfig) {
376
376
  instruction: firstContent?.instruction || 'CHOOSE THE CORRECT ANSWER',
377
377
  };
378
378
  }
379
+ function asChooseAnswerOptionId(value) {
380
+ if (typeof value === 'string') {
381
+ return value.trim();
382
+ }
383
+ if (Array.isArray(value)) {
384
+ const first = value.find((item) => typeof item === 'string' && item.trim() !== '');
385
+ return typeof first === 'string' ? first.trim() : '';
386
+ }
387
+ return '';
388
+ }
379
389
  export function transformChooseTheCorrectAnswerGroup(questions) {
380
390
  const parent = questions[0];
381
391
  const content = parent?.content;
@@ -416,13 +426,20 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
416
426
  return trimmed !== '' ? trimmed : undefined;
417
427
  };
418
428
  const groupAudioUrl = toAudioUrl(content?.meta?.audioUrl);
429
+ const groupImageUrlRaw = toQuestionImageUrl(content?.meta?.imageUrl);
430
+ const groupImageUrl = Array.isArray(groupImageUrlRaw)
431
+ ? groupImageUrlRaw[0]
432
+ : groupImageUrlRaw;
419
433
  let example;
420
434
  if (exampleItem) {
421
435
  const exampleIndex = items.indexOf(exampleItem);
422
436
  const exOptions = mapOptions(exampleItem.content?.options, `${parent.id}-ex`);
423
- const exCorrect = exampleItem.correctAnswer?.answer || parentCorrect[exampleIndex] || exOptions[0]?.id || '';
437
+ const exCorrect = asChooseAnswerOptionId(exampleItem.correctAnswer?.answer)
438
+ || asChooseAnswerOptionId(parentCorrect[exampleIndex])
439
+ || exOptions[0]?.id
440
+ || '';
424
441
  const exampleImageUrl = toQuestionImageUrl(exampleItem.content?.imageUrl);
425
- const exampleAudioUrl = !groupAudioUrl ? toAudioUrl(exampleItem.content?.audioUrl) : undefined;
442
+ const exampleAudioUrl = toAudioUrl(exampleItem.content?.audioUrl);
426
443
  example = {
427
444
  questionNumber: exampleItem.questionNumber || exampleIndex + 1,
428
445
  questionText: exampleItem.content?.question || '',
@@ -436,9 +453,11 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
436
453
  const transformed = regularItems.map((item) => {
437
454
  const itemIndex = items.indexOf(item);
438
455
  const options = mapOptions(item.content?.options, `${parent.id}-item-${itemIndex}`);
439
- const correctId = item.correctAnswer?.answer || parentCorrect[itemIndex] || '';
456
+ const correctId = asChooseAnswerOptionId(item.correctAnswer?.answer)
457
+ || asChooseAnswerOptionId(parentCorrect[itemIndex])
458
+ || '';
440
459
  const itemImageUrl = toQuestionImageUrl(item.content?.imageUrl);
441
- const itemAudioUrl = !groupAudioUrl ? toAudioUrl(item.content?.audioUrl) : undefined;
460
+ const itemAudioUrl = toAudioUrl(item.content?.audioUrl);
442
461
  return {
443
462
  id: `${parent.id}-item-${itemIndex}`,
444
463
  questionNumber: item.questionNumber || itemIndex + 1,
@@ -456,6 +475,7 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
456
475
  example,
457
476
  instruction: content?.meta?.instruction || 'Choose the correct answer.',
458
477
  ...(groupAudioUrl ? { audioUrl: groupAudioUrl } : {}),
478
+ ...(groupImageUrl ? { imageUrl: groupImageUrl } : {}),
459
479
  };
460
480
  }
461
481
  export function transformChooseCorrectAdjective(questions) {
@@ -1392,3 +1412,28 @@ export function transformMatchingWithLinesGroup(questions) {
1392
1412
  explanation: parent?.explanation,
1393
1413
  };
1394
1414
  }
1415
+ export function transformWordOrderingGroupTake(questions) {
1416
+ const parent = questions[0];
1417
+ const content = (parent?.content || {});
1418
+ const items = Array.isArray(content.items) ? content.items : [];
1419
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
1420
+ ? parent.correct_answer.answer
1421
+ : [];
1422
+ return {
1423
+ questions: items.map((item, index) => {
1424
+ const isExample = item.isExample === true;
1425
+ const fromItem = Array.isArray(item.correctAnswer?.answer) ? item.correctAnswer.answer : [];
1426
+ const fromParent = Array.isArray(parentCorrect[index]) ? parentCorrect[index] : [];
1427
+ return {
1428
+ id: `${parent?.id || 'q'}-item-${index}`,
1429
+ questionNumber: item.questionNumber || index + 1,
1430
+ words: Array.isArray(item.content?.words) ? item.content.words : [],
1431
+ answer: fromItem.length ? fromItem : fromParent,
1432
+ isExample,
1433
+ points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
1434
+ };
1435
+ }),
1436
+ instruction: content.meta?.instruction || 'Order the words.',
1437
+ explanation: parent?.explanation,
1438
+ };
1439
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './CueCardEditor';
2
2
  export * from './shared/RichTextEditor';
3
+ export * from './shared/RichTextHtml';
3
4
  export * from './exams';
4
5
  export * from './themes';
5
6
  export * from './results';
@@ -1,5 +1,6 @@
1
1
  export * from './CueCardEditor';
2
2
  export * from './shared/RichTextEditor';
3
+ export * from './shared/RichTextHtml';
3
4
  export * from './exams';
4
5
  export * from './themes';
5
6
  export * from './results';
@@ -347,6 +347,21 @@ export const QUESTION_TYPE_CONFIG = {
347
347
  requiresContent: true,
348
348
  hasDedicatedComponent: true,
349
349
  },
350
+ MATCH_COLUMNS_TO_MAKE_SENTENCES: {
351
+ label: 'Ghép cột thành câu',
352
+ requiresContent: true,
353
+ hasDedicatedComponent: true,
354
+ },
355
+ MATCH_WORD_TO_PICTURE_GROUP: {
356
+ label: 'Nối từ với tranh (nhóm)',
357
+ requiresContent: true,
358
+ hasDedicatedComponent: true,
359
+ },
360
+ WORD_ORDERING_GROUP: {
361
+ label: 'Sắp xếp từ (nhóm)',
362
+ requiresContent: true,
363
+ hasDedicatedComponent: true,
364
+ },
350
365
  };
351
366
  export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
352
367
  'FILL_IN_BLANK',
@@ -398,6 +413,9 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
398
413
  'WORD_FILL_STRUCTURED_FORM',
399
414
  'CROSSWORD_PUZZLE',
400
415
  'FIND_WORDS_IN_MATRIX',
416
+ 'MATCH_COLUMNS_TO_MAKE_SENTENCES',
417
+ 'MATCH_WORD_TO_PICTURE_GROUP',
418
+ 'WORD_ORDERING_GROUP',
401
419
  ];
402
420
  export function getQuestionTypeLabel(type) {
403
421
  return QUESTION_TYPE_CONFIG[type]?.label || type;
@@ -24,6 +24,7 @@ export interface ChooseTheCorrectAnswerGroupData {
24
24
  instruction: string;
25
25
  passage?: string;
26
26
  audioUrl?: string;
27
+ imageUrl?: ChooseTheCorrectAnswerGroupImageUrl;
27
28
  items: ChooseTheCorrectAnswerGroupItemData[];
28
29
  explanation?: string;
29
30
  points?: number;
@@ -15,4 +15,5 @@ export * from './true-false-correct-group.type';
15
15
  export * from './fill-in-blank-group.type';
16
16
  export * from './match-word-to-picture.type';
17
17
  export * from './matching-with-lines-group.type';
18
+ export * from './match-columns-to-make-sentences.type';
18
19
  export * from './spontaneous-qa-group.type';
@@ -15,4 +15,5 @@ export * from './true-false-correct-group.type';
15
15
  export * from './fill-in-blank-group.type';
16
16
  export * from './match-word-to-picture.type';
17
17
  export * from './matching-with-lines-group.type';
18
+ export * from './match-columns-to-make-sentences.type';
18
19
  export * from './spontaneous-qa-group.type';
@@ -0,0 +1,56 @@
1
+ import type { MutableRefObject } from 'react';
2
+ export declare const MATCH_COLUMNS_TO_MAKE_SENTENCE_ITEM_TYPE: "MATCH_COLUMNS_TO_MAKE_SENTENCE";
3
+ export declare const MATCH_COLUMNS_DEFAULT_INSTRUCTION = "Match the information to make sentences. You can use the same words more than once.";
4
+ export type MatchColumnsColumnRole = 'stem' | 'choice';
5
+ export interface MatchColumnsFragment {
6
+ id: string;
7
+ text: string;
8
+ }
9
+ export interface MatchColumnsColumn {
10
+ id: string;
11
+ label?: string;
12
+ role: MatchColumnsColumnRole;
13
+ fragments: MatchColumnsFragment[];
14
+ }
15
+ export interface MatchColumnsItemAnswer {
16
+ fragmentIds: string[];
17
+ sentence: string;
18
+ }
19
+ export interface MatchColumnsStudentItemAnswer {
20
+ fragmentIds: string[];
21
+ sentence: string;
22
+ }
23
+ export interface MatchColumnsGroupItemData {
24
+ stemFragmentId: string;
25
+ fragmentIds: string[];
26
+ sentence: string;
27
+ isExample?: boolean;
28
+ points: number;
29
+ questionNumber: number;
30
+ }
31
+ export interface MatchColumnsGroupData {
32
+ title?: string;
33
+ instruction: string;
34
+ columns: MatchColumnsColumn[];
35
+ items: MatchColumnsGroupItemData[];
36
+ explanation?: string;
37
+ points?: number;
38
+ }
39
+ export interface MatchColumnsGroupCreatorProps {
40
+ initialData?: MatchColumnsGroupData;
41
+ onSave?: (data: MatchColumnsGroupData) => void;
42
+ onCancel?: () => void;
43
+ onChange?: (data: MatchColumnsGroupData) => void;
44
+ externalErrors?: string[];
45
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
46
+ validationRef?: MutableRefObject<{
47
+ triggerValidation: () => Promise<boolean>;
48
+ getFormData?: () => MatchColumnsGroupData;
49
+ } | null>;
50
+ }
51
+ export interface MatchColumnsGroupClientProps {
52
+ questionData: MatchColumnsGroupData;
53
+ isReviewMode?: boolean;
54
+ userAnswers?: MatchColumnsStudentItemAnswer[];
55
+ onAnswerChange?: (answers: MatchColumnsStudentItemAnswer[]) => void;
56
+ }
@@ -0,0 +1,2 @@
1
+ export const MATCH_COLUMNS_TO_MAKE_SENTENCE_ITEM_TYPE = 'MATCH_COLUMNS_TO_MAKE_SENTENCE';
2
+ export const MATCH_COLUMNS_DEFAULT_INSTRUCTION = 'Match the information to make sentences. You can use the same words more than once.';
@@ -0,0 +1,35 @@
1
+ import type { MutableRefObject } from 'react';
2
+ export declare const MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION = "Look and complete.";
3
+ export declare const MATCH_WORD_TO_PICTURE_ITEM_TYPE: "MATCH_WORD_TO_PICTURE";
4
+ export interface MatchWordToPictureGroupItemData {
5
+ imageUrl: string;
6
+ correctAnswer: string;
7
+ isExample?: boolean;
8
+ points: number;
9
+ questionNumber: number;
10
+ }
11
+ export interface MatchWordToPictureGroupData {
12
+ instruction: string;
13
+ wordBank: string[];
14
+ items: MatchWordToPictureGroupItemData[];
15
+ explanation?: string;
16
+ points?: number;
17
+ }
18
+ export interface MatchWordToPictureGroupCreatorProps {
19
+ initialData?: MatchWordToPictureGroupData;
20
+ onSave?: (data: MatchWordToPictureGroupData) => void;
21
+ onCancel?: () => void;
22
+ onChange?: (data: MatchWordToPictureGroupData) => void;
23
+ externalErrors?: string[];
24
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
25
+ validationRef?: MutableRefObject<{
26
+ triggerValidation: () => Promise<boolean>;
27
+ getFormData?: () => MatchWordToPictureGroupData;
28
+ } | null>;
29
+ }
30
+ export interface MatchWordToPictureGroupClientProps {
31
+ questionData: MatchWordToPictureGroupData;
32
+ isReviewMode?: boolean;
33
+ userAnswers?: string[];
34
+ onAnswerChange?: (answers: string[]) => void;
35
+ }
@@ -0,0 +1,2 @@
1
+ export const MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION = 'Look and complete.';
2
+ export const MATCH_WORD_TO_PICTURE_ITEM_TYPE = 'MATCH_WORD_TO_PICTURE';
@@ -0,0 +1,34 @@
1
+ import type { MutableRefObject } from 'react';
2
+ export declare const WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION = "Order the words.";
3
+ export declare const WORD_ORDERING_ITEM_TYPE: "WORD_ORDERING";
4
+ export interface WordOrderingGroupItemData {
5
+ words: string[];
6
+ answer: string[];
7
+ isExample?: boolean;
8
+ points: number;
9
+ questionNumber: number;
10
+ }
11
+ export interface WordOrderingGroupData {
12
+ instruction: string;
13
+ items: WordOrderingGroupItemData[];
14
+ explanation?: string;
15
+ points?: number;
16
+ }
17
+ export interface WordOrderingGroupCreatorProps {
18
+ initialData?: WordOrderingGroupData;
19
+ onSave?: (data: WordOrderingGroupData) => void;
20
+ onCancel?: () => void;
21
+ onChange?: (data: WordOrderingGroupData) => void;
22
+ externalErrors?: string[];
23
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
24
+ validationRef?: MutableRefObject<{
25
+ triggerValidation: () => Promise<boolean>;
26
+ getFormData?: () => WordOrderingGroupData;
27
+ } | null>;
28
+ }
29
+ export interface WordOrderingGroupClientProps {
30
+ questionData: WordOrderingGroupData;
31
+ isReviewMode?: boolean;
32
+ userAnswers?: string[][];
33
+ onAnswerChange?: (answers: string[][]) => void;
34
+ }
@@ -0,0 +1,2 @@
1
+ export const WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION = 'Order the words.';
2
+ export const WORD_ORDERING_ITEM_TYPE = 'WORD_ORDERING';
@@ -68,6 +68,9 @@ const getQuestionTypeName = (type) => {
68
68
  'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu',
69
69
  'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
70
70
  'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
71
+ 'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
72
+ 'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
73
+ 'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',
71
74
  };
72
75
  return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
73
76
  };
@@ -28,6 +28,9 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
28
28
  'MATCH_BY_WRITING_ANSWER',
29
29
  'MATCH_WORD_TO_PICTURE',
30
30
  'MATCHING_WITH_LINES_GROUP',
31
+ 'MATCH_COLUMNS_TO_MAKE_SENTENCES',
32
+ 'MATCH_WORD_TO_PICTURE_GROUP',
33
+ 'WORD_ORDERING_GROUP',
31
34
  'WRITE_SENTENCES',
32
35
  'FILL_IN_BLANK_GROUP',
33
36
  ];
@@ -49,6 +49,9 @@ import { wordFillStructuredFormRegistration } from '../types/word-fill-structure
49
49
  import { wordOrderAndMatchGroupRegistration } from '../types/word-order-and-match-group/register';
50
50
  import { crosswordPuzzleRegistration } from '../types/crossword-puzzle/register';
51
51
  import { findWordsInMatrixRegistration } from '../types/find-words-in-matrix/register';
52
+ import { matchColumnsToMakeSentencesRegistration } from '../types/match-columns-to-make-sentences/register';
53
+ import { matchWordToPictureGroupRegistration } from '../types/match-word-to-picture-group/register';
54
+ import { wordOrderingGroupRegistration } from '../types/word-ordering-group/register';
52
55
  // ─── Registry map ───────────────────────────────────────────────────────────
53
56
  export const questionTypeRegistry = {
54
57
  CHOOSE_THE_CORRECT_ANSWER: chooseTheCorrectAnswerRegistration,
@@ -101,4 +104,7 @@ export const questionTypeRegistry = {
101
104
  WORD_FILL_STRUCTURED_FORM: wordFillStructuredFormRegistration,
102
105
  CROSSWORD_PUZZLE: crosswordPuzzleRegistration,
103
106
  FIND_WORDS_IN_MATRIX: findWordsInMatrixRegistration,
107
+ MATCH_COLUMNS_TO_MAKE_SENTENCES: matchColumnsToMakeSentencesRegistration,
108
+ MATCH_WORD_TO_PICTURE_GROUP: matchWordToPictureGroupRegistration,
109
+ WORD_ORDERING_GROUP: wordOrderingGroupRegistration,
104
110
  };