@tinyweb_dev/oe-exam-sdk 0.2.12 → 0.2.14

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 (116) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +13 -3
  3. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts +14 -0
  4. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +32 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +1 -1
  6. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +10 -2
  7. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +1 -1
  8. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -2
  9. package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.d.ts +16 -0
  10. package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
  11. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  12. package/dist/components/exams/take/components/question-renderers/index.js +2 -0
  13. package/dist/components/exams/take/utils/answer-transformers.js +34 -1
  14. package/dist/components/exams/take/utils/question-transformers.d.ts +43 -4
  15. package/dist/components/exams/take/utils/question-transformers.js +71 -1
  16. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  17. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +7 -0
  18. package/dist/components/questions/_shared/types/answer-the-question-group.type.js +19 -1
  19. package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
  20. package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
  21. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
  22. package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
  23. package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +14 -2
  24. package/dist/components/questions/_shared/types/index.d.ts +2 -0
  25. package/dist/components/questions/_shared/types/index.js +2 -0
  26. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +10 -1
  27. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
  28. package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +9 -2
  29. package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
  30. package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
  31. package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
  32. package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
  33. package/dist/components/questions/creator/question-type-registry.js +4 -0
  34. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
  35. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
  36. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
  37. package/dist/components/questions/types/answer-the-question/transform.js +9 -3
  38. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +47 -3
  39. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +8 -2
  40. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +21 -2
  41. package/dist/components/questions/types/answer-the-question-group/transform.js +10 -1
  42. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
  43. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
  44. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
  45. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
  46. package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
  47. package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
  48. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
  49. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
  50. package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
  51. package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
  52. package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
  53. package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -0
  54. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +51 -16
  55. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +52 -20
  56. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +23 -0
  57. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +89 -0
  58. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +24 -6
  59. package/dist/components/questions/types/fill-in-blank-group/transform.js +18 -3
  60. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +41 -12
  61. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +23 -8
  62. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +11 -2
  63. package/dist/components/questions/types/match-word-to-picture-group/transform.js +25 -9
  64. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +4 -2
  65. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +4 -4
  66. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +13 -2
  67. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +25 -13
  68. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +19 -2
  69. package/dist/components/questions/types/matching-with-lines-group/transform.js +29 -13
  70. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
  71. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
  72. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
  73. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
  74. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
  75. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
  76. package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
  77. package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
  78. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
  79. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
  80. package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
  81. package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
  82. package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
  83. package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
  84. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -0
  85. package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
  86. package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
  87. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
  88. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
  89. package/dist/components/questions/viewer/QuestionViewer.js +13 -2
  90. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
  91. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
  92. package/dist/components/questions/viewer/index.d.ts +3 -0
  93. package/dist/components/questions/viewer/index.js +3 -0
  94. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
  95. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
  96. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  97. package/dist/components/results/renderers/review-question-body-movers.js +24 -2
  98. package/dist/components/results/review-data.js +28 -0
  99. package/dist/index.d.ts +1 -1
  100. package/dist/index.js +1 -1
  101. package/dist/shared/constants/question-skills.js +4 -0
  102. package/dist/shared/lib/utils/question-reverse-transform.js +126 -6
  103. package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
  104. package/dist/shared/lib/utils/question-transform.js +4 -0
  105. package/dist/shared/types/common.types.d.ts +1 -1
  106. package/dist/shared/types/questions/answer-the-question-group.d.ts +3 -0
  107. package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
  108. package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
  109. package/dist/shared/types/questions/fill-in-blank-group.d.ts +9 -2
  110. package/dist/shared/types/questions/index.d.ts +2 -0
  111. package/dist/shared/types/questions/index.js +3 -0
  112. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +7 -1
  113. package/dist/shared/types/questions/matching-with-lines-group.d.ts +7 -2
  114. package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
  115. package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
  116. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { ChooseThenAnswerGroupClient } from '../types/choose-then-answer-group/ChooseThenAnswerGroupClient';
5
+ import { mapQuestionToChooseThenAnswerGroupData, parseStudentAnswer, } from '../types/choose-then-answer-group/map-choose-then-answer-group-data';
6
+ function readStudentAnswers(value, itemCount) {
7
+ const record = value && typeof value === 'object' && !Array.isArray(value)
8
+ ? value
9
+ : null;
10
+ const raw = Array.isArray(record?.answer)
11
+ ? record.answer
12
+ : Array.isArray(value)
13
+ ? value
14
+ : null;
15
+ if (!raw)
16
+ return undefined;
17
+ return Array.from({ length: itemCount }, (_, index) => parseStudentAnswer(raw[index]));
18
+ }
19
+ export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
20
+ const questionData = mapQuestionToChooseThenAnswerGroupData(question);
21
+ const seed = questionData.items.map((item) => ({
22
+ optionId: item.isExample ? item.optionId : '',
23
+ text: '',
24
+ }));
25
+ const parsedUser = readStudentAnswers(userAnswer, questionData.items.length);
26
+ const questionKey = `${question.id}:${question.type}:${questionData.items.length}`;
27
+ const [cachedKey, setCachedKey] = useState(questionKey);
28
+ const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? seed);
29
+ if (cachedKey !== questionKey) {
30
+ setCachedKey(questionKey);
31
+ setLocalAnswers(parsedUser ?? seed);
32
+ }
33
+ const handleChange = (index, next) => {
34
+ const updated = localAnswers.map((item, itemIndex) => itemIndex === index ? next : item);
35
+ setLocalAnswers(updated);
36
+ onSubmitAnswer?.({ answer: updated });
37
+ };
38
+ return (_jsx(ChooseThenAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localAnswers), onAnswerChange: isReviewMode ? undefined : handleChange }));
39
+ }
@@ -21,6 +21,8 @@ import { FillMissingWordsInGridClient } from '../types/fill-missing-words-in-gri
21
21
  import { WordFillStructuredFormClient } from '../types/word-fill-structured-form/WordFillStructuredFormClient';
22
22
  import { CrosswordPuzzleClient } from '../types/crossword-puzzle/CrosswordPuzzleClient';
23
23
  import { FindWordsInMatrixClient } from '../types/find-words-in-matrix/FindWordsInMatrixClient';
24
+ import { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
25
+ import { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
24
26
  import { parseFillBlankStemLine } from '../_shared/utils/parseFillBlankStem';
25
27
  import { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
26
28
  import { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
@@ -31,6 +33,7 @@ import { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
31
33
  import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
32
34
  import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
33
35
  import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
36
+ import { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
34
37
  import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
35
38
  import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
36
39
  import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
@@ -647,6 +650,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
647
650
  return (_jsx(MatchColumnsToMakeSentencesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
648
651
  case 'CROSS_OUT_WORD_GROUP':
649
652
  return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
653
+ case 'ANSWER_THE_QUESTION':
654
+ return (_jsx(AnswerTheQuestionViewer, { question: question, isReviewMode: isReviewMode, userAnswer: typeof userAnswer === 'string' ? userAnswer : undefined, onSubmitAnswer: onSubmitAnswer }));
650
655
  case 'ANSWER_THE_QUESTION_GROUP':
651
656
  return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
652
657
  case 'SPONTANEOUS_QA_GROUP':
@@ -703,6 +708,10 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
703
708
  return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.answer, isReviewMode: isReviewMode, userAnswers: userAnswer }));
704
709
  case 'FIND_WORDS_IN_MATRIX':
705
710
  return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.answer, showSolution: isReviewMode }));
711
+ case 'SORT_WORDS_INTO_CATEGORIES':
712
+ return (_jsx(SortWordsIntoCategoriesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
713
+ case 'CHOOSE_THEN_ANSWER_GROUP':
714
+ return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
706
715
  default:
707
716
  return _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
708
717
  }
@@ -716,7 +725,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
716
725
  // WFSF and Crossword show title/instruction inside the client — no outer card label.
717
726
  if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
718
727
  question.type === 'CROSSWORD_PUZZLE' ||
719
- question.type === 'MATCH_WORD_TO_PICTURE') {
728
+ question.type === 'MATCH_WORD_TO_PICTURE' ||
729
+ question.type === 'ANSWER_THE_QUESTION') {
720
730
  return null;
721
731
  }
722
732
  // Modern fill stems live entirely in the body; title is a short label.
@@ -741,7 +751,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
741
751
  // WFSF and Crossword already have their own bordered shell — avoid double Card border.
742
752
  if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
743
753
  question.type === 'CROSSWORD_PUZZLE' ||
744
- question.type === 'MATCH_WORD_TO_PICTURE') {
754
+ question.type === 'MATCH_WORD_TO_PICTURE' ||
755
+ question.type === 'ANSWER_THE_QUESTION') {
745
756
  return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
746
757
  }
747
758
  return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsxs("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: [question.points, " \u0111i\u1EC3m"] })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
@@ -0,0 +1,10 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ import type { SortWordsPlacement } from '../_shared/types/sort-words-into-categories.type';
3
+ export declare function SortWordsIntoCategoriesViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
4
+ question: Question;
5
+ isReviewMode?: boolean;
6
+ userAnswer?: unknown;
7
+ onSubmitAnswer?: (answer: {
8
+ placements: SortWordsPlacement[];
9
+ }) => void;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { SortWordsIntoCategoriesClient } from '../types/sort-words-into-categories/SortWordsIntoCategoriesClient';
5
+ import { mapQuestionToSortWordsIntoCategoriesData, readPlacements, } from '../types/sort-words-into-categories/map-sort-words-into-categories-data';
6
+ export function SortWordsIntoCategoriesViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
7
+ const questionData = mapQuestionToSortWordsIntoCategoriesData(question);
8
+ const parsedUser = readPlacements(userAnswer);
9
+ const questionKey = `${question.id}:${question.type}:${questionData.words.map((word) => word.id).join(',')}`;
10
+ const [cachedKey, setCachedKey] = useState(questionKey);
11
+ const [localPlacements, setLocalPlacements] = useState(() => parsedUser ?? questionData.words.map((word) => ({
12
+ wordId: word.id,
13
+ categoryId: word.isExample ? word.categoryId : '',
14
+ })));
15
+ if (cachedKey !== questionKey) {
16
+ setCachedKey(questionKey);
17
+ setLocalPlacements(parsedUser ?? questionData.words.map((word) => ({
18
+ wordId: word.id,
19
+ categoryId: word.isExample ? word.categoryId : '',
20
+ })));
21
+ }
22
+ const handleChange = (next) => {
23
+ setLocalPlacements(next);
24
+ onSubmitAnswer?.({ placements: next });
25
+ };
26
+ return (_jsx(SortWordsIntoCategoriesClient, { questionData: questionData, isReviewMode: isReviewMode, userPlacements: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localPlacements), onPlacementsChange: isReviewMode ? undefined : handleChange }));
27
+ }
@@ -9,8 +9,11 @@ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
9
9
  export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
10
10
  export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
11
11
  export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
12
+ export { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
12
13
  export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
13
14
  export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
14
15
  export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
15
16
  export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
16
17
  export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
18
+ export { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
19
+ export { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
@@ -9,8 +9,11 @@ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
9
9
  export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
10
10
  export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
11
11
  export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
12
+ export { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
12
13
  export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
13
14
  export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
14
15
  export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
15
16
  export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
16
17
  export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
18
+ export { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
19
+ export { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
@@ -11,9 +11,10 @@ interface ReviewAnswerTheQuestionGroupRendererProps {
11
11
  questionCount: number;
12
12
  instruction: string;
13
13
  groupContent?: string;
14
+ imageUrl?: string | string[];
14
15
  questions: AnswerTheQuestionQuestion[];
15
16
  answers: Record<string, string>;
16
17
  itemMeta: Record<string, ReviewAnswerTheQuestionGroupItemMeta>;
17
18
  }
18
- export declare function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, questions, answers, itemMeta, }: ReviewAnswerTheQuestionGroupRendererProps): import("react").JSX.Element;
19
+ export declare function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, imageUrl, questions, answers, itemMeta, }: ReviewAnswerTheQuestionGroupRendererProps): import("react").JSX.Element;
19
20
  export {};
@@ -1,10 +1,19 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { BookOpen, Check, Sparkles, Star, X } from 'lucide-react';
4
+ import { ResolvedImage } from '../../../components/common/ResolvedImage';
4
5
  import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
5
6
  import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
6
- export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, questions, answers, itemMeta, }) {
7
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), groupContent && (_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 leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })] })), questions.map((question) => {
7
+ import { toAnswerTheQuestionImageUrls } from '../../questions/_shared/types/answer-the-question.type';
8
+ function ReviewImages({ imageUrl, alt, }) {
9
+ const urls = toAnswerTheQuestionImageUrls(imageUrl);
10
+ if (urls.length === 0) {
11
+ return null;
12
+ }
13
+ return (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: urls.map((url, imageIndex) => (_jsx(ResolvedImage, { src: url, alt: `${alt} ${imageIndex + 1}`, className: "max-h-56 w-auto rounded-lg object-contain" }, `${alt}-${imageIndex}`))) }));
14
+ }
15
+ export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, imageUrl, questions, answers, itemMeta, }) {
16
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx(ReviewImages, { imageUrl: imageUrl, alt: "Shared illustration" }), groupContent && (_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 leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })] })), questions.map((question) => {
8
17
  const meta = itemMeta[question.id];
9
18
  const studentAnswer = answers[question.id] || '';
10
19
  const expectedAnswers = meta?.expectedAnswers || [];
@@ -16,7 +25,7 @@ export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, que
16
25
  ? 'bg-green-500'
17
26
  : isCorrect === false
18
27
  ? 'bg-red-500'
19
- : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber] }), question.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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), isCorrect === true && _jsx(Check, { className: "h-4 w-4 text-green-600" }), isCorrect === false && _jsx(X, { className: "h-4 w-4 text-red-600" })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: question.question }), _jsx("div", { className: `rounded-lg border px-3 py-2 text-sm ${isCorrect === true
28
+ : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber] }), question.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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), isCorrect === true && _jsx(Check, { className: "h-4 w-4 text-green-600" }), isCorrect === false && _jsx(X, { className: "h-4 w-4 text-red-600" })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx(ReviewImages, { imageUrl: question.imageUrl, alt: `Hình ảnh câu ${question.questionNumber}` }), _jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: question.question }), _jsx("div", { className: `rounded-lg border px-3 py-2 text-sm ${isCorrect === true
20
29
  ? 'border-green-200 bg-green-50 text-green-800'
21
30
  : isCorrect === false
22
31
  ? 'border-red-200 bg-red-50 text-red-800'
@@ -115,7 +115,7 @@ export function renderDedicatedReviewBody(ctx) {
115
115
  feedbackVi: typeof feedback.vi === 'string' ? feedback.vi : undefined,
116
116
  }];
117
117
  }));
118
- return (_jsx(ReviewAnswerTheQuestionGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: data.questions, answers: answers, itemMeta: itemMeta }));
118
+ return (_jsx(ReviewAnswerTheQuestionGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, imageUrl: data.imageUrl, questions: data.questions, answers: answers, itemMeta: itemMeta }));
119
119
  }
120
120
  case 'SPONTANEOUS_QA_GROUP': {
121
121
  const parent = questions[0];
@@ -4,8 +4,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  * Movers fallback review cases (isReviewMode=true).
5
5
  * Source: oe-exam-fe ReviewQuestionRenderer.tsx
6
6
  */
7
- import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
8
- import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
7
+ import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, ChooseThenAnswerGroupRenderer, SortWordsIntoCategoriesRenderer, } from '../../../components/exams/take/components/question-renderers';
8
+ import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, transformChooseThenAnswerGroup, transformSortWordsIntoCategories, } from '../../../components/exams/take/utils/question-transformers';
9
9
  import { normalizeCrossOutWordGroupAnswer, transformCrossOutWordGroup, } from '../../../components/exams/take/utils/cross-out-word-group.transformer';
10
10
  function asApiQuestions(questions) {
11
11
  return questions;
@@ -110,6 +110,28 @@ export function renderMoversReviewBody(ctx) {
110
110
  });
111
111
  return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
112
112
  }
113
+ case 'CHOOSE_THEN_ANSWER_GROUP': {
114
+ const data = transformChooseThenAnswerGroup(apiQuestions);
115
+ const gradableCount = data.questions.filter((item) => !item.isExample).length;
116
+ return (_jsx(ChooseThenAnswerGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: () => undefined, isReviewMode: true }));
117
+ }
118
+ case 'SORT_WORDS_INTO_CATEGORIES': {
119
+ const data = transformSortWordsIntoCategories(apiQuestions);
120
+ const firstQuestion = apiQuestions[0];
121
+ const stored = firstQuestion?.id ? answers[firstQuestion.id] : undefined;
122
+ const storedRecord = stored && typeof stored === 'object' && !Array.isArray(stored)
123
+ ? stored
124
+ : undefined;
125
+ const storedPlacements = Array.isArray(storedRecord?.placements)
126
+ ? storedRecord.placements
127
+ : Array.isArray(stored)
128
+ ? stored
129
+ : [];
130
+ const gradableCount = data.words.filter((word) => !word.isExample).length;
131
+ return (_jsx(SortWordsIntoCategoriesRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: {
132
+ [data.questionId]: { placements: storedPlacements },
133
+ }, onAnswerChange: () => undefined, isReviewMode: true }));
134
+ }
113
135
  case 'TRUE_FALSE_CORRECT_GROUP': {
114
136
  const data = transformTrueFalseCorrectGroup(apiQuestions);
115
137
  const correctAnswerMap = {};
@@ -9,6 +9,7 @@ const FLATTENED_ANSWER_TYPES = new Set([
9
9
  'ANSWER_THE_QUESTION_GROUP',
10
10
  'CROSS_OUT_WORD_GROUP',
11
11
  'TRUE_FALSE_CORRECT_GROUP',
12
+ 'CHOOSE_THEN_ANSWER_GROUP',
12
13
  'FILL_IN_BLANK_GROUP',
13
14
  'MATCHING_WITH_LINES_GROUP',
14
15
  ]);
@@ -186,6 +187,26 @@ function transformStudentAnswer(questionId, questionType, value) {
186
187
  normalizeCrossOutWordGroupAnswer(item),
187
188
  ]));
188
189
  }
190
+ if (questionType === 'CHOOSE_THEN_ANSWER_GROUP') {
191
+ const answer = Array.isArray(record.answer)
192
+ ? record.answer
193
+ : Array.isArray(value)
194
+ ? value
195
+ : [];
196
+ return Object.fromEntries(answer.map((item, index) => {
197
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
198
+ const obj = item;
199
+ return [
200
+ `${questionId}-item-${index}`,
201
+ {
202
+ optionId: typeof obj.optionId === 'string' ? obj.optionId : '',
203
+ text: typeof obj.text === 'string' ? obj.text : '',
204
+ },
205
+ ];
206
+ }
207
+ return [`${questionId}-item-${index}`, { optionId: '', text: '' }];
208
+ }));
209
+ }
189
210
  if (questionType === 'TRUE_FALSE_CORRECT_GROUP') {
190
211
  const answer = Array.isArray(record.answer)
191
212
  ? record.answer
@@ -282,6 +303,13 @@ function transformStudentAnswer(questionId, questionType, value) {
282
303
  }
283
304
  if (questionType === 'READ_AND_COLOR_OBJECTS' || questionType === 'IMAGE_OBJECT_MATCHING' || questionType === 'CROSSWORD_PUZZLE')
284
305
  return record.cells ?? record.answers ?? value;
306
+ if (questionType === 'SORT_WORDS_INTO_CATEGORIES') {
307
+ if (Array.isArray(record.placements))
308
+ return record;
309
+ if (Array.isArray(value))
310
+ return { placements: value };
311
+ return value;
312
+ }
285
313
  if (questionType === 'WRITE_A_SHORT_LETTER')
286
314
  return typeof value === 'string' ? value : record.answer ?? '';
287
315
  return record.answer ?? value;
package/dist/index.d.ts CHANGED
@@ -26,4 +26,4 @@ export { validateQuestion } from './components/questions/_shared/utils/question-
26
26
  export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
27
27
  export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
28
28
  export * from './components/questions/_shared/types';
29
- export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
29
+ export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
package/dist/index.js CHANGED
@@ -21,4 +21,4 @@ export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from '
21
21
  export { validateQuestion } from './components/questions/_shared/utils/question-validation';
22
22
  export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
23
23
  export * from './components/questions/_shared/types';
24
- export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
24
+ export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
@@ -84,6 +84,8 @@ export const QUESTION_TYPE_SKILLS = {
84
84
  'WORD_FILL_STRUCTURED_FORM': SkillEnum.READING,
85
85
  'CROSSWORD_PUZZLE': SkillEnum.READING,
86
86
  'FIND_WORDS_IN_MATRIX': SkillEnum.READING,
87
+ 'SORT_WORDS_INTO_CATEGORIES': SkillEnum.READING,
88
+ 'CHOOSE_THEN_ANSWER_GROUP': SkillEnum.READING,
87
89
  'MATCH_COLUMNS_TO_MAKE_SENTENCES': SkillEnum.READING,
88
90
  'MATCH_WORD_TO_PICTURE_GROUP': SkillEnum.READING,
89
91
  'WORD_ORDERING_GROUP': SkillEnum.READING,
@@ -219,6 +221,8 @@ export const QUESTION_TYPE_LABELS = {
219
221
  'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
220
222
  'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
221
223
  'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
224
+ 'SORT_WORDS_INTO_CATEGORIES': 'Sắp xếp từ vào nhóm',
225
+ 'CHOOSE_THEN_ANSWER_GROUP': 'Chọn đáp án rồi trả lời',
222
226
  'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
223
227
  'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
224
228
  'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',
@@ -1,3 +1,5 @@
1
+ import { fromFillInBlankImageUrls, toFillInBlankImageUrls, } from '../../../components/questions/types/fill-in-blank-group/blank-utils';
2
+ import { fromMatchWordToPictureImageUrls } from '../../../components/questions/_shared/types/match-word-to-picture-group.type';
1
3
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
4
  const asRecord = (value) => isRecord(value) ? value : {};
3
5
  const asArray = (value) => Array.isArray(value) ? value : [];
@@ -301,19 +303,30 @@ const reverseAnswerTheQuestionGroup = (apiQuestion) => {
301
303
  .filter((value) => value !== '');
302
304
  const expectedAnswers = nested.length > 0 ? nested : (fallbackExpected.length > 0 ? fallbackExpected : ['']);
303
305
  const isExample = item.isExample === true;
306
+ const imageUrl = normalizeChooseAnswerGroupImageUrl(itemContent.imageUrl);
304
307
  return {
305
308
  question: asString(itemContent.question),
306
309
  expectedAnswers,
307
310
  isExample,
308
311
  points: isExample ? 0 : asNumber(item.points, 1),
309
312
  questionNumber: asNumber(item.questionNumber, index + 1),
313
+ ...(imageUrl !== undefined ? { imageUrl } : {}),
310
314
  };
311
315
  });
316
+ const meta = asRecord(content.meta);
317
+ const audioUrl = asString(meta.audioUrl).trim();
318
+ const imageUrl = asArray(meta.imageUrl)
319
+ .filter((value) => typeof value === 'string')
320
+ .map((value) => value.trim())
321
+ .filter((value) => value !== '');
322
+ const singleImage = asString(meta.imageUrl).trim();
312
323
  return {
313
324
  content: '',
314
325
  answer: {
315
- instruction: asString(asRecord(content.meta).instruction, 'Read the text again and answer the questions.'),
316
- passage: asString(asRecord(content.meta).passage),
326
+ instruction: asString(meta.instruction, 'Read the text again and answer the questions.'),
327
+ passage: asString(meta.passage),
328
+ ...(audioUrl ? { audioUrl } : {}),
329
+ ...(imageUrl.length ? { imageUrl } : (singleImage ? { imageUrl: [singleImage] } : {})),
317
330
  items,
318
331
  explanation: asString(apiQuestion.explanation),
319
332
  points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
@@ -383,6 +396,45 @@ const reverseTrueFalseCorrectGroup = (apiQuestion) => {
383
396
  },
384
397
  };
385
398
  };
399
+ const reverseChooseThenAnswerGroup = (apiQuestion) => {
400
+ const content = asRecord(apiQuestion.content);
401
+ const answer = readAnswer(apiQuestion);
402
+ const fallbackAnswers = asArray(answer.answer);
403
+ const meta = asRecord(content.meta);
404
+ const items = asArray(content.items).map((rawItem, index) => {
405
+ const item = asRecord(rawItem);
406
+ const itemContent = asRecord(item.content);
407
+ const itemAnswer = asRecord(asRecord(item.correctAnswer).answer);
408
+ const fallback = asRecord(fallbackAnswers[index]);
409
+ const rawAnswer = Object.keys(itemAnswer).length > 0 ? itemAnswer : fallback;
410
+ const isExample = item.isExample === true;
411
+ const options = asArray(itemContent.options).map((rawOption, optionIndex) => {
412
+ const option = asRecord(rawOption);
413
+ return {
414
+ id: asString(option.id, `opt-${optionIndex + 1}`),
415
+ text: asString(option.text),
416
+ };
417
+ });
418
+ return {
419
+ question: asString(itemContent.question),
420
+ options,
421
+ optionId: asString(rawAnswer.optionId) || options[0]?.id || '',
422
+ isExample,
423
+ points: isExample ? 0 : asNumber(item.points, 1),
424
+ questionNumber: asNumber(item.questionNumber, index + 1),
425
+ };
426
+ });
427
+ return {
428
+ content: '',
429
+ answer: {
430
+ title: asString(meta.title),
431
+ instruction: asString(meta.instruction, 'Read and choose. Then answer for you.'),
432
+ items,
433
+ explanation: asString(apiQuestion.explanation),
434
+ points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
435
+ },
436
+ };
437
+ };
386
438
  const reverseFillInBlankGroup = (apiQuestion) => {
387
439
  const content = asRecord(apiQuestion.content);
388
440
  const answer = readAnswer(apiQuestion);
@@ -402,27 +454,34 @@ const reverseFillInBlankGroup = (apiQuestion) => {
402
454
  : [String(blank ?? '').trim()].filter(Boolean);
403
455
  return alts.length > 0 ? alts : [''];
404
456
  });
457
+ const wordBank = asArray(itemContent.wordBank);
405
458
  return {
406
459
  question,
407
460
  answers: answers.length > 0 ? answers : [['']],
408
461
  caseSensitive: itemAnswer.caseSensitive === true || fallback.caseSensitive === true,
409
- imageUrl: asString(itemContent.imageUrl),
462
+ imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(itemContent.imageUrl)),
410
463
  audioUrl: asString(itemContent.audioUrl),
411
464
  showHints: itemContent.showHints === true,
412
465
  hints: asArray(itemContent.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
466
+ ...(wordBank.length > 0 ? { wordBank } : {}),
413
467
  isExample,
414
468
  points: isExample ? 0 : asNumber(item.points, 1),
415
469
  questionNumber: asNumber(item.questionNumber, index + 1),
416
470
  };
417
471
  });
472
+ const groupWordBank = asArray(meta.wordBank);
473
+ const viewMode = asString(meta.viewMode) === 'WITH_OPTIONS' ? 'WITH_OPTIONS' : undefined;
418
474
  return {
419
475
  content: '',
420
476
  answer: {
421
477
  instruction: asString(meta.instruction, 'Complete the sentences.'),
478
+ passage: asString(meta.passage),
422
479
  audioUrl: asString(meta.audioUrl),
423
- imageUrl: asString(meta.imageUrl),
480
+ imageUrl: fromFillInBlankImageUrls(toFillInBlankImageUrls(meta.imageUrl)),
424
481
  showHints: meta.showHints === true,
425
482
  hints: asArray(meta.hints).map((hint) => String(hint ?? '').trim()).filter(Boolean),
483
+ ...(viewMode ? { viewMode } : {}),
484
+ ...(groupWordBank.length > 0 ? { wordBank: groupWordBank } : {}),
426
485
  items,
427
486
  explanation: asString(apiQuestion.explanation),
428
487
  points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
@@ -436,9 +495,12 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
436
495
  const meta = asRecord(content.meta);
437
496
  const rightItems = asArray(content.rightItems).map((raw, index) => {
438
497
  const item = asRecord(raw);
498
+ const imageUrl = asString(item.imageUrl).trim();
499
+ const text = asString(item.text).trim();
439
500
  return {
440
501
  id: asString(item.id, `r${index + 1}`),
441
- text: asString(item.text),
502
+ ...(text ? { text } : {}),
503
+ ...(imageUrl ? { imageUrl } : {}),
442
504
  };
443
505
  });
444
506
  const items = asArray(content.items).map((rawItem, index) => {
@@ -449,6 +511,7 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
449
511
  const isExample = item.isExample === true;
450
512
  return {
451
513
  imageUrl: asString(itemContent.imageUrl),
514
+ text: asString(itemContent.text),
452
515
  correctAnswer: asString(itemAnswer.answer, typeof fallback === 'string' ? fallback : ''),
453
516
  isExample,
454
517
  points: isExample ? 0 : asNumber(item.points, 1),
@@ -460,6 +523,9 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
460
523
  answer: {
461
524
  title: asString(meta.title),
462
525
  instruction: asString(meta.instruction, 'Look and match.'),
526
+ ...(asString(meta.passage).trim() ? { passage: asString(meta.passage).trim() } : {}),
527
+ ...(asString(meta.audioUrl).trim() ? { audioUrl: asString(meta.audioUrl).trim() } : {}),
528
+ ...(asString(meta.imageUrl).trim() ? { imageUrl: asString(meta.imageUrl).trim() } : {}),
463
529
  rightItems,
464
530
  items,
465
531
  explanation: asString(apiQuestion.explanation),
@@ -658,8 +724,9 @@ const reverseMatchWordToPictureGroup = (apiQuestion) => {
658
724
  const itemContent = asRecord(item.content);
659
725
  const itemAnswer = asRecord(item.correctAnswer);
660
726
  const isExample = item.isExample === true;
727
+ const itemImageUrl = fromMatchWordToPictureImageUrls(itemContent.imageUrl ?? item.imageUrl);
661
728
  return {
662
- imageUrl: asString(itemContent.imageUrl),
729
+ ...(itemImageUrl ? { imageUrl: itemImageUrl } : {}),
663
730
  correctAnswer: asString(itemAnswer.answer) || asString(fallbackAnswers[index]),
664
731
  isExample,
665
732
  points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
@@ -669,7 +736,13 @@ const reverseMatchWordToPictureGroup = (apiQuestion) => {
669
736
  return {
670
737
  content: '',
671
738
  answer: {
739
+ ...(asString(meta.title).trim() ? { title: asString(meta.title).trim() } : {}),
672
740
  instruction: asString(meta.instruction, 'Look and complete.'),
741
+ ...(asString(meta.passage).trim() ? { passage: asString(meta.passage).trim() } : {}),
742
+ ...(asString(meta.audioUrl).trim() ? { audioUrl: asString(meta.audioUrl).trim() } : {}),
743
+ ...(fromMatchWordToPictureImageUrls(meta.imageUrl)
744
+ ? { imageUrl: fromMatchWordToPictureImageUrls(meta.imageUrl) }
745
+ : {}),
673
746
  wordBank: asArray(meta.wordBank).map((word) => asString(word)).filter(Boolean),
674
747
  items: mappedItems,
675
748
  explanation: asString(apiQuestion.explanation),
@@ -717,6 +790,7 @@ const handlers = {
717
790
  ANSWER_THE_QUESTION_GROUP: reverseAnswerTheQuestionGroup,
718
791
  TRUE_FALSE_GROUP: reverseTrueFalseGroup,
719
792
  TRUE_FALSE_CORRECT_GROUP: reverseTrueFalseCorrectGroup,
793
+ CHOOSE_THEN_ANSWER_GROUP: reverseChooseThenAnswerGroup,
720
794
  FILL_IN_BLANK_GROUP: reverseFillInBlankGroup,
721
795
  MATCHING_WITH_LINES_GROUP: reverseMatchingWithLinesGroup,
722
796
  CROSS_OUT_WORD_GROUP: reverseCrossOutWordGroup,
@@ -761,6 +835,7 @@ const handlers = {
761
835
  GN_SPEAKING_INTERVIEW: reverseGroupPayload,
762
836
  CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
763
837
  FIND_WORDS_IN_MATRIX: reverseFindWordsInMatrix,
838
+ SORT_WORDS_INTO_CATEGORIES: reverseSortWordsIntoCategories,
764
839
  MATCH_COLUMNS_TO_MAKE_SENTENCES: reverseMatchColumnsToMakeSentences,
765
840
  MATCH_WORD_TO_PICTURE_GROUP: reverseMatchWordToPictureGroup,
766
841
  WORD_ORDERING_GROUP: reverseWordOrderingGroup,
@@ -935,6 +1010,51 @@ function reverseFindWordsInMatrix(apiQuestion) {
935
1010
  },
936
1011
  };
937
1012
  }
1013
+ function reverseSortWordsIntoCategories(apiQuestion) {
1014
+ const apiContent = asRecord(apiQuestion.content);
1015
+ const apiAnswer = readAnswer(apiQuestion);
1016
+ const meta = asRecord(apiContent.meta);
1017
+ const categories = asArray(apiContent.categories);
1018
+ const words = asArray(apiContent.words);
1019
+ const placements = asArray(apiAnswer.placements);
1020
+ const placementMap = new Map();
1021
+ placements.forEach((item) => {
1022
+ const record = asRecord(item);
1023
+ const wordId = asString(record.wordId);
1024
+ if (wordId && !placementMap.has(wordId)) {
1025
+ placementMap.set(wordId, asString(record.categoryId));
1026
+ }
1027
+ });
1028
+ const mappedWords = words.map((raw, index) => {
1029
+ const item = asRecord(raw);
1030
+ const id = asString(item.id, `w${index + 1}`);
1031
+ const isExample = item.isExample === true;
1032
+ return {
1033
+ id,
1034
+ text: asString(item.text),
1035
+ categoryId: placementMap.get(id) || '',
1036
+ isExample,
1037
+ points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
1038
+ };
1039
+ });
1040
+ return {
1041
+ content: '',
1042
+ answer: {
1043
+ title: asString(meta.title),
1044
+ instruction: asString(meta.instruction, 'Read and sort the words.'),
1045
+ categories: categories.map((raw, index) => {
1046
+ const item = asRecord(raw);
1047
+ return {
1048
+ id: asString(item.id, `cat_${index + 1}`),
1049
+ name: asString(item.name),
1050
+ };
1051
+ }),
1052
+ words: mappedWords,
1053
+ explanation: asString(apiQuestion.explanation),
1054
+ points: mappedWords.reduce((total, word) => (word.isExample ? total : total + (word.points || 0)), 0),
1055
+ },
1056
+ };
1057
+ }
938
1058
  export function transformApiQuestionToFrontend(apiQuestion) {
939
1059
  const questionType = String(apiQuestion.questionType ?? apiQuestion.question_type ?? apiQuestion.type ?? '');
940
1060
  const handlerResult = (handlers[questionType] ?? defaultReverse)(apiQuestion);
@@ -312,7 +312,7 @@ export interface AnswerTheQuestionAnswerData {
312
312
  groupNumber?: number;
313
313
  isExample?: boolean;
314
314
  suggestedAnswers?: string[];
315
- imageUrl?: string;
315
+ imageUrl?: string | string[];
316
316
  title?: string;
317
317
  subtitle?: string;
318
318
  groupContent?: string;
@@ -47,6 +47,8 @@ import { transformGNSpeakingInterview } from '../../../components/questions/type
47
47
  import { transformWordFillStructuredForm } from '../../../components/questions/types/word-fill-structured-form/transform';
48
48
  import { transformCrosswordPuzzle } from '../../../components/questions/types/crossword-puzzle/transform';
49
49
  import { transformFindWordsInMatrix } from '../../../components/questions/types/find-words-in-matrix/transform';
50
+ import { transformSortWordsIntoCategories } from '../../../components/questions/types/sort-words-into-categories/transform';
51
+ import { transformChooseThenAnswerGroup } from '../../../components/questions/types/choose-then-answer-group/transform';
50
52
  import { transformMatchColumnsToMakeSentences } from '../../../components/questions/types/match-columns-to-make-sentences/transform';
51
53
  import { transformMatchWordToPictureGroup } from '../../../components/questions/types/match-word-to-picture-group/transform';
52
54
  import { transformWordOrderingGroup } from '../../../components/questions/types/word-ordering-group/transform';
@@ -103,6 +105,8 @@ const questionTransformHandlers = {
103
105
  WORD_FILL_STRUCTURED_FORM: transformWordFillStructuredForm,
104
106
  CROSSWORD_PUZZLE: transformCrosswordPuzzle,
105
107
  FIND_WORDS_IN_MATRIX: transformFindWordsInMatrix,
108
+ SORT_WORDS_INTO_CATEGORIES: transformSortWordsIntoCategories,
109
+ CHOOSE_THEN_ANSWER_GROUP: transformChooseThenAnswerGroup,
106
110
  MATCH_COLUMNS_TO_MAKE_SENTENCES: transformMatchColumnsToMakeSentences,
107
111
  MATCH_WORD_TO_PICTURE_GROUP: transformMatchWordToPictureGroup,
108
112
  WORD_ORDERING_GROUP: transformWordOrderingGroup,