@tinyweb_dev/oe-exam-sdk 0.2.9 → 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 (91) 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/MoversWordOrderingGroupRenderer.d.ts +13 -0
  4. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
  5. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  6. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  7. package/dist/components/exams/take/utils/answer-transformers.js +8 -1
  8. package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
  9. package/dist/components/exams/take/utils/question-transformers.js +47 -2
  10. package/dist/components/questions/_shared/config/question-types.config.js +18 -0
  11. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
  12. package/dist/components/questions/_shared/types/index.d.ts +1 -0
  13. package/dist/components/questions/_shared/types/index.js +1 -0
  14. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
  15. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
  16. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
  17. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
  18. package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
  19. package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
  20. package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
  21. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  22. package/dist/components/questions/creator/question-type-registry.js +6 -0
  23. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +1 -1
  24. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +5 -3
  25. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
  26. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  27. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
  28. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
  29. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
  30. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
  31. package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
  32. package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
  33. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
  34. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
  35. package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
  36. package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
  37. package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
  38. package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
  39. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
  40. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
  41. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
  42. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
  43. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
  44. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
  45. package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
  46. package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
  47. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
  48. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
  49. package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
  50. package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
  51. package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
  52. package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
  53. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
  54. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
  55. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
  56. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +106 -0
  57. package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
  58. package/dist/components/questions/types/word-ordering-group/index.js +5 -0
  59. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
  60. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
  61. package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
  62. package/dist/components/questions/types/word-ordering-group/register.js +36 -0
  63. package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
  64. package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
  65. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
  66. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
  67. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
  68. package/dist/components/questions/viewer/QuestionViewer.js +6 -0
  69. package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
  70. package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
  71. package/dist/components/questions/viewer/index.d.ts +2 -0
  72. package/dist/components/questions/viewer/index.js +2 -0
  73. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  74. package/dist/components/results/renderers/review-question-body-movers.js +6 -2
  75. package/dist/components/results/review-data.js +13 -0
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/shared/constants/question-skills.js +6 -0
  79. package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
  80. package/dist/shared/lib/utils/question-transform.js +6 -0
  81. package/dist/shared/types/common.types.d.ts +1 -1
  82. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  83. package/dist/shared/types/questions/index.d.ts +3 -0
  84. package/dist/shared/types/questions/index.js +6 -0
  85. package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
  86. package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
  87. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
  88. package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
  89. package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
  90. package/dist/shared/types/questions/word-ordering-group.js +1 -0
  91. 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 }));
@@ -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,11 +426,18 @@ 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
442
  const exampleAudioUrl = toAudioUrl(exampleItem.content?.audioUrl);
426
443
  example = {
@@ -436,7 +453,9 @@ 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
460
  const itemAudioUrl = toAudioUrl(item.content?.audioUrl);
442
461
  return {
@@ -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
+ }
@@ -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
  };
@@ -34,7 +34,7 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
34
34
  next[itemIndex] = optionId;
35
35
  onAnswerChange(next);
36
36
  };
37
- return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && _jsx(ClientAudio, { url: questionData.audioUrl }), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
37
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && _jsx(ClientAudio, { url: questionData.audioUrl }), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
38
38
  const selectedId = userAnswers[itemIndex];
39
39
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
40
40
  return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), item.audioUrl && _jsx(ClientAudio, { url: item.audioUrl }), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), item.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: item.options.map((option, optionIndex) => {
@@ -70,6 +70,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
70
70
  const [instruction, setInstruction] = useState(initialData?.instruction || 'Choose the correct answer.');
71
71
  const [passage, setPassage] = useState(initialData?.passage || '');
72
72
  const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || '');
73
+ const [imageUrl, setImageUrl] = useState(toChooseAnswerGroupImageUrls(initialData?.imageUrl)[0] || '');
73
74
  const [items, setItems] = useState(() => normalizeItems(initialData?.items));
74
75
  const [explanation, setExplanation] = useState(initialData?.explanation || '');
75
76
  const [isClientMode, setIsClientMode] = useState(false);
@@ -80,10 +81,11 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
80
81
  onChangeRef.current = onChange;
81
82
  }, [onChange]);
82
83
  const isContentEmpty = (html) => !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
83
- const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrl = audioUrl) => ({
84
+ const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrl = audioUrl, nextImageUrl = imageUrl) => ({
84
85
  instruction: nextInstruction,
85
86
  ...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
86
87
  ...(nextAudioUrl.trim() ? { audioUrl: nextAudioUrl.trim() } : {}),
88
+ ...(nextImageUrl.trim() ? { imageUrl: nextImageUrl.trim() } : {}),
87
89
  items: nextItems,
88
90
  explanation: nextExplanation,
89
91
  points: sumPoints(nextItems),
@@ -100,7 +102,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
100
102
  previousPayloadRef.current = serialized;
101
103
  onUnsavedChangesChange?.(true);
102
104
  debouncedOnChange(payload);
103
- }, [instruction, passage, audioUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
105
+ }, [instruction, passage, audioUrl, imageUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
104
106
  const validate = () => {
105
107
  const nextErrors = [];
106
108
  if (!instruction.trim()) {
@@ -165,7 +167,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
165
167
  return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Ch\u1EC9nh s\u1EEDa"] }) }), _jsx(ChooseTheCorrectAnswerGroupClient, { questionData: buildPayload(), isReviewMode: true })] }));
166
168
  }
167
169
  const allErrors = [...errors, ...(externalErrors || [])];
168
- return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer." })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-audio", label: "Audio chung (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage - Tu\u1EF3 ch\u1ECDn)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
170
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer." })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-audio", label: "Audio chung (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: "choose-answer-group-image", label: "H\u00ECnh \u1EA3nh chung (tu\u1EF3 ch\u1ECDn)", accept: "image/*", value: imageUrl, onChange: setImageUrl, maxSize: 5, placeholder: "Upload \u1EA3nh nh\u00F3m ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage - Tu\u1EF3 ch\u1ECDn)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
169
171
  isExample: checked,
170
172
  points: checked ? 0 : (item.points || 1),
171
173
  }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "A place for someone to stay or live is called" })] }), _jsx("div", { className: "space-y-2", children: _jsx(FileUpload, { id: `choose-answer-item-audio-${itemIndex}`, label: "Audio c\u00E2u h\u1ECFi (tu\u1EF3 ch\u1ECDn)", accept: "audio/*", value: item.audioUrl || '', onChange: (url) => updateItem(itemIndex, { audioUrl: url }), maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "flex items-center gap-2", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-teal-500" }), "H\u00ECnh \u1EA3nh (tu\u1EF3 ch\u1ECDn)"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {