@tinyweb_dev/oe-exam-sdk 0.2.13 → 1.0.0

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 (99) hide show
  1. package/README.md +12 -0
  2. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  3. package/dist/components/exams/take/components/QuestionRenderer.js +13 -3
  4. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts +14 -0
  5. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +32 -0
  6. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +1 -1
  7. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +10 -2
  8. package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.d.ts +16 -0
  9. package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
  10. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  11. package/dist/components/exams/take/components/question-renderers/index.js +2 -0
  12. package/dist/components/exams/take/utils/answer-transformers.js +34 -1
  13. package/dist/components/exams/take/utils/question-transformers.d.ts +42 -3
  14. package/dist/components/exams/take/utils/question-transformers.js +71 -1
  15. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  16. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +5 -1
  17. package/dist/components/questions/_shared/types/answer-the-question-group.type.js +7 -0
  18. package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
  19. package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
  20. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
  21. package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
  22. package/dist/components/questions/_shared/types/index.d.ts +2 -0
  23. package/dist/components/questions/_shared/types/index.js +2 -0
  24. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +9 -3
  25. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
  26. package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
  27. package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
  28. package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
  29. package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
  30. package/dist/components/questions/creator/question-type-registry.js +4 -0
  31. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
  32. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
  33. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
  34. package/dist/components/questions/types/answer-the-question/transform.js +9 -3
  35. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +42 -9
  36. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -5
  37. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +18 -4
  38. package/dist/components/questions/types/answer-the-question-group/transform.js +8 -2
  39. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -2
  40. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
  41. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
  42. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
  43. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
  44. package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
  45. package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
  46. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
  47. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
  48. package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
  49. package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
  50. package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
  51. package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -0
  52. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +39 -10
  53. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +15 -8
  54. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +8 -3
  55. package/dist/components/questions/types/match-word-to-picture-group/transform.js +21 -13
  56. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
  57. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
  58. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
  59. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
  60. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
  61. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
  62. package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
  63. package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
  64. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
  65. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
  66. package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
  67. package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
  68. package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
  69. package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
  70. package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
  71. package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
  72. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
  73. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
  74. package/dist/components/questions/viewer/QuestionViewer.js +13 -2
  75. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
  76. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
  77. package/dist/components/questions/viewer/index.d.ts +3 -0
  78. package/dist/components/questions/viewer/index.js +3 -0
  79. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
  80. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
  81. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  82. package/dist/components/results/renderers/review-question-body-movers.js +24 -2
  83. package/dist/components/results/review-data.js +28 -0
  84. package/dist/index.d.ts +1 -1
  85. package/dist/index.js +1 -1
  86. package/dist/shared/constants/question-skills.js +4 -0
  87. package/dist/shared/lib/utils/question-reverse-transform.js +98 -2
  88. package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
  89. package/dist/shared/lib/utils/question-transform.js +4 -0
  90. package/dist/shared/types/common.types.d.ts +1 -1
  91. package/dist/shared/types/questions/answer-the-question-group.d.ts +3 -0
  92. package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
  93. package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
  94. package/dist/shared/types/questions/index.d.ts +2 -0
  95. package/dist/shared/types/questions/index.js +3 -0
  96. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +6 -2
  97. package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
  98. package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
  99. package/package.json +1 -1
package/README.md CHANGED
@@ -419,6 +419,18 @@ npm run dev
419
419
 
420
420
  ## Publish
421
421
 
422
+ CI release uses `.github/workflows/release_sdk.yml` on tags `sdk-v*` with
423
+ **npm Trusted Publishing (OIDC)** (no long-lived `NPM_TOKEN`).
424
+
425
+ Configure Trusted Publisher on npmjs.com for `@tinyweb_dev/oe-exam-sdk`:
426
+
427
+ - Organization or user: `ocean-edu`
428
+ - Repository: `oe-exam`
429
+ - Workflow filename: `release_sdk.yml`
430
+ - Allowed actions: `npm publish`
431
+
432
+ Manual local publish (maintainer + 2FA):
433
+
422
434
  ```bash
423
435
  cd sdk
424
436
  npm version patch
@@ -643,6 +643,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
643
643
  map.set(`${question.id}-item-${index}`, question.id);
644
644
  });
645
645
  }
646
+ if (part.questionType === 'CHOOSE_THEN_ANSWER_GROUP' &&
647
+ Array.isArray(content?.items)) {
648
+ content.items.forEach((_, index) => {
649
+ map.set(`${question.id}-item-${index}`, question.id);
650
+ });
651
+ }
646
652
  });
647
653
  });
648
654
  return map;
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * 2. Transforms data to renderer-specific props
9
9
  * 3. Renders the correct component for each question type
10
10
  */
11
- import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
11
+ import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, SortWordsIntoCategoriesRenderer, ChooseThenAnswerGroupRenderer, } from './question-renderers';
12
12
  import { SpeakingQuestionRenderer } from './speaking/renderers';
13
- import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
13
+ import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, transformSortWordsIntoCategories, transformChooseThenAnswerGroup, } from '../utils/question-transformers';
14
14
  import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
15
15
  import { normalizeColoringAssignments } from '../utils/coloring-assignments';
16
16
  export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
@@ -141,6 +141,16 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
141
141
  const data = transformCrosswordPuzzle(questions);
142
142
  return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
143
143
  }
144
+ case 'SORT_WORDS_INTO_CATEGORIES': {
145
+ const data = transformSortWordsIntoCategories(questions);
146
+ const gradableCount = data.words.filter((word) => !word.isExample).length;
147
+ return (_jsx(SortWordsIntoCategoriesRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
148
+ }
149
+ case 'CHOOSE_THEN_ANSWER_GROUP': {
150
+ const data = transformChooseThenAnswerGroup(questions);
151
+ const gradableCount = data.questions.filter((item) => !item.isExample).length;
152
+ return (_jsx(ChooseThenAnswerGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
153
+ }
144
154
  case 'FIND_WORDS_IN_MATRIX': {
145
155
  const data = transformFindWordsInMatrix(questions);
146
156
  return (_jsx(FindWordsInMatrixRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, type: data.type, title: data.title, gridSize: data.gridSize, grid: data.grid, categories: data.categories, wordBank: data.wordBank, exampleSelection: data.exampleSelection, correctWords: data.correctWords, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, questionId: questions[0]?.id || 'find-words' }));
@@ -151,7 +161,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
151
161
  }
152
162
  case 'ANSWER_THE_QUESTION_GROUP': {
153
163
  const data = transformAnswerTheQuestionGroup(questions);
154
- return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
164
+ return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
155
165
  }
156
166
  case 'TRUE_FALSE_GROUP': {
157
167
  const data = transformTrueFalseGroup(questions);
@@ -0,0 +1,14 @@
1
+ import type { ChooseThenAnswerGroupPartData } from '../../utils/question-transformers';
2
+ import type { ChooseThenAnswerStudentItemAnswer } from '../../../../questions/_shared/types/choose-then-answer-group.type';
3
+ interface ChooseThenAnswerGroupRendererProps {
4
+ partNumber: number;
5
+ partName?: string;
6
+ questionCount: number;
7
+ instruction: string;
8
+ data: ChooseThenAnswerGroupPartData;
9
+ answers: Record<string, unknown>;
10
+ onAnswerChange: (questionId: string, value: ChooseThenAnswerStudentItemAnswer) => void;
11
+ isReviewMode?: boolean;
12
+ }
13
+ export declare function ChooseThenAnswerGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: ChooseThenAnswerGroupRendererProps): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
4
+ import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
5
+ import { ChooseThenAnswerGroupClient } from '../../../../questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient';
6
+ import { parseStudentAnswer } from '../../../../questions/types/choose-then-answer-group/map-choose-then-answer-group-data';
7
+ export function ChooseThenAnswerGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
8
+ const userAnswers = data.questions.map((item) => {
9
+ if (item.isExample) {
10
+ return { optionId: item.correctOptionId, text: parseStudentAnswer(answers[item.id]).text };
11
+ }
12
+ return parseStudentAnswer(answers[item.id]);
13
+ });
14
+ return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(ChooseThenAnswerGroupClient, { questionData: {
15
+ title: data.title,
16
+ instruction: data.instruction,
17
+ items: data.questions.map((item) => ({
18
+ question: item.question,
19
+ options: item.options,
20
+ optionId: item.correctOptionId,
21
+ isExample: item.isExample,
22
+ points: item.points,
23
+ questionNumber: item.questionNumber,
24
+ })),
25
+ explanation: data.explanation,
26
+ }, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: isReviewMode ? undefined : (index, next) => {
27
+ const item = data.questions[index];
28
+ if (!item || item.isExample)
29
+ return;
30
+ onAnswerChange(item.id, next);
31
+ } }) })] }));
32
+ }
@@ -8,7 +8,7 @@ interface MoversAnswerTheQuestionRendererProps {
8
8
  partName?: string;
9
9
  questionCount: number;
10
10
  instruction: string;
11
- imageUrl?: string;
11
+ imageUrl?: string | string[];
12
12
  audioUrl?: string;
13
13
  title?: string;
14
14
  groupContent?: string;
@@ -7,6 +7,14 @@ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
7
7
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
8
8
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
9
9
  import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
10
+ import { toAnswerTheQuestionImageUrls } from '../../../../questions/_shared/types/answer-the-question.type';
11
+ function QuestionImages({ imageUrl, alt, className, }) {
12
+ const urls = toAnswerTheQuestionImageUrls(imageUrl);
13
+ if (urls.length === 0) {
14
+ return null;
15
+ }
16
+ 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: className }, `${alt}-${imageIndex}`))) }));
17
+ }
10
18
  // Single Question Component
11
19
  function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswerTextMap, id, }) {
12
20
  const isExampleQ = question.isExample;
@@ -17,7 +25,7 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
17
25
  const hasCorrectAnswer = isReviewMode && correctAnswer !== undefined;
18
26
  const isCorrect = hasCorrectAnswer && userAnswer.trim().toLowerCase() === correctAnswer.trim().toLowerCase();
19
27
  const isWrong = hasCorrectAnswer && !!userAnswer && !isCorrect;
20
- return (_jsxs("div", { id: id, className: `flex items-start gap-3 rounded-lg p-3 ${isExampleQ ? 'border-2 border-orange-200 bg-orange-50' : ''}`, children: [_jsx("span", { className: `flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-sm font-bold text-white ${isExampleQ ? 'bg-orange-400' : isCorrect ? 'bg-green-500' : isWrong ? 'bg-red-500' : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [question.imageUrl && (_jsx(ResolvedImage, { src: question.imageUrl, alt: `Q${question.questionNumber}`, className: "max-h-[120px] rounded-lg object-contain" })), _jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [_jsx("span", { className: "font-medium text-gray-800", children: question.question }), question.prefixText && _jsx("span", { className: "text-gray-600", children: question.prefixText }), _jsx("input", { type: "text", value: displayValue, onChange: (e) => onAnswerChange(question.id, e.target.value), disabled: isDisabled, placeholder: isExampleQ ? '' : 'Type your answer...', className: `min-w-[140px] flex-1 border-b-2 bg-transparent px-1 py-1 text-center outline-none focus:border-blue-500 disabled:cursor-default ${isExampleQ ? 'border-orange-400 font-semibold text-orange-700 disabled:opacity-100' :
28
+ return (_jsxs("div", { id: id, className: `flex items-start gap-3 rounded-lg p-3 ${isExampleQ ? 'border-2 border-orange-200 bg-orange-50' : ''}`, children: [_jsx("span", { className: `flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-sm font-bold text-white ${isExampleQ ? 'bg-orange-400' : isCorrect ? 'bg-green-500' : isWrong ? 'bg-red-500' : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [_jsx(QuestionImages, { imageUrl: question.imageUrl, alt: `Q${question.questionNumber}`, className: "max-h-[120px] rounded-lg object-contain" }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [_jsx("span", { className: "font-medium text-gray-800", children: question.question }), question.prefixText && _jsx("span", { className: "text-gray-600", children: question.prefixText }), _jsx("input", { type: "text", value: displayValue, onChange: (e) => onAnswerChange(question.id, e.target.value), disabled: isDisabled, placeholder: isExampleQ ? '' : 'Type your answer...', className: `min-w-[140px] flex-1 border-b-2 bg-transparent px-1 py-1 text-center outline-none focus:border-blue-500 disabled:cursor-default ${isExampleQ ? 'border-orange-400 font-semibold text-orange-700 disabled:opacity-100' :
21
29
  isCorrect ? 'border-green-500 text-green-700' :
22
30
  isWrong ? 'border-red-500 text-red-700' :
23
31
  'border-gray-400 disabled:opacity-60'}` }), question.suffixText && _jsx("span", { className: "text-gray-600", children: question.suffixText }), isExampleQ && (_jsx("span", { className: "rounded bg-orange-100 px-2 py-0.5 text-xs font-semibold text-orange-600", children: "Example" }))] }), !isExampleQ && isWrong && hasCorrectAnswer && (_jsxs("p", { className: "text-sm text-green-600", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", correctAnswer] })), !isExampleQ && hasCorrectAnswer && !userAnswer && (_jsxs("p", { className: "text-sm text-green-600", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", correctAnswer] })), question.suggestedAnswers && question.suggestedAnswers.length > 0 && !isDisabled && (_jsx("div", { className: "flex flex-wrap gap-2", children: question.suggestedAnswers.map((sa, idx) => (_jsx("button", { type: "button", onClick: () => onAnswerChange(question.id, sa), className: "rounded-full border border-blue-300 bg-blue-50 px-3 py-1 text-sm text-blue-700 transition-colors hover:bg-blue-100", children: sa }, idx))) }))] })] }));
@@ -26,5 +34,5 @@ export function MoversAnswerTheQuestionRenderer({ partNumber, partName, question
26
34
  // Separate example questions from regular questions
27
35
  const exampleQuestions = questions.filter((q) => q.isExample);
28
36
  const regularQuestions = questions.filter((q) => !q.isExample);
29
- return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar", children: [_jsxs("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), audioUrl && (_jsx("div", { className: "relative z-10", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }) })), imageUrl && (_jsx("div", { className: "relative z-0 flex items-center justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Shared illustration", className: "max-h-[450px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsx("div", { className: "rounded-lg bg-amber-50 p-4 text-base leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })), exampleQuestions.length > 0 && (_jsx("div", { className: "flex flex-col gap-4 mt-2", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-slate-200/60 my-1" }) })), _jsx("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5 p-3 sm:px-6 sm:pb-6", children: regularQuestions.map((question) => (_jsx("div", { id: `question-${question.id}`, className: "w-full rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:border-indigo-200 hover:shadow-md", children: _jsx(QuestionItem, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }) }, question.id))) })] }));
37
+ return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar", children: [_jsxs("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), audioUrl && (_jsx("div", { className: "relative z-10", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }) })), toAnswerTheQuestionImageUrls(imageUrl).length > 0 && (_jsx("div", { className: "relative z-0 flex items-center justify-center", children: _jsx(QuestionImages, { imageUrl: imageUrl, alt: "Shared illustration", className: "max-h-[450px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsx("div", { className: "rounded-lg bg-amber-50 p-4 text-base leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })), exampleQuestions.length > 0 && (_jsx("div", { className: "flex flex-col gap-4 mt-2", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-slate-200/60 my-1" }) })), _jsx("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5 p-3 sm:px-6 sm:pb-6", children: regularQuestions.map((question) => (_jsx("div", { id: `question-${question.id}`, className: "w-full rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:border-indigo-200 hover:shadow-md", children: _jsx(QuestionItem, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }) }, question.id))) })] }));
30
38
  }
@@ -0,0 +1,16 @@
1
+ import type { SortWordsIntoCategoriesPartData } from '../../utils/question-transformers';
2
+ import type { SortWordsPlacement } from '../../../../questions/_shared/types/sort-words-into-categories.type';
3
+ interface SortWordsIntoCategoriesRendererProps {
4
+ partNumber: number;
5
+ partName?: string;
6
+ questionCount: number;
7
+ instruction: string;
8
+ data: SortWordsIntoCategoriesPartData;
9
+ answers: Record<string, unknown>;
10
+ onAnswerChange: (questionId: string, value: {
11
+ placements: SortWordsPlacement[];
12
+ }) => void;
13
+ isReviewMode?: boolean;
14
+ }
15
+ export declare function SortWordsIntoCategoriesRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: SortWordsIntoCategoriesRendererProps): import("react").JSX.Element;
16
+ export {};
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
4
+ import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
5
+ import { SortWordsIntoCategoriesClient } from '../../../../questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient';
6
+ import { padPlacements, readPlacements } from '../../../../questions/types/sort-words-into-categories/map-sort-words-into-categories-data';
7
+ export function SortWordsIntoCategoriesRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
8
+ const stored = readPlacements(answers[data.questionId]);
9
+ const exampleSeed = data.words.map((word) => ({
10
+ wordId: word.id,
11
+ categoryId: word.isExample ? word.categoryId : '',
12
+ }));
13
+ const userPlacements = padPlacements(data.words, stored ?? exampleSeed);
14
+ return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(SortWordsIntoCategoriesClient, { questionData: {
15
+ title: data.title,
16
+ instruction: data.instruction,
17
+ categories: data.categories,
18
+ words: data.words,
19
+ explanation: data.explanation,
20
+ }, isReviewMode: isReviewMode, userPlacements: userPlacements, onPlacementsChange: isReviewMode ? undefined : (next) => {
21
+ onAnswerChange(data.questionId, { placements: next });
22
+ } }) })] }));
23
+ }
@@ -25,4 +25,6 @@ export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchG
25
25
  export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
26
26
  export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
27
27
  export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
28
+ export { SortWordsIntoCategoriesRenderer } from './SortWordsIntoCategoriesRenderer';
29
+ export { ChooseThenAnswerGroupRenderer } from './ChooseThenAnswerGroupRenderer';
28
30
  export { ArticlesViewer } from './ArticlesViewer';
@@ -29,4 +29,6 @@ export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchG
29
29
  export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
30
30
  export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
31
31
  export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
32
+ export { SortWordsIntoCategoriesRenderer } from './SortWordsIntoCategoriesRenderer';
33
+ export { ChooseThenAnswerGroupRenderer } from './ChooseThenAnswerGroupRenderer';
32
34
  export { ArticlesViewer } from './ArticlesViewer';
@@ -93,7 +93,8 @@ function findParentQuestion(subQuestionId, parts) {
93
93
  part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
94
94
  part.questionType === 'SPONTANEOUS_QA_GROUP' ||
95
95
  part.questionType === 'ANSWER_THE_QUESTION_GROUP' ||
96
- part.questionType === 'WORD_ORDERING_GROUP') {
96
+ part.questionType === 'WORD_ORDERING_GROUP' ||
97
+ part.questionType === 'CHOOSE_THEN_ANSWER_GROUP') {
97
98
  const parent = part.questions.find((question) => subQuestionId.startsWith(`${question.id}-item-`));
98
99
  if (parent) {
99
100
  return {
@@ -205,7 +206,29 @@ function findQuestionType(questionId, parts) {
205
206
  /**
206
207
  * Wrap answer in proper format for backend grading
207
208
  */
209
+ function normalizeSortWordsIntoCategoriesAnswer(answer) {
210
+ const raw = answer && typeof answer === 'object' && !Array.isArray(answer)
211
+ ? answer.placements
212
+ : Array.isArray(answer)
213
+ ? answer
214
+ : [];
215
+ const source = Array.isArray(raw) ? raw : [];
216
+ return {
217
+ placements: source.map((item) => {
218
+ const record = item && typeof item === 'object'
219
+ ? item
220
+ : {};
221
+ return {
222
+ wordId: typeof record.wordId === 'string' ? record.wordId : '',
223
+ categoryId: typeof record.categoryId === 'string' ? record.categoryId : '',
224
+ };
225
+ }),
226
+ };
227
+ }
208
228
  function wrapAnswer(answer, questionType) {
229
+ if (questionType === 'SORT_WORDS_INTO_CATEGORIES') {
230
+ return normalizeSortWordsIntoCategoriesAnswer(answer);
231
+ }
209
232
  // For single/multiple choice, wrap in { answer: value }
210
233
  if (typeof answer === 'number') {
211
234
  return { answer };
@@ -373,6 +396,16 @@ export function transformAnswersForSubmission(answers, parts) {
373
396
  if (parentType === 'CROSS_OUT_WORD_GROUP') {
374
397
  return normalizeCrossOutWordGroupAnswer(raw);
375
398
  }
399
+ if (parentType === 'CHOOSE_THEN_ANSWER_GROUP') {
400
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
401
+ const obj = raw;
402
+ return {
403
+ optionId: typeof obj.optionId === 'string' ? obj.optionId : '',
404
+ text: typeof obj.text === 'string' ? obj.text : '',
405
+ };
406
+ }
407
+ return { optionId: '', text: '' };
408
+ }
376
409
  if (parentType === 'TRUE_FALSE_CORRECT_GROUP') {
377
410
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
378
411
  const obj = raw;
@@ -5,7 +5,7 @@
5
5
  * Each transformer handles specific question type data structure.
6
6
  */
7
7
  import type { FillBlankQuestion, ChooseBestAnswerQuestion, PictureChooseSubQuestion, PictureFillBlankSubQuestion, ReadingPassageQuestion, LabelThePictureLabel, FillWordHintSubQuestion, ColoringHintItem, ColoringRegion } from '../types';
8
- export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
8
+ export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'SORT_WORDS_INTO_CATEGORIES' | 'CHOOSE_THEN_ANSWER_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
9
9
  export interface ApiQuestion {
10
10
  id: string;
11
11
  content: any;
@@ -233,7 +233,7 @@ export interface AnswerTheQuestionQuestion {
233
233
  id: string;
234
234
  questionNumber: number;
235
235
  question: string;
236
- imageUrl?: string;
236
+ imageUrl?: string | string[];
237
237
  prefixText?: string;
238
238
  suffixText?: string;
239
239
  suggestedAnswers?: string[];
@@ -243,7 +243,7 @@ export interface AnswerTheQuestionQuestion {
243
243
  export declare function transformAnswerTheQuestion(questions: ApiQuestion[]): {
244
244
  questions: AnswerTheQuestionQuestion[];
245
245
  instruction: string;
246
- imageUrl?: string;
246
+ imageUrl?: string | string[];
247
247
  title?: string;
248
248
  groupContent?: string;
249
249
  };
@@ -251,6 +251,7 @@ export declare function transformAnswerTheQuestionGroup(questions: ApiQuestion[]
251
251
  questions: AnswerTheQuestionQuestion[];
252
252
  instruction: string;
253
253
  groupContent?: string;
254
+ imageUrl?: string | string[];
254
255
  };
255
256
  export interface TrueFalseQuestion {
256
257
  id: string;
@@ -532,6 +533,44 @@ export interface FindWordsInMatrixPartData {
532
533
  explanation?: string;
533
534
  }
534
535
  export declare function transformFindWordsInMatrix(questions: ApiQuestion[]): FindWordsInMatrixPartData;
536
+ export interface SortWordsIntoCategoriesPartData {
537
+ questionId: string;
538
+ title?: string;
539
+ instruction: string;
540
+ categories: Array<{
541
+ id: string;
542
+ name: string;
543
+ }>;
544
+ words: Array<{
545
+ id: string;
546
+ text: string;
547
+ categoryId: string;
548
+ isExample?: boolean;
549
+ points: number;
550
+ }>;
551
+ explanation?: string;
552
+ }
553
+ export declare function transformSortWordsIntoCategories(questions: ApiQuestion[]): SortWordsIntoCategoriesPartData;
554
+ export interface ChooseThenAnswerGroupQuestion {
555
+ id: string;
556
+ questionNumber: number;
557
+ question: string;
558
+ options: Array<{
559
+ id: string;
560
+ text: string;
561
+ }>;
562
+ correctOptionId: string;
563
+ isExample: boolean;
564
+ points: number;
565
+ }
566
+ export interface ChooseThenAnswerGroupPartData {
567
+ questionId: string;
568
+ title?: string;
569
+ instruction: string;
570
+ questions: ChooseThenAnswerGroupQuestion[];
571
+ explanation?: string;
572
+ }
573
+ export declare function transformChooseThenAnswerGroup(questions: ApiQuestion[]): ChooseThenAnswerGroupPartData;
535
574
  export interface MatchingWithLinesGroupQuestionItem {
536
575
  id: string;
537
576
  questionNumber: number;
@@ -829,7 +829,8 @@ export function transformAnswerTheQuestion(questions) {
829
829
  return {
830
830
  questions: transformed,
831
831
  instruction: 'ANSWER THE QUESTION',
832
- imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl || firstContent?.imageUrl,
832
+ // Do not lift the first item image here — that duplicates content.imageUrl.
833
+ imageUrl: groupPayload?.mainImageUrl || groupPayload?.imageUrl,
833
834
  title: firstContent?.title,
834
835
  groupContent: groupPayload?.groupContent || firstContent?.groupContent,
835
836
  };
@@ -857,6 +858,7 @@ export function transformAnswerTheQuestionGroup(questions) {
857
858
  id: `${parent.id}-item-${itemIndex}`,
858
859
  questionNumber: item.questionNumber || itemIndex + 1,
859
860
  question: item.content?.question || '',
861
+ imageUrl: item.content?.imageUrl,
860
862
  isExample,
861
863
  exampleAnswer: isExample ? exampleAnswer : undefined,
862
864
  };
@@ -865,6 +867,7 @@ export function transformAnswerTheQuestionGroup(questions) {
865
867
  questions: transformed,
866
868
  instruction: content?.meta?.instruction || 'Read the text again and answer the questions.',
867
869
  groupContent: content?.meta?.passage,
870
+ imageUrl: content?.meta?.imageUrl,
868
871
  };
869
872
  }
870
873
  export function transformTrueFalse(questions) {
@@ -1379,6 +1382,73 @@ export function transformFindWordsInMatrix(questions) {
1379
1382
  explanation: question?.explanation,
1380
1383
  };
1381
1384
  }
1385
+ export function transformSortWordsIntoCategories(questions) {
1386
+ const question = questions[0];
1387
+ const content = (question?.content || {});
1388
+ const correctAnswer = (question?.correct_answer || question?.correctAnswer || {});
1389
+ const placementMap = new Map();
1390
+ (Array.isArray(correctAnswer.placements) ? correctAnswer.placements : []).forEach((item) => {
1391
+ if (item?.wordId && !placementMap.has(item.wordId)) {
1392
+ placementMap.set(item.wordId, item.categoryId || '');
1393
+ }
1394
+ });
1395
+ const words = (Array.isArray(content.words) ? content.words : []).map((item, index) => {
1396
+ const id = item?.id || `w${index + 1}`;
1397
+ const isExample = item?.isExample === true;
1398
+ return {
1399
+ id,
1400
+ text: item?.text || '',
1401
+ categoryId: placementMap.get(id) || '',
1402
+ isExample,
1403
+ points: isExample ? 0 : (typeof item?.points === 'number' ? item.points : 1),
1404
+ };
1405
+ });
1406
+ return {
1407
+ questionId: question?.id || 'sort-words',
1408
+ title: content.meta?.title || '',
1409
+ instruction: question?.instruction ||
1410
+ content.meta?.instruction ||
1411
+ 'Read and sort the words.',
1412
+ categories: (Array.isArray(content.categories) ? content.categories : []).map((item, index) => ({
1413
+ id: item?.id || `cat_${index + 1}`,
1414
+ name: item?.name || '',
1415
+ })),
1416
+ words,
1417
+ explanation: question?.explanation,
1418
+ };
1419
+ }
1420
+ export function transformChooseThenAnswerGroup(questions) {
1421
+ const parent = questions[0];
1422
+ const content = (parent?.content || {});
1423
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
1424
+ ? parent.correct_answer.answer
1425
+ : [];
1426
+ const items = Array.isArray(content.items) ? content.items : [];
1427
+ return {
1428
+ questionId: parent?.id || 'choose-then-answer',
1429
+ title: content.meta?.title || '',
1430
+ instruction: parent?.instruction ||
1431
+ content.meta?.instruction ||
1432
+ 'Read and choose. Then answer for you.',
1433
+ questions: items.map((item, index) => {
1434
+ const fallback = parentCorrect[index];
1435
+ const optionId = item.correctAnswer?.answer?.optionId || fallback?.optionId || '';
1436
+ return {
1437
+ id: `${parent?.id || 'choose-then-answer'}-item-${index}`,
1438
+ questionNumber: item.questionNumber || index + 1,
1439
+ question: item.content?.question || '',
1440
+ options: (Array.isArray(item.content?.options) ? item.content.options : []).map((option, optionIndex) => ({
1441
+ id: option?.id || `opt-${optionIndex + 1}`,
1442
+ text: option?.text || '',
1443
+ })),
1444
+ correctOptionId: optionId,
1445
+ isExample: item.isExample === true,
1446
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
1447
+ };
1448
+ }),
1449
+ explanation: parent?.explanation,
1450
+ };
1451
+ }
1382
1452
  export function transformMatchingWithLinesGroup(questions) {
1383
1453
  const parent = questions[0];
1384
1454
  const content = (parent?.content || {});
@@ -347,6 +347,16 @@ export const QUESTION_TYPE_CONFIG = {
347
347
  requiresContent: true,
348
348
  hasDedicatedComponent: true,
349
349
  },
350
+ SORT_WORDS_INTO_CATEGORIES: {
351
+ label: 'Sắp xếp từ vào nhóm',
352
+ requiresContent: true,
353
+ hasDedicatedComponent: true,
354
+ },
355
+ CHOOSE_THEN_ANSWER_GROUP: {
356
+ label: 'Chọn đáp án rồi trả lời',
357
+ requiresContent: true,
358
+ hasDedicatedComponent: true,
359
+ },
350
360
  MATCH_COLUMNS_TO_MAKE_SENTENCES: {
351
361
  label: 'Ghép cột thành câu',
352
362
  requiresContent: true,
@@ -413,6 +423,8 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
413
423
  'WORD_FILL_STRUCTURED_FORM',
414
424
  'CROSSWORD_PUZZLE',
415
425
  'FIND_WORDS_IN_MATRIX',
426
+ 'SORT_WORDS_INTO_CATEGORIES',
427
+ 'CHOOSE_THEN_ANSWER_GROUP',
416
428
  'MATCH_COLUMNS_TO_MAKE_SENTENCES',
417
429
  'MATCH_WORD_TO_PICTURE_GROUP',
418
430
  'WORD_ORDERING_GROUP',
@@ -5,13 +5,17 @@ export interface AnswerTheQuestionGroupItemData {
5
5
  isExample?: boolean;
6
6
  points: number;
7
7
  questionNumber: number;
8
+ imageUrl?: string | string[];
8
9
  }
9
10
  export declare function toAnswerTheQuestionGroupImageUrls(value?: unknown): string[];
11
+ export declare function fromAnswerTheQuestionGroupImageUrls(value?: unknown): string | string[] | undefined;
10
12
  export interface AnswerTheQuestionGroupData {
11
13
  instruction: string;
12
14
  passage?: string;
15
+ /** Group-level listening audio (one player for the whole group). */
16
+ audioUrl?: string;
13
17
  /** Shared images above the question list (1 or many). */
14
- imageUrl?: string[];
18
+ imageUrl?: string | string[];
15
19
  items: AnswerTheQuestionGroupItemData[];
16
20
  explanation?: string;
17
21
  points?: number;
@@ -10,3 +10,10 @@ export function toAnswerTheQuestionGroupImageUrls(value) {
10
10
  }
11
11
  return [];
12
12
  }
13
+ export function fromAnswerTheQuestionGroupImageUrls(value) {
14
+ const urls = toAnswerTheQuestionGroupImageUrls(value);
15
+ if (urls.length === 0) {
16
+ return undefined;
17
+ }
18
+ return urls.length === 1 ? urls[0] : urls;
19
+ }
@@ -23,13 +23,15 @@ export interface AnswerTheQuestionData {
23
23
  groupNumber?: number;
24
24
  isExample?: boolean;
25
25
  suggestedAnswers?: string[];
26
- imageUrl: string;
26
+ imageUrl?: string | string[];
27
27
  title?: string;
28
28
  subtitle?: string;
29
29
  groupContent?: string;
30
30
  groups?: AnswerTheQuestionGroup[];
31
31
  explanation?: string;
32
32
  }
33
+ export declare function toAnswerTheQuestionImageUrls(value?: unknown): string[];
34
+ export declare function fromAnswerTheQuestionImageUrls(value?: unknown): string | string[] | undefined;
33
35
  import type { ValidationRef } from './look-and-choose-answer.type';
34
36
  export type { ValidationRef };
35
37
  export interface AnswerTheQuestionCreatorProps {
@@ -1 +1,19 @@
1
- export {};
1
+ export function toAnswerTheQuestionImageUrls(value) {
2
+ if (Array.isArray(value)) {
3
+ return value
4
+ .filter((url) => typeof url === 'string')
5
+ .map((url) => url.trim())
6
+ .filter((url) => url !== '');
7
+ }
8
+ if (typeof value === 'string' && value.trim() !== '') {
9
+ return [value.trim()];
10
+ }
11
+ return [];
12
+ }
13
+ export function fromAnswerTheQuestionImageUrls(value) {
14
+ const urls = toAnswerTheQuestionImageUrls(value);
15
+ if (urls.length === 0) {
16
+ return undefined;
17
+ }
18
+ return urls.length === 1 ? urls[0] : urls;
19
+ }
@@ -0,0 +1,47 @@
1
+ import type { MutableRefObject } from 'react';
2
+ export declare const CHOOSE_THEN_ANSWER_ITEM_TYPE: "CHOOSE_THEN_ANSWER";
3
+ export declare const CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION = "Read and choose. Then answer for you.";
4
+ export interface ChooseThenAnswerOption {
5
+ id: string;
6
+ text: string;
7
+ }
8
+ export interface ChooseThenAnswerItemAnswer {
9
+ optionId: string;
10
+ }
11
+ export interface ChooseThenAnswerStudentItemAnswer {
12
+ optionId: string;
13
+ text: string;
14
+ }
15
+ export interface ChooseThenAnswerGroupItemData {
16
+ question: string;
17
+ options: ChooseThenAnswerOption[];
18
+ optionId: string;
19
+ isExample?: boolean;
20
+ points: number;
21
+ questionNumber: number;
22
+ }
23
+ export interface ChooseThenAnswerGroupData {
24
+ title?: string;
25
+ instruction: string;
26
+ items: ChooseThenAnswerGroupItemData[];
27
+ explanation?: string;
28
+ points?: number;
29
+ }
30
+ export interface ChooseThenAnswerGroupCreatorProps {
31
+ initialData?: ChooseThenAnswerGroupData;
32
+ onSave?: (data: ChooseThenAnswerGroupData) => void;
33
+ onCancel?: () => void;
34
+ onChange?: (data: ChooseThenAnswerGroupData) => void;
35
+ externalErrors?: string[];
36
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
37
+ validationRef?: MutableRefObject<{
38
+ triggerValidation: () => Promise<boolean>;
39
+ getFormData?: () => ChooseThenAnswerGroupData;
40
+ } | null>;
41
+ }
42
+ export interface ChooseThenAnswerGroupClientProps {
43
+ questionData: ChooseThenAnswerGroupData;
44
+ isReviewMode?: boolean;
45
+ userAnswers?: ChooseThenAnswerStudentItemAnswer[];
46
+ onAnswerChange?: (index: number, next: ChooseThenAnswerStudentItemAnswer) => void;
47
+ }
@@ -0,0 +1,2 @@
1
+ export const CHOOSE_THEN_ANSWER_ITEM_TYPE = 'CHOOSE_THEN_ANSWER';
2
+ export const CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION = 'Read and choose. Then answer for you.';