@tinyweb_dev/oe-exam-sdk 0.2.13 → 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 (97) 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/SortWordsIntoCategoriesRenderer.d.ts +16 -0
  8. package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
  9. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  10. package/dist/components/exams/take/components/question-renderers/index.js +2 -0
  11. package/dist/components/exams/take/utils/answer-transformers.js +34 -1
  12. package/dist/components/exams/take/utils/question-transformers.d.ts +42 -3
  13. package/dist/components/exams/take/utils/question-transformers.js +71 -1
  14. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  15. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +5 -1
  16. package/dist/components/questions/_shared/types/answer-the-question-group.type.js +7 -0
  17. package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
  18. package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
  19. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
  20. package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
  21. package/dist/components/questions/_shared/types/index.d.ts +2 -0
  22. package/dist/components/questions/_shared/types/index.js +2 -0
  23. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +9 -3
  24. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
  25. package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
  26. package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
  27. package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
  28. package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
  29. package/dist/components/questions/creator/question-type-registry.js +4 -0
  30. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
  31. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
  32. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
  33. package/dist/components/questions/types/answer-the-question/transform.js +9 -3
  34. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +42 -9
  35. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -5
  36. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +18 -4
  37. package/dist/components/questions/types/answer-the-question-group/transform.js +8 -2
  38. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
  39. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
  40. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
  41. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
  42. package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
  43. package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
  44. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
  45. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
  46. package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
  47. package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
  48. package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
  49. package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -0
  50. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +39 -10
  51. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +15 -8
  52. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +8 -3
  53. package/dist/components/questions/types/match-word-to-picture-group/transform.js +21 -13
  54. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
  55. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
  56. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
  57. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
  58. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
  59. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
  60. package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
  61. package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
  62. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
  63. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
  64. package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
  65. package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
  66. package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
  67. package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
  68. package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
  69. package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
  70. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
  71. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
  72. package/dist/components/questions/viewer/QuestionViewer.js +13 -2
  73. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
  74. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
  75. package/dist/components/questions/viewer/index.d.ts +3 -0
  76. package/dist/components/questions/viewer/index.js +3 -0
  77. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
  78. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
  79. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  80. package/dist/components/results/renderers/review-question-body-movers.js +24 -2
  81. package/dist/components/results/review-data.js +28 -0
  82. package/dist/index.d.ts +1 -1
  83. package/dist/index.js +1 -1
  84. package/dist/shared/constants/question-skills.js +4 -0
  85. package/dist/shared/lib/utils/question-reverse-transform.js +98 -2
  86. package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
  87. package/dist/shared/lib/utils/question-transform.js +4 -0
  88. package/dist/shared/types/common.types.d.ts +1 -1
  89. package/dist/shared/types/questions/answer-the-question-group.d.ts +3 -0
  90. package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
  91. package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
  92. package/dist/shared/types/questions/index.d.ts +2 -0
  93. package/dist/shared/types/questions/index.js +3 -0
  94. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +6 -2
  95. package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
  96. package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
  97. package/package.json +1 -1
@@ -0,0 +1,9 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ type AnswerTheQuestionViewerProps = {
3
+ question: Question;
4
+ isReviewMode?: boolean;
5
+ userAnswer?: string;
6
+ onSubmitAnswer?: (answer: string) => void;
7
+ };
8
+ export declare function AnswerTheQuestionViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: AnswerTheQuestionViewerProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { AnswerTheQuestionClient } from '../types/answer-the-question/AnswerTheQuestionClient';
4
+ import { mapQuestionToAnswerTheQuestionData } from '../types/answer-the-question/map-answer-the-question-data';
5
+ export function AnswerTheQuestionViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
+ const questionData = mapQuestionToAnswerTheQuestionData(question);
7
+ return (_jsx(AnswerTheQuestionClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswer: userAnswer, autoFillCorrectAnswers: isReviewMode && !userAnswer, explanation: question.explanation || questionData.explanation, onSubmit: isReviewMode ? undefined : onSubmitAnswer }));
8
+ }
@@ -0,0 +1,10 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ import type { ChooseThenAnswerStudentItemAnswer } from '../_shared/types/choose-then-answer-group.type';
3
+ export declare function ChooseThenAnswerGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
4
+ question: Question;
5
+ isReviewMode?: boolean;
6
+ userAnswer?: unknown;
7
+ onSubmitAnswer?: (answer: {
8
+ answer: ChooseThenAnswerStudentItemAnswer[];
9
+ }) => void;
10
+ }): import("react").JSX.Element;
@@ -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,4 +1,5 @@
1
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';
2
3
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
3
4
  const asRecord = (value) => isRecord(value) ? value : {};
4
5
  const asArray = (value) => Array.isArray(value) ? value : [];
@@ -302,15 +303,18 @@ const reverseAnswerTheQuestionGroup = (apiQuestion) => {
302
303
  .filter((value) => value !== '');
303
304
  const expectedAnswers = nested.length > 0 ? nested : (fallbackExpected.length > 0 ? fallbackExpected : ['']);
304
305
  const isExample = item.isExample === true;
306
+ const imageUrl = normalizeChooseAnswerGroupImageUrl(itemContent.imageUrl);
305
307
  return {
306
308
  question: asString(itemContent.question),
307
309
  expectedAnswers,
308
310
  isExample,
309
311
  points: isExample ? 0 : asNumber(item.points, 1),
310
312
  questionNumber: asNumber(item.questionNumber, index + 1),
313
+ ...(imageUrl !== undefined ? { imageUrl } : {}),
311
314
  };
312
315
  });
313
316
  const meta = asRecord(content.meta);
317
+ const audioUrl = asString(meta.audioUrl).trim();
314
318
  const imageUrl = asArray(meta.imageUrl)
315
319
  .filter((value) => typeof value === 'string')
316
320
  .map((value) => value.trim())
@@ -321,6 +325,7 @@ const reverseAnswerTheQuestionGroup = (apiQuestion) => {
321
325
  answer: {
322
326
  instruction: asString(meta.instruction, 'Read the text again and answer the questions.'),
323
327
  passage: asString(meta.passage),
328
+ ...(audioUrl ? { audioUrl } : {}),
324
329
  ...(imageUrl.length ? { imageUrl } : (singleImage ? { imageUrl: [singleImage] } : {})),
325
330
  items,
326
331
  explanation: asString(apiQuestion.explanation),
@@ -391,6 +396,45 @@ const reverseTrueFalseCorrectGroup = (apiQuestion) => {
391
396
  },
392
397
  };
393
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
+ };
394
438
  const reverseFillInBlankGroup = (apiQuestion) => {
395
439
  const content = asRecord(apiQuestion.content);
396
440
  const answer = readAnswer(apiQuestion);
@@ -680,8 +724,9 @@ const reverseMatchWordToPictureGroup = (apiQuestion) => {
680
724
  const itemContent = asRecord(item.content);
681
725
  const itemAnswer = asRecord(item.correctAnswer);
682
726
  const isExample = item.isExample === true;
727
+ const itemImageUrl = fromMatchWordToPictureImageUrls(itemContent.imageUrl ?? item.imageUrl);
683
728
  return {
684
- imageUrl: asString(itemContent.imageUrl),
729
+ ...(itemImageUrl ? { imageUrl: itemImageUrl } : {}),
685
730
  correctAnswer: asString(itemAnswer.answer) || asString(fallbackAnswers[index]),
686
731
  isExample,
687
732
  points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
@@ -693,7 +738,11 @@ const reverseMatchWordToPictureGroup = (apiQuestion) => {
693
738
  answer: {
694
739
  ...(asString(meta.title).trim() ? { title: asString(meta.title).trim() } : {}),
695
740
  instruction: asString(meta.instruction, 'Look and complete.'),
696
- ...(asString(meta.imageUrl).trim() ? { imageUrl: asString(meta.imageUrl).trim() } : {}),
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
+ : {}),
697
746
  wordBank: asArray(meta.wordBank).map((word) => asString(word)).filter(Boolean),
698
747
  items: mappedItems,
699
748
  explanation: asString(apiQuestion.explanation),
@@ -741,6 +790,7 @@ const handlers = {
741
790
  ANSWER_THE_QUESTION_GROUP: reverseAnswerTheQuestionGroup,
742
791
  TRUE_FALSE_GROUP: reverseTrueFalseGroup,
743
792
  TRUE_FALSE_CORRECT_GROUP: reverseTrueFalseCorrectGroup,
793
+ CHOOSE_THEN_ANSWER_GROUP: reverseChooseThenAnswerGroup,
744
794
  FILL_IN_BLANK_GROUP: reverseFillInBlankGroup,
745
795
  MATCHING_WITH_LINES_GROUP: reverseMatchingWithLinesGroup,
746
796
  CROSS_OUT_WORD_GROUP: reverseCrossOutWordGroup,
@@ -785,6 +835,7 @@ const handlers = {
785
835
  GN_SPEAKING_INTERVIEW: reverseGroupPayload,
786
836
  CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
787
837
  FIND_WORDS_IN_MATRIX: reverseFindWordsInMatrix,
838
+ SORT_WORDS_INTO_CATEGORIES: reverseSortWordsIntoCategories,
788
839
  MATCH_COLUMNS_TO_MAKE_SENTENCES: reverseMatchColumnsToMakeSentences,
789
840
  MATCH_WORD_TO_PICTURE_GROUP: reverseMatchWordToPictureGroup,
790
841
  WORD_ORDERING_GROUP: reverseWordOrderingGroup,
@@ -959,6 +1010,51 @@ function reverseFindWordsInMatrix(apiQuestion) {
959
1010
  },
960
1011
  };
961
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
+ }
962
1058
  export function transformApiQuestionToFrontend(apiQuestion) {
963
1059
  const questionType = String(apiQuestion.questionType ?? apiQuestion.question_type ?? apiQuestion.type ?? '');
964
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,
@@ -29,7 +29,7 @@ export declare enum StudentSelectionType {
29
29
  }
30
30
  export type ResultStatus = 'PENDING' | 'PARTIAL_GRADED' | 'GRADED';
31
31
  export type StudentStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'SUBMITTED';
32
- export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'MATCH_COLUMNS_TO_MAKE_SENTENCES' | 'MATCH_WORD_TO_PICTURE_GROUP' | 'WORD_ORDERING_GROUP';
32
+ export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'SORT_WORDS_INTO_CATEGORIES' | 'CHOOSE_THEN_ANSWER_GROUP' | 'MATCH_COLUMNS_TO_MAKE_SENTENCES' | 'MATCH_WORD_TO_PICTURE_GROUP' | 'WORD_ORDERING_GROUP';
33
33
  export type Level = 'Pre-A1' | 'A1' | 'A2' | 'B1' | 'B2' | 'C1' | 'C2';
34
34
  export type Difficulty = 'EASY' | 'MEDIUM' | 'HARD';
35
35
  export type Skill = 'LISTENING' | 'READING' | 'WRITING' | 'SPEAKING';