@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.11

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 (124) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +7 -7
  3. package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
  4. package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
  5. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
  6. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.d.ts +14 -0
  7. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.js +36 -0
  8. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +4 -0
  9. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  10. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  11. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
  12. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
  13. package/dist/components/exams/take/utils/answer-transformers.js +17 -1
  14. package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
  15. package/dist/components/exams/take/utils/question-transformers.d.ts +32 -3
  16. package/dist/components/exams/take/utils/question-transformers.js +51 -2
  17. package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
  18. package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
  19. package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
  20. package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
  21. package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
  22. package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
  23. package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
  24. package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
  25. package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
  26. package/dist/components/questions/_shared/components/compact/index.js +4 -0
  27. package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
  28. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +1 -0
  29. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +14 -0
  30. package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +2 -2
  31. package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
  32. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -0
  33. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
  34. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
  35. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
  36. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
  37. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
  38. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
  39. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
  40. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
  41. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
  42. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
  43. package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
  44. package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
  45. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
  46. package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
  47. package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
  48. package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
  49. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
  50. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
  51. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
  52. package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
  53. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
  54. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
  55. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
  56. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
  57. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
  58. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
  59. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
  60. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
  61. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
  62. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
  63. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
  64. package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
  65. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
  66. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
  67. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
  68. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
  69. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
  70. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
  71. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
  72. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
  73. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
  74. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
  75. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
  76. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
  77. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
  78. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +24 -35
  79. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +16 -3
  80. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +2 -1
  81. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +25 -6
  82. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +1 -1
  83. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +22 -10
  84. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +60 -18
  85. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +2 -1
  86. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +24 -5
  87. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +8 -4
  88. package/dist/components/questions/types/matching-with-lines-group/transform.js +5 -4
  89. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
  90. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
  91. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
  92. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
  93. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
  94. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  95. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
  96. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
  97. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
  98. package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
  99. package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
  100. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
  101. package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
  102. package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
  103. package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
  104. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +21 -23
  105. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
  106. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
  107. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
  108. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
  109. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
  110. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
  111. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
  112. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
  113. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
  114. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
  115. package/dist/components/questions/viewer/QuestionViewer.js +2 -2
  116. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
  117. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
  118. package/dist/components/results/renderers/review-question-body-movers.js +7 -3
  119. package/dist/components/results/review-data.js +20 -1
  120. package/dist/shared/lib/utils/question-reverse-transform.js +48 -7
  121. package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
  122. package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
  123. package/dist/shared/types/questions/matching-with-lines-group.d.ts +2 -2
  124. package/package.json +1 -1
@@ -632,6 +632,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
632
632
  map.set(`${question.id}-item-${index}`, question.id);
633
633
  });
634
634
  }
635
+ if (part.questionType === 'MATCH_WORD_TO_PICTURE_GROUP' &&
636
+ Array.isArray(content?.items)) {
637
+ content.items.forEach((_, index) => {
638
+ map.set(`${question.id}-item-${index}`, question.id);
639
+ });
640
+ }
635
641
  if (part.questionType === 'CHOOSE_THEN_ANSWER_GROUP' &&
636
642
  Array.isArray(content?.items)) {
637
643
  content.items.forEach((_, index) => {
@@ -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, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, SortWordsIntoCategoriesRenderer, ChooseThenAnswerGroupRenderer, } from './question-renderers';
11
+ import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversMatchWordToPictureGroupRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, SortWordsIntoCategoriesRenderer, ChooseThenAnswerGroupRenderer, } from './question-renderers';
12
12
  import { SpeakingQuestionRenderer } from './speaking/renderers';
13
- import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformReadingPassage, transformLabelPicture, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, transformSortWordsIntoCategories, transformChooseThenAnswerGroup, } from '../utils/question-transformers';
13
+ import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformReadingPassage, transformLabelPicture, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformMatchWordToPictureGroup, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, transformSortWordsIntoCategories, transformChooseThenAnswerGroup, } 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, compactLayout = false, }) {
@@ -102,7 +102,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
102
102
  }
103
103
  case 'CROSSWORD_PUZZLE': {
104
104
  const data = transformCrosswordPuzzle(questions);
105
- return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
105
+ return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, markedColumn: data.markedColumn, hiddenWord: data.hiddenWord, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, caseSensitive: data.caseSensitive }));
106
106
  }
107
107
  case 'IMAGE_OBJECT_MATCHING': {
108
108
  // Reuse coloring renderer — same content structure as READ_AND_COLOR_OBJECTS
@@ -125,10 +125,6 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
125
125
  onAnswerChange(regionId, hintItemId);
126
126
  }, isReviewMode: isReviewMode }));
127
127
  }
128
- case 'CROSSWORD_PUZZLE': {
129
- const data = transformCrosswordPuzzle(questions);
130
- return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
131
- }
132
128
  case 'SORT_WORDS_INTO_CATEGORIES': {
133
129
  const data = transformSortWordsIntoCategories(questions);
134
130
  const gradableCount = data.words.filter((word) => !word.isExample).length;
@@ -204,6 +200,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
204
200
  const data = transformMatchByWritingAnswer(questions);
205
201
  return (_jsx(MoversMatchByWritingAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: part.audioUrl, options: data.options, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, viewMode: data.viewMode, groupImageUrl: data.groupImageUrl }));
206
202
  }
203
+ case 'MATCH_WORD_TO_PICTURE_GROUP': {
204
+ const data = transformMatchWordToPictureGroup(questions);
205
+ return (_jsx(MoversMatchWordToPictureGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: data.questions.filter((q) => !q.isExample).length, instruction: part.instructions || data.instruction, audioUrl: part.audioUrl, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
206
+ }
207
207
  case 'MATCH_WORD_TO_PICTURE': {
208
208
  const data = transformMatchWordToPicture(questions);
209
209
  return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, wordBank: data.wordBank, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
@@ -8,9 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  import { useMemo } from 'react';
9
9
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
10
10
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
11
- function tokenize(sentence) {
12
- return sentence.trim().split(/\s+/).filter(Boolean);
13
- }
11
+ import { tokenizeCrossOutQuestion } from '../../../../questions/types/cross-out-word-group/tokenize';
14
12
  function normalizeWord(word) {
15
13
  return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
16
14
  }
@@ -27,12 +25,20 @@ function formatAnswerLabel(answer) {
27
25
  return 'câu đúng (không gạch)';
28
26
  return `gạch "${answer.crossOut}"${posInfo}`;
29
27
  }
28
+ function isCorrectToken(correct, tokenIndex, plain) {
29
+ if (!correct)
30
+ return false;
31
+ if (typeof correct.wordIndex === 'number' && correct.wordIndex >= 0) {
32
+ return correct.wordIndex === tokenIndex;
33
+ }
34
+ return Boolean(correct.crossOut) && normalizeWord(correct.crossOut) === plain && plain !== '';
35
+ }
30
36
  function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswer, id, }) {
31
37
  const isDisabled = isReviewMode || question.isExample;
32
38
  const currentAnswer = question.isExample
33
39
  ? question.exampleCorrectAnswer
34
40
  : answer;
35
- const tokens = useMemo(() => tokenize(question.question), [question.question]);
41
+ const tokens = useMemo(() => tokenizeCrossOutQuestion(question.question), [question.question]);
36
42
  const selectedWord = currentAnswer?.crossOut || '';
37
43
  const selectedIndex = currentAnswer?.wordIndex;
38
44
  const replaceWith = currentAnswer?.replaceWith || '';
@@ -76,11 +82,10 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
76
82
  });
77
83
  }
78
84
  else {
79
- const cleanWord = rawToken.replace(/[^\p{L}\p{N}'-]/gu, '') || rawToken;
80
85
  onAnswerChange(question.id, {
81
86
  type: mode,
82
87
  wordIndex: tokenIndex,
83
- crossOut: cleanWord,
88
+ crossOut: rawToken,
84
89
  ...(mode === 'CROSS_OUT_AND_REPLACE' ? { replaceWith } : {}),
85
90
  });
86
91
  }
@@ -109,9 +114,9 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
109
114
  ? 'bg-emerald-500'
110
115
  : isWrong
111
116
  ? 'bg-rose-500'
112
- : 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full border border-amber-300 bg-amber-100/90 px-2 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800", children: "EXAMPLE" })), _jsx("span", { className: `rounded-full px-2.5 py-0.5 text-[11px] font-bold ${mode === 'CROSS_OUT_AND_REPLACE'
117
+ : 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full border border-amber-300 bg-amber-100/90 px-2 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800", children: "EXAMPLE" })), isReviewMode ? (_jsx("span", { className: `rounded-full px-2.5 py-0.5 text-[11px] font-bold ${mode === 'CROSS_OUT_AND_REPLACE'
113
118
  ? 'bg-violet-100/80 text-violet-700 border border-violet-200/60'
114
- : 'bg-sky-100/80 text-sky-700 border border-sky-200/60'}`, children: mode === 'CROSS_OUT_AND_REPLACE' ? 'Gạch + Thay thế' : 'Chỉ gạch' }), !question.isExample && selectedWord && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10.5px] font-bold text-emerald-700", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }), "\u0110\u00E3 g\u1EA1ch: \"", selectedWord, "\"", typeof selectedIndex === 'number' && (_jsxs("span", { className: "text-[10px] text-emerald-600 font-normal", children: ["(t\u1EEB #", selectedIndex + 1, ")"] }))] }))] }), mode === 'CROSS_OUT_AND_REPLACE' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-bold text-slate-600 whitespace-nowrap", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: isDisabled, onChange: (e) => setReplaceWith(e.target.value), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 w-40 sm:w-52 rounded-lg border border-slate-200 bg-white px-2.5 text-sm font-semibold text-slate-800 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-slate-50" })] }))] }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2 text-[15.5px] sm:text-[16.5px] font-medium leading-loose text-slate-800 px-1", children: tokens.map((token, tokenIndex) => {
119
+ : 'bg-sky-100/80 text-sky-700 border border-sky-200/60'}`, children: mode === 'CROSS_OUT_AND_REPLACE' ? 'Gạch + Thay thế' : 'Chỉ gạch' })) : null, !question.isExample && selectedWord && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10.5px] font-bold text-emerald-700", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }), "\u0110\u00E3 g\u1EA1ch: \"", selectedWord, "\"", typeof selectedIndex === 'number' && (_jsxs("span", { className: "text-[10px] text-emerald-600 font-normal", children: ["(t\u1EEB #", selectedIndex + 1, ")"] }))] }))] }), mode === 'CROSS_OUT_AND_REPLACE' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-bold text-slate-600 whitespace-nowrap", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: isDisabled, onChange: (e) => setReplaceWith(e.target.value), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 w-40 sm:w-52 rounded-lg border border-slate-200 bg-white px-2.5 text-sm font-semibold text-slate-800 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-slate-50" })] }))] }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2 text-[15.5px] sm:text-[16.5px] font-medium leading-loose text-slate-800 px-1", children: tokens.map((token, tokenIndex) => {
115
120
  const plain = normalizeWord(token);
116
121
  const isSelected = selectedIndex !== undefined && selectedIndex !== null
117
122
  ? selectedIndex === tokenIndex
@@ -119,12 +124,21 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
119
124
  const isCorrectTargetInReview = isReviewMode &&
120
125
  !question.isExample &&
121
126
  correctAnswer?.wordIndex === tokenIndex;
127
+ const isSelectedCorrect = isReviewMode &&
128
+ !question.isExample &&
129
+ isSelected &&
130
+ isCorrectToken(correctAnswer, tokenIndex, plain);
131
+ const isSelectedWrong = isReviewMode &&
132
+ !question.isExample &&
133
+ isSelected &&
134
+ !!correctAnswer &&
135
+ !isSelectedCorrect;
122
136
  return (_jsx("button", { type: "button", disabled: isDisabled || plain === '', onClick: () => setCrossOut(tokenIndex, token), className: `rounded-lg px-2 py-1 transition-all duration-150 select-none relative ${isSelected
123
137
  ? question.isExample
124
138
  ? 'bg-amber-200/90 text-amber-900 font-bold line-through decoration-amber-700 decoration-2 shadow-sm'
125
- : isCorrect
139
+ : isSelectedCorrect
126
140
  ? 'bg-emerald-100 text-emerald-800 font-bold line-through decoration-emerald-600 decoration-2 shadow-sm border border-emerald-300'
127
- : isWrong
141
+ : isSelectedWrong
128
142
  ? 'bg-rose-100 text-rose-700 font-bold line-through decoration-rose-600 decoration-2 shadow-sm border border-rose-300 scale-[0.98]'
129
143
  : 'bg-blue-100 text-blue-700 font-bold line-through decoration-blue-600 decoration-2 shadow-sm border border-blue-200 scale-[0.98]'
130
144
  : isCorrectTargetInReview
@@ -307,5 +307,5 @@ export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCo
307
307
  inputRefs.current.delete(key);
308
308
  }, type: "text", maxLength: 1, disabled: Boolean(isExample), value: cellValue, onChange: (e) => handleInputChange(e.target.value, r, c), onKeyDown: (e) => handleKeyDown(e, r, c), onFocus: () => handleCellClick(r, c), className: `w-full h-full text-center bg-transparent uppercase font-bold outline-none ${isExample ? 'cursor-default' : 'cursor-pointer'}` })) : (_jsxs("div", { className: "flex flex-col items-center justify-center", children: [_jsx("span", { children: cellValue || '—' }), reviewStatus && !reviewStatus.isCorrect && (_jsx("span", { className: "absolute -bottom-1 -right-1 text-[9px] bg-green-600 text-white font-bold px-1 rounded-full shadow", children: reviewStatus.expected }))] })), isReviewMode && reviewStatus && (_jsx("span", { className: `absolute -top-1 -right-1 rounded-full p-0.5 text-white ${reviewStatus.isCorrect ? 'bg-green-500' : 'bg-red-500'}`, children: reviewStatus.isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5" })) : (_jsx(X, { className: "h-2.5 w-2.5" })) }))] }, key));
309
309
  });
310
- }) }) })] })] })] }));
310
+ }) }) }), (markedColumn || hiddenWord) && (_jsxs("div", { className: "w-full max-w-md rounded-xl border-2 border-amber-200 bg-amber-50 p-4", children: [_jsx("label", { className: "block text-sm font-bold text-amber-900 mb-2", children: "What's the hidden word?" }), !isReviewMode ? (_jsx("input", { type: "text", value: answers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded-lg border border-amber-300 bg-white px-3 py-2 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [answers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: answers['hidden-word'] })), hiddenWord && (_jsxs("div", { className: "mt-1 text-xs", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: hiddenWord })] }))] }))] }))] })] })] }));
311
311
  }
@@ -8,6 +8,7 @@ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
8
8
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
9
9
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
10
10
  import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
11
+ import { FillInBlankAnswerKey } from '../../../../questions/types/fill-in-blank-group/FillInBlankAnswerKey';
11
12
  import { extractPlaceholderIndices, isBlankMatch, padStudentAnswers, splitStem, toFillInBlankImageUrls, } from '../../../../questions/types/fill-in-blank-group/blank-utils';
12
13
  function emptyAnswer(blankCount) {
13
14
  return { answers: Array.from({ length: blankCount }, () => '') };
@@ -53,7 +54,7 @@ function ItemCard({ question, answer, onAnswerChange, isReviewMode, correctAnswe
53
54
  : blankWrong
54
55
  ? 'border-rose-400 bg-rose-50 text-rose-800'
55
56
  : 'border-blue-200 bg-blue-50/40 text-blue-900 focus:border-blue-500 focus:bg-white'}` }, partIndex));
56
- }) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsxs("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-sm text-emerald-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", expected.map((alts) => alts.join(' / ') || '(trống)').join(' | ')] }))] }));
57
+ }) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsx("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2", children: _jsx(FillInBlankAnswerKey, { answersByBlank: expected, blankIndices: blankIndices }) }))] }));
57
58
  }
58
59
  export function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints = false, hints = [], questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap, }) {
59
60
  return (_jsxs("div", { className: "relative flex h-full flex-col gap-4 overflow-y-auto custom-scrollbar sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 p-3 sm:gap-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), (audioUrl || toFillInBlankImageUrls(imageUrl).length > 0) && (_jsxs("div", { className: "sticky top-0 z-20 flex flex-col gap-3 border-b border-gray-100 bg-white/95 p-3 pb-2 shadow-sm backdrop-blur sm:px-6", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), toFillInBlankImageUrls(imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(imageUrl).map((url, imageIndex) => (_jsx(ResolvedImage, { src: url, alt: `Shared illustration ${imageIndex + 1}`, className: "max-h-[180px] rounded-lg object-contain" }, `group-image-${imageIndex}`))) }))] })), showHints && hints.length > 0 && !isReviewMode && (_jsx("div", { className: "flex flex-wrap gap-2 px-3 sm:px-6", children: hints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:px-6 lg:grid-cols-3", children: questions.map((question) => (_jsx(ItemCard, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap?.[question.id] }, question.id))) })] }));
@@ -0,0 +1,14 @@
1
+ import type { MatchWordToPictureGroupPartData } from '../../utils/question-transformers';
2
+ interface MoversMatchWordToPictureGroupRendererProps {
3
+ partNumber: number;
4
+ partName?: string;
5
+ questionCount: number;
6
+ instruction: string;
7
+ audioUrl?: string;
8
+ data: MatchWordToPictureGroupPartData;
9
+ answers: Record<string, string | undefined>;
10
+ onAnswerChange: (questionId: string, value: string) => void;
11
+ isReviewMode?: boolean;
12
+ }
13
+ export declare function MoversMatchWordToPictureGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, data, answers, onAnswerChange, isReviewMode, }: MoversMatchWordToPictureGroupRendererProps): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,36 @@
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 { MatchWordToPictureGroupClient } from '../../../../questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient';
6
+ export function MoversMatchWordToPictureGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, data, answers, onAnswerChange, isReviewMode = false, }) {
7
+ const userAnswers = data.questions.map((question) => {
8
+ if (question.isExample)
9
+ return question.correctAnswer;
10
+ return typeof answers[question.id] === 'string' ? answers[question.id] || '' : '';
11
+ });
12
+ const resolvedAudio = data.audioUrl || audioUrl || '';
13
+ 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(MatchWordToPictureGroupClient, { questionData: {
14
+ title: data.title,
15
+ instruction: data.instruction,
16
+ ...(data.passage ? { passage: data.passage } : {}),
17
+ ...(resolvedAudio ? { audioUrl: resolvedAudio } : {}),
18
+ ...(data.imageUrl ? { imageUrl: data.imageUrl } : {}),
19
+ wordBank: data.wordBank,
20
+ items: data.questions.map((question) => ({
21
+ imageUrl: question.imageUrl,
22
+ correctAnswer: question.correctAnswer,
23
+ isExample: question.isExample,
24
+ points: question.points,
25
+ questionNumber: question.questionNumber,
26
+ })),
27
+ explanation: data.explanation,
28
+ }, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: (next) => {
29
+ next.forEach((value, index) => {
30
+ const target = data.questions[index];
31
+ if (target && !target.isExample) {
32
+ onAnswerChange(target.id, value);
33
+ }
34
+ });
35
+ } }) })] }));
36
+ }
@@ -13,9 +13,13 @@ export function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, que
13
13
  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(MatchingWithLinesGroupClient, { questionData: {
14
14
  title: data.title,
15
15
  instruction: data.instruction,
16
+ ...(data.passage ? { passage: data.passage } : {}),
17
+ ...(data.audioUrl ? { audioUrl: data.audioUrl } : {}),
18
+ ...(data.imageUrl ? { imageUrl: data.imageUrl } : {}),
16
19
  rightItems: data.rightItems,
17
20
  items: data.questions.map((question) => ({
18
21
  imageUrl: question.imageUrl,
22
+ text: question.text,
19
23
  correctAnswer: question.correctRightId,
20
24
  isExample: question.isExample,
21
25
  points: question.points,
@@ -17,6 +17,7 @@ export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRender
17
17
  export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
18
18
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
19
19
  export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
20
+ export { MoversMatchWordToPictureGroupRenderer } from './MoversMatchWordToPictureGroupRenderer';
20
21
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
21
22
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
22
23
  export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
@@ -21,6 +21,7 @@ export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRender
21
21
  export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
22
22
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
23
23
  export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
24
+ export { MoversMatchWordToPictureGroupRenderer } from './MoversMatchWordToPictureGroupRenderer';
24
25
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
25
26
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
26
27
  export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
@@ -37,14 +37,24 @@ export function SpeakingSpontaneousQaGroupRenderer(props) {
37
37
  useEffect(() => {
38
38
  setSequenceStep(getInitialSequenceStep());
39
39
  }, [getInitialSequenceStep, setup.nav.currentIndex]);
40
+ const hasQuestionAudio = Boolean(content?.audioUrl?.trim());
40
41
  useEffect(() => {
41
42
  if (sequenceStep !== SPONTANEOUS_QA_SEQUENCE_STEP.DELAYING || shouldResumeCompletedState)
42
43
  return;
43
44
  const timer = setTimeout(() => {
44
- setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION);
45
+ setSequenceStep(hasQuestionAudio
46
+ ? SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION
47
+ : SPONTANEOUS_QA_SEQUENCE_STEP.ALLOW_RECORDING);
45
48
  }, 3000);
46
49
  return () => clearTimeout(timer);
47
- }, [sequenceStep, shouldResumeCompletedState]);
50
+ }, [hasQuestionAudio, sequenceStep, shouldResumeCompletedState]);
51
+ useEffect(() => {
52
+ if (sequenceStep === SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION &&
53
+ !hasQuestionAudio &&
54
+ !shouldResumeCompletedState) {
55
+ setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.ALLOW_RECORDING);
56
+ }
57
+ }, [hasQuestionAudio, sequenceStep, shouldResumeCompletedState]);
48
58
  const handleTeacherVideoEnded = useCallback(() => {
49
59
  setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.DELAYING);
50
60
  props.onTeacherIntroPlayed?.();
@@ -27,8 +27,9 @@ export function useSpontaneousQaGroupRendererSetup({ part, answers, onAnswerChan
27
27
  flattenIndex: child.flattenIndex,
28
28
  expectedAnswers: child.expectedAnswers,
29
29
  points: child.points,
30
+ isExample: child.isExample,
30
31
  },
31
- is_example: false,
32
+ is_example: child.isExample,
32
33
  correct_answer: { expectedAnswers: child.expectedAnswers },
33
34
  total_points: String(child.points),
34
35
  question_type: 'SPONTANEOUS_QA_GROUP',
@@ -90,6 +90,7 @@ function findParentQuestion(subQuestionId, parts) {
90
90
  part.questionType === 'TRUE_FALSE_CORRECT_GROUP' ||
91
91
  part.questionType === 'FILL_IN_BLANK_GROUP' ||
92
92
  part.questionType === 'MATCHING_WITH_LINES_GROUP' ||
93
+ part.questionType === 'MATCH_WORD_TO_PICTURE_GROUP' ||
93
94
  part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
94
95
  part.questionType === 'SPONTANEOUS_QA_GROUP' ||
95
96
  part.questionType === 'ANSWER_THE_QUESTION_GROUP' ||
@@ -263,6 +264,16 @@ export function transformAnswersForSubmission(answers, parts) {
263
264
  if (speakingQuestionIds.has(questionId) && typeof answer === 'string') {
264
265
  continue;
265
266
  }
267
+ if (questionId === 'hidden-word') {
268
+ const parent = findGridParentQuestion(parts);
269
+ if (parent?.questionType === 'CROSSWORD_PUZZLE') {
270
+ if (!gridCellAnswers.has(parent.parentId)) {
271
+ gridCellAnswers.set(parent.parentId, {});
272
+ }
273
+ gridCellAnswers.get(parent.parentId)['hidden-word'] = String(answer);
274
+ }
275
+ continue;
276
+ }
266
277
  // Handle grid cell keys (from FILL_MISSING_WORDS_IN_GRID)
267
278
  if (isGridCellKey(questionId)) {
268
279
  const parent = findGridParentQuestion(parts);
@@ -454,9 +465,14 @@ export function transformAnswersForSubmission(answers, parts) {
454
465
  words[`down-${c.number}`] = word;
455
466
  }
456
467
  }
468
+ const hiddenWord = cellAnswers['hidden-word'];
469
+ const cells = Object.fromEntries(Object.entries(cellAnswers).filter(([key]) => key !== 'hidden-word'));
470
+ if (hiddenWord) {
471
+ words['hidden-word'] = hiddenWord;
472
+ }
457
473
  result.push({
458
474
  questionId: parentId,
459
- answer: { answers: words, cells: cellAnswers },
475
+ answer: { answers: words, cells },
460
476
  });
461
477
  }
462
478
  else {
@@ -63,21 +63,19 @@ export function normalizeCrossOutWordGroupAnswer(raw) {
63
63
  const rawIndex = obj.wordIndex;
64
64
  const wordIndex = typeof rawIndex === 'number' && Number.isInteger(rawIndex) && rawIndex >= 0
65
65
  ? rawIndex
66
- : rawIndex === null
67
- ? null
68
- : undefined;
66
+ : null;
69
67
  return type === 'CROSS_OUT_AND_REPLACE'
70
68
  ? {
71
69
  type,
72
- ...(wordIndex !== undefined ? { wordIndex } : {}),
70
+ wordIndex,
73
71
  crossOut: typeof obj.crossOut === 'string' ? obj.crossOut : '',
74
72
  replaceWith: typeof obj.replaceWith === 'string' ? obj.replaceWith : '',
75
73
  }
76
74
  : {
77
75
  type: 'CROSS_OUT',
78
- ...(wordIndex !== undefined ? { wordIndex } : {}),
76
+ wordIndex,
79
77
  crossOut: typeof obj.crossOut === 'string' ? obj.crossOut : '',
80
78
  };
81
79
  }
82
- return { type: 'CROSS_OUT', crossOut: '' };
80
+ return { type: 'CROSS_OUT', wordIndex: null, crossOut: '' };
83
81
  }
@@ -5,7 +5,7 @@
5
5
  * Each transformer handles specific question type data structure.
6
6
  */
7
7
  import type { FillBlankQuestion, ChooseBestAnswerQuestion, ReadingPassageQuestion, LabelThePictureLabel, 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' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | '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' | 'SORT_WORDS_INTO_CATEGORIES' | 'CHOOSE_THEN_ANSWER_GROUP' | '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' | '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' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | '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' | 'MATCH_WORD_TO_PICTURE_GROUP' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'SORT_WORDS_INTO_CATEGORIES' | 'CHOOSE_THEN_ANSWER_GROUP' | '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' | 'SPEAKING_CUE_CARD';
9
9
  export interface ApiQuestion {
10
10
  id: string;
11
11
  content: any;
@@ -372,6 +372,25 @@ export declare function transformMatchWordToPicture(questions: ApiQuestion[]): {
372
372
  wordBank: string[];
373
373
  instruction: string;
374
374
  };
375
+ export interface MatchWordToPictureGroupQuestionItem {
376
+ id: string;
377
+ questionNumber: number;
378
+ imageUrl?: string | string[];
379
+ correctAnswer: string;
380
+ isExample: boolean;
381
+ points: number;
382
+ }
383
+ export interface MatchWordToPictureGroupPartData {
384
+ questions: MatchWordToPictureGroupQuestionItem[];
385
+ wordBank: string[];
386
+ title?: string;
387
+ instruction: string;
388
+ passage?: string;
389
+ audioUrl?: string;
390
+ imageUrl?: string | string[];
391
+ explanation?: string;
392
+ }
393
+ export declare function transformMatchWordToPictureGroup(questions: ApiQuestion[]): MatchWordToPictureGroupPartData;
375
394
  export declare function transformWriteSentences(questions: ApiQuestion[]): {
376
395
  questions: Array<{
377
396
  id: string;
@@ -472,14 +491,19 @@ export interface CrosswordClueItem {
472
491
  startRow: number;
473
492
  startCol: number;
474
493
  length: number;
494
+ isExample?: boolean;
475
495
  }
476
496
  export interface CrosswordPuzzlePartData {
497
+ type?: 'STANDARD' | 'HIDDEN_WORD';
477
498
  title?: string;
478
499
  instruction: string;
479
500
  gridSize: {
480
501
  rows: number;
481
502
  cols: number;
482
503
  };
504
+ markedColumn?: number;
505
+ hiddenWord?: string;
506
+ caseSensitive?: boolean;
483
507
  clues: {
484
508
  across: CrosswordClueItem[];
485
509
  down: CrosswordClueItem[];
@@ -569,7 +593,8 @@ export declare function transformChooseThenAnswerGroup(questions: ApiQuestion[])
569
593
  export interface MatchingWithLinesGroupQuestionItem {
570
594
  id: string;
571
595
  questionNumber: number;
572
- imageUrl: string;
596
+ imageUrl?: string;
597
+ text?: string;
573
598
  correctRightId: string;
574
599
  isExample: boolean;
575
600
  points: number;
@@ -578,10 +603,14 @@ export interface MatchingWithLinesGroupPartData {
578
603
  questions: MatchingWithLinesGroupQuestionItem[];
579
604
  rightItems: Array<{
580
605
  id: string;
581
- text: string;
606
+ text?: string;
607
+ imageUrl?: string;
582
608
  }>;
583
609
  title?: string;
584
610
  instruction: string;
611
+ passage?: string;
612
+ audioUrl?: string | string[];
613
+ imageUrl?: string | string[];
585
614
  explanation?: string;
586
615
  }
587
616
  export declare function transformMatchingWithLinesGroup(questions: ApiQuestion[]): MatchingWithLinesGroupPartData;
@@ -1045,6 +1045,39 @@ export function transformMatchWordToPicture(questions) {
1045
1045
  instruction,
1046
1046
  };
1047
1047
  }
1048
+ export function transformMatchWordToPictureGroup(questions) {
1049
+ const parent = questions[0];
1050
+ const content = (parent?.content || {});
1051
+ const items = Array.isArray(content.items) ? content.items : [];
1052
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
1053
+ ? parent.correct_answer.answer
1054
+ : [];
1055
+ const title = typeof content.meta?.title === 'string' ? content.meta.title.trim() : '';
1056
+ const passage = typeof content.meta?.passage === 'string' ? content.meta.passage.trim() : '';
1057
+ const audioUrl = typeof content.meta?.audioUrl === 'string' ? content.meta.audioUrl.trim() : '';
1058
+ return {
1059
+ questions: items.map((item, index) => {
1060
+ const isExample = item.isExample === true;
1061
+ const fromItem = typeof item.correctAnswer?.answer === 'string' ? item.correctAnswer.answer : '';
1062
+ const fromParent = typeof parentCorrect[index] === 'string' ? parentCorrect[index] : '';
1063
+ return {
1064
+ id: `${parent?.id || 'q'}-item-${index}`,
1065
+ questionNumber: item.questionNumber || index + 1,
1066
+ imageUrl: item.content?.imageUrl,
1067
+ correctAnswer: fromItem || fromParent,
1068
+ isExample,
1069
+ points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
1070
+ };
1071
+ }),
1072
+ wordBank: Array.isArray(content.meta?.wordBank) ? content.meta.wordBank : [],
1073
+ ...(title ? { title } : {}),
1074
+ instruction: content.meta?.instruction || 'Look and complete.',
1075
+ ...(passage ? { passage } : {}),
1076
+ ...(audioUrl ? { audioUrl } : {}),
1077
+ ...(content.meta?.imageUrl ? { imageUrl: content.meta.imageUrl } : {}),
1078
+ explanation: parent?.explanation,
1079
+ };
1080
+ }
1048
1081
  export function transformWriteSentences(questions) {
1049
1082
  const firstQuestion = questions[0];
1050
1083
  const content = firstQuestion?.content;
@@ -1258,6 +1291,7 @@ export function transformCrosswordPuzzle(questions) {
1258
1291
  startRow: Number(c.startRow) || 1,
1259
1292
  startCol: Number(c.startCol) || 1,
1260
1293
  length: Number(c.length) || 1,
1294
+ ...(c.isExample === true ? { isExample: true } : {}),
1261
1295
  }))
1262
1296
  : [];
1263
1297
  const down = Array.isArray(content?.clues?.down)
@@ -1267,15 +1301,20 @@ export function transformCrosswordPuzzle(questions) {
1267
1301
  startRow: Number(c.startRow) || 1,
1268
1302
  startCol: Number(c.startCol) || 1,
1269
1303
  length: Number(c.length) || 1,
1304
+ ...(c.isExample === true ? { isExample: true } : {}),
1270
1305
  }))
1271
1306
  : [];
1272
1307
  const correctAnswers = correctAnswer?.answers && typeof correctAnswer.answers === 'object'
1273
1308
  ? correctAnswer.answers
1274
1309
  : {};
1275
1310
  return {
1311
+ type: content?.type === 'HIDDEN_WORD' ? 'HIDDEN_WORD' : 'STANDARD',
1276
1312
  title: content?.title || '',
1277
1313
  instruction: question?.instruction || content?.instruction || 'Complete the crossword.',
1278
1314
  gridSize: { rows, cols },
1315
+ ...(typeof content?.markedColumn === 'number' ? { markedColumn: content.markedColumn } : {}),
1316
+ hiddenWord: correctAnswers['hidden-word'] || '',
1317
+ caseSensitive: correctAnswer?.caseSensitive === true,
1279
1318
  clues: { across, down },
1280
1319
  correctAnswers,
1281
1320
  explanation: question?.explanation,
@@ -1382,18 +1421,25 @@ export function transformMatchingWithLinesGroup(questions) {
1382
1421
  const rightItems = Array.isArray(content.rightItems)
1383
1422
  ? content.rightItems.map((item, index) => ({
1384
1423
  id: typeof item?.id === 'string' ? item.id : `r${index + 1}`,
1385
- text: typeof item?.text === 'string' ? item.text : '',
1424
+ ...(typeof item?.text === 'string' && item.text.trim() ? { text: item.text } : {}),
1425
+ ...(typeof item?.imageUrl === 'string' && item.imageUrl.trim() ? { imageUrl: item.imageUrl } : {}),
1386
1426
  }))
1387
1427
  : [];
1428
+ const passage = content.meta?.passage?.trim() || '';
1429
+ const audioUrl = content.meta?.audioUrl;
1430
+ const imageUrl = content.meta?.imageUrl;
1388
1431
  return {
1389
1432
  questions: items.map((item, index) => {
1390
1433
  const isExample = item.isExample === true;
1391
1434
  const fromItem = typeof item.correctAnswer?.answer === 'string' ? item.correctAnswer.answer : '';
1392
1435
  const fromParent = typeof parentCorrect[index] === 'string' ? parentCorrect[index] : '';
1436
+ const itemImage = item.content?.imageUrl?.trim() || '';
1437
+ const itemText = item.content?.text?.trim() || '';
1393
1438
  return {
1394
1439
  id: `${parent?.id || 'q'}-item-${index}`,
1395
1440
  questionNumber: item.questionNumber || index + 1,
1396
- imageUrl: item.content?.imageUrl || '',
1441
+ ...(itemImage ? { imageUrl: itemImage } : {}),
1442
+ ...(itemText ? { text: itemText } : {}),
1397
1443
  correctRightId: fromItem || fromParent,
1398
1444
  isExample,
1399
1445
  points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
@@ -1402,6 +1448,9 @@ export function transformMatchingWithLinesGroup(questions) {
1402
1448
  rightItems,
1403
1449
  title: content.meta?.title || '',
1404
1450
  instruction: content.meta?.instruction || 'Look and match.',
1451
+ ...(passage ? { passage } : {}),
1452
+ ...(audioUrl ? { audioUrl } : {}),
1453
+ ...(imageUrl ? { imageUrl } : {}),
1405
1454
  explanation: parent?.explanation,
1406
1455
  };
1407
1456
  }
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function optionLetter(index: number): string;
3
+ export declare function KidChoiceRow({ letter, selected, isReviewMode, isCorrect, disabled, onClick, children, className, }: {
4
+ letter: string;
5
+ selected?: boolean;
6
+ isReviewMode?: boolean;
7
+ isCorrect?: boolean;
8
+ disabled?: boolean;
9
+ onClick?: () => void;
10
+ children: ReactNode;
11
+ className?: string;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, X } from 'lucide-react';
4
+ import { cn } from '../../../../../shared/lib/utils';
5
+ export function optionLetter(index) {
6
+ return String.fromCharCode(65 + index);
7
+ }
8
+ export function KidChoiceRow({ letter, selected = false, isReviewMode = false, isCorrect = false, disabled = false, onClick, children, className, }) {
9
+ const showStatus = isReviewMode && (isCorrect || selected);
10
+ const rowTone = isReviewMode
11
+ ? isCorrect
12
+ ? 'border-green-400 bg-green-50'
13
+ : selected
14
+ ? 'border-red-400 bg-red-50'
15
+ : 'border-gray-200 bg-gray-50'
16
+ : selected
17
+ ? 'border-blue-500 bg-blue-50 shadow-sm ring-2 ring-blue-100'
18
+ : 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm';
19
+ const badgeTone = isReviewMode
20
+ ? isCorrect
21
+ ? 'bg-green-500 text-white'
22
+ : selected
23
+ ? 'bg-red-500 text-white'
24
+ : 'bg-gray-200 text-gray-600'
25
+ : selected
26
+ ? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm'
27
+ : 'bg-gray-100 text-gray-600 group-hover:bg-gray-200';
28
+ return (_jsxs("button", { type: "button", onClick: onClick, disabled: disabled || isReviewMode || !onClick, className: cn('group flex w-full items-center gap-2 rounded-md border px-3 py-1.5 text-left transition-all', rowTone, (disabled || isReviewMode || !onClick) && 'cursor-default', className), children: [_jsx("div", { className: cn('flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-bold transition-all', badgeTone), children: letter }), _jsx("div", { className: cn('min-w-0 flex-1 text-sm font-medium', isReviewMode && isCorrect
29
+ ? 'text-green-800'
30
+ : isReviewMode && selected && !isCorrect
31
+ ? 'text-red-800'
32
+ : 'text-gray-700'), children: children }), showStatus && (_jsx("div", { className: cn('flex h-5 w-5 shrink-0 items-center justify-center rounded-full', isCorrect ? 'bg-green-500' : 'bg-red-500'), children: isCorrect ? _jsx(Check, { className: "h-3 w-3 text-white" }) : _jsx(X, { className: "h-3 w-3 text-white" }) }))] }));
33
+ }