@tinyweb_dev/oe-exam-sdk 0.2.3 → 0.2.5

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 (106) hide show
  1. package/dist/components/exams/take/components/QuestionRenderer.js +11 -3
  2. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
  3. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
  4. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
  5. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
  6. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
  7. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
  8. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
  9. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
  10. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
  11. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  12. package/dist/components/exams/take/components/question-renderers/index.js +2 -0
  13. package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
  14. package/dist/components/exams/take/exam-taking.utils.js +9 -1
  15. package/dist/components/exams/take/types.d.ts +3 -2
  16. package/dist/components/exams/take/utils/answer-transformers.js +12 -0
  17. package/dist/components/exams/take/utils/question-transformers.d.ts +29 -1
  18. package/dist/components/exams/take/utils/question-transformers.js +98 -0
  19. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  20. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
  21. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
  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.type.d.ts +62 -0
  25. package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
  26. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
  27. package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
  28. package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
  29. package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
  30. package/dist/components/questions/components/QuestionTypeSelector.js +1 -0
  31. package/dist/components/questions/creator/question-type-registry.js +4 -0
  32. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
  33. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
  34. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
  35. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
  36. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
  37. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
  38. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
  39. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  40. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
  41. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
  42. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
  43. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
  44. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
  45. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
  46. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
  47. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
  48. package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
  49. package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
  50. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
  51. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
  52. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
  53. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
  54. package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
  55. package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
  56. package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
  57. package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
  58. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
  59. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
  60. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
  61. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
  62. package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
  63. package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
  64. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
  65. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
  66. package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
  67. package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
  68. package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
  69. package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
  70. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +27 -3
  71. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
  72. package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
  73. package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
  74. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +8 -0
  75. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +11 -0
  76. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
  77. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
  78. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
  79. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
  80. package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
  81. package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
  82. package/dist/components/questions/viewer/QuestionViewer.js +58 -9
  83. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
  84. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
  85. package/dist/components/questions/viewer/index.d.ts +4 -0
  86. package/dist/components/questions/viewer/index.js +4 -0
  87. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
  88. package/dist/components/results/renderers/review-question-body-movers.js +39 -3
  89. package/dist/components/results/review-data.js +22 -0
  90. package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
  91. package/dist/index.d.ts +1 -1
  92. package/dist/index.js +1 -1
  93. package/dist/shared/constants/question-skills.js +4 -0
  94. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
  95. package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
  96. package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
  97. package/dist/shared/lib/utils/question-transform.js +4 -0
  98. package/dist/shared/types/common.types.d.ts +1 -1
  99. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  100. package/dist/shared/types/questions/index.d.ts +2 -0
  101. package/dist/shared/types/questions/index.js +4 -0
  102. package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
  103. package/dist/shared/types/questions/match-word-to-picture.js +5 -0
  104. package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
  105. package/dist/shared/types/questions/true-false-correct-group.js +1 -0
  106. package/package.json +7 -2
@@ -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, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
11
+ import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
12
12
  import { SpeakingQuestionRenderer } from './speaking/renderers';
13
- import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, 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, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
14
14
  import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
15
15
  import { normalizeColoringAssignments } from '../utils/coloring-assignments';
16
16
  export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
@@ -66,7 +66,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
66
66
  const firstQuestion = questions[0];
67
67
  const data = transformWriteShortLetter(firstQuestion);
68
68
  const writingAnswer = answers[firstQuestion.id] || '';
69
- return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: (value) => onAnswerChange(firstQuestion.id, value), isReviewMode: isReviewMode, guideImages: data.guideImages }));
69
+ return (_jsx(MoversWritingRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions, prompt: data.prompt, requirements: data.requirements, minWords: data.minWords, maxWords: data.maxWords, answer: writingAnswer, onAnswerChange: (value) => onAnswerChange(firstQuestion.id, value), isReviewMode: isReviewMode, guideImages: data.guideImages, passage: data.passage }));
70
70
  }
71
71
  case 'READ_AND_COLOR_OBJECTS': {
72
72
  const data = transformColorObjects(questions);
@@ -157,6 +157,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
157
157
  const data = transformTrueFalseGroup(questions);
158
158
  return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
159
159
  }
160
+ case 'TRUE_FALSE_CORRECT_GROUP': {
161
+ const data = transformTrueFalseCorrectGroup(questions);
162
+ return (_jsx(MoversTrueFalseCorrectGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
163
+ }
160
164
  case 'CROSS_OUT_WORD_GROUP': {
161
165
  const data = transformCrossOutWordGroup(questions);
162
166
  return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
@@ -190,6 +194,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
190
194
  const data = transformMatchByWritingAnswer(questions);
191
195
  return (_jsx(MoversMatchByWritingAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: part.audioUrl, options: data.options, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, viewMode: data.viewMode, groupImageUrl: data.groupImageUrl }));
192
196
  }
197
+ case 'MATCH_WORD_TO_PICTURE': {
198
+ const data = transformMatchWordToPicture(questions);
199
+ return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, wordBank: data.wordBank, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
200
+ }
193
201
  case 'WORD_FILL_STRUCTURED_FORM': {
194
202
  const data = transformWordFillStructuredForm(questions);
195
203
  return (_jsx(MoversWordFillStructuredFormRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, groupTitle: data.groupTitle, groupContent: data.groupContent, groupImageUrl: data.groupImageUrl, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
@@ -0,0 +1,10 @@
1
+ export declare function getChooseBestAnswerOptionImageSrc(option: {
2
+ imageUrl?: string;
3
+ text?: string;
4
+ }): string;
5
+ export declare function toChooseBestAnswerImageUrls(value?: string | string[]): string[];
6
+ export declare function ChooseBestAnswerImages({ imageUrl, className, imageClassName, }: {
7
+ imageUrl?: string | string[];
8
+ className?: string;
9
+ imageClassName?: string;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { ZoomIn } from 'lucide-react';
4
+ import { useState } from 'react';
5
+ import { cn } from '../../../../../shared/lib/utils';
6
+ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
7
+ export function getChooseBestAnswerOptionImageSrc(option) {
8
+ if (typeof option.imageUrl === 'string' && option.imageUrl.trim() !== '') {
9
+ return option.imageUrl.trim();
10
+ }
11
+ if (typeof option.text === 'string' && option.text.trim() !== '') {
12
+ return option.text.trim();
13
+ }
14
+ return '';
15
+ }
16
+ export function toChooseBestAnswerImageUrls(value) {
17
+ if (Array.isArray(value)) {
18
+ return value
19
+ .filter((url) => typeof url === 'string')
20
+ .map((url) => url.trim())
21
+ .filter((url) => url !== '');
22
+ }
23
+ if (typeof value === 'string' && value.trim() !== '') {
24
+ return [value.trim()];
25
+ }
26
+ return [];
27
+ }
28
+ function QuestionImage({ imageUrl, imageClassName, }) {
29
+ const [isZoomed, setIsZoomed] = useState(false);
30
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative flex items-center justify-center", children: [_jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: cn('cursor-pointer rounded-lg object-contain shadow-md', imageClassName ?? 'max-h-[250px]'), onClick: () => setIsZoomed(true) }), _jsx("button", { type: "button", onClick: () => setIsZoomed(true), className: "absolute right-2 top-2 rounded-full bg-white/80 p-1.5 shadow-md hover:bg-white", "aria-label": "Zoom image", children: _jsx(ZoomIn, { className: "h-4 w-4 text-gray-600" }) })] }), isZoomed && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70", onClick: () => setIsZoomed(false), children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration (zoomed)", className: "max-h-[90vh] max-w-[90vw] rounded-lg object-contain", showLoadingState: false }) }))] }));
31
+ }
32
+ export function ChooseBestAnswerImages({ imageUrl, className, imageClassName, }) {
33
+ const urls = toChooseBestAnswerImageUrls(imageUrl);
34
+ if (urls.length === 0) {
35
+ return null;
36
+ }
37
+ return (_jsx("div", { className: cn('flex flex-wrap justify-center gap-3', className), children: urls.map((url, index) => (_jsx(QuestionImage, { imageUrl: url, imageClassName: imageClassName }, `${url}-${index}`))) }));
38
+ }
@@ -1,12 +1,11 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { ZoomIn } from 'lucide-react';
4
- import { useState } from 'react';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
3
  import { cn } from '../../../../../shared/lib/utils';
6
4
  import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
7
5
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
8
6
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
9
7
  import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
8
+ import { ChooseBestAnswerImages, getChooseBestAnswerOptionImageSrc } from './ChooseBestAnswerImages';
10
9
  function normalizeRenderableText(value) {
11
10
  if (typeof value === 'string') {
12
11
  return value;
@@ -41,16 +40,11 @@ function normalizeRenderableText(value) {
41
40
  }
42
41
  return '';
43
42
  }
44
- // Image with zoom functionality
45
- function QuestionImage({ imageUrl }) {
46
- const [isZoomed, setIsZoomed] = useState(false);
47
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative flex items-center justify-center", children: [_jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: "max-h-[250px] cursor-pointer rounded-lg object-contain shadow-md", onClick: () => setIsZoomed(true) }), _jsx("button", { type: "button", onClick: () => setIsZoomed(true), className: "absolute right-2 top-2 rounded-full bg-white/80 p-1.5 shadow-md hover:bg-white", "aria-label": "Zoom image", children: _jsx(ZoomIn, { className: "h-4 w-4 text-gray-600" }) })] }), isZoomed && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70", onClick: () => setIsZoomed(false), children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration (zoomed)", className: "max-h-[90vh] max-w-[90vw] rounded-lg object-contain", showLoadingState: false }) }))] }));
48
- }
49
43
  // Example Section Component
50
44
  function ExampleSection({ example }) {
51
- return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [example.questionNumber, ". ", normalizeRenderableText(example.questionText)] }), example.imageUrl && (_jsx("div", { className: "mb-3", children: _jsx(QuestionImage, { imageUrl: example.imageUrl }) })), example.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${example.options.length}, minmax(0, 1fr))` }, children: example.options.map((option) => {
45
+ return (_jsxs("div", { className: "rounded-lg border-2 border-orange-300 bg-white p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-gray-700", children: "Example:" }), _jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [example.questionNumber, ". ", normalizeRenderableText(example.questionText)] }), _jsx(ChooseBestAnswerImages, { imageUrl: example.imageUrl, className: "mb-3" }), example.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${example.options.length}, minmax(0, 1fr))` }, children: example.options.map((option) => {
52
46
  const isCorrect = option.id === example.correctOptionId;
53
- return (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: option.text, alt: `Option ${option.label}`, className: "w-40 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", isCorrect
47
+ return (_jsxs("div", { className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${isCorrect ? 'border-green-500 bg-green-50' : 'border-blue-200 bg-white'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-40 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", isCorrect
54
48
  ? "bg-green-500 border-green-500 text-white"
55
49
  : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id));
56
50
  }) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: example.options.map((option) => {
@@ -105,7 +99,7 @@ function QuestionItem({ question, selectedOptionIds, onSelect, isReviewMode, id,
105
99
  }
106
100
  return selectedOptionIds.includes(optionId) || correctOptionIds.includes(optionId);
107
101
  };
108
- return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [question.questionNumber, ". ", normalizeRenderableText(question.questionText)] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), question.imageUrl && (_jsx("div", { className: "mb-3", children: _jsx(QuestionImage, { imageUrl: question.imageUrl }) })), question.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: option.text, alt: `Option ${option.label}`, className: "w-35 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", showIndicatorMark(option.id)
102
+ return (_jsxs("div", { id: id, className: "mb-6", children: [_jsxs("p", { className: "mb-3 text-[19px] font-bold text-[#001590]", children: [question.questionNumber, ". ", normalizeRenderableText(question.questionText)] }), isMultipleAnswers && (_jsx("p", { className: "mb-3 pl-2 text-sm font-medium text-blue-600", children: "Ch\u1ECDn nhi\u1EC1u \u0111\u00E1p \u00E1n" })), _jsx(ChooseBestAnswerImages, { imageUrl: question.imageUrl, className: "mb-3" }), question.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 px-2", style: { gridTemplateColumns: `repeat(${question.options.length}, minmax(0, 1fr))` }, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `relative flex flex-col items-center gap-1 rounded-[16px] border-[3px] p-4 pt-6 mt-3 transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsx("div", { className: "absolute -top-[14px] left-1/2 -translate-x-1/2 bg-blue-600 text-white w-[28px] h-[28px] rounded-full flex items-center justify-center font-bold text-[15px] shadow-sm", children: option.label }), _jsx(ResolvedImage, { src: getChooseBestAnswerOptionImageSrc(option), alt: `Option ${option.label}`, className: "w-35 rounded object-contain mb-2" }), _jsx("div", { className: cn("w-7 h-7 rounded-full flex items-center justify-center border-[3px] transition-all mt-auto", showIndicatorMark(option.id)
109
103
  ? "bg-blue-600 border-blue-600 text-white"
110
104
  : "border-gray-300 bg-white text-transparent"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }) })] }, option.id))) })) : (_jsx("div", { className: "flex flex-col gap-2 pl-2", children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => !isReviewMode && onSelect(option.id), disabled: isReviewMode, className: `flex items-center gap-3 rounded-lg border-2 px-3 py-2 text-left transition-colors ${getOptionStyle(option.id)} ${isReviewMode ? 'cursor-default' : 'cursor-pointer'}`, children: [_jsxs("span", { className: "text-sm font-medium text-gray-600", children: [option.label, "."] }), _jsx("span", { className: "flex-1 text-sm text-gray-700", children: normalizeRenderableText(option.text) }), _jsx("div", { className: `flex h-5 w-5 items-center justify-center border-2 ${isMultipleAnswers ? 'rounded-md' : 'rounded-full'} ${getIndicatorStyle(option.id)}`, children: showIndicatorMark(option.id) && (_jsx("div", { className: `${isMultipleAnswers ? 'h-2.5 w-2.5 rounded-sm' : 'h-2 w-2 rounded-full'} bg-white` })) })] }, option.id))) }))] }));
111
105
  }
@@ -0,0 +1,16 @@
1
+ import type { MatchWordToPictureQuestion } from '../../utils/question-transformers';
2
+ interface MoversMatchWordToPictureRendererProps {
3
+ partNumber: number;
4
+ partName?: string;
5
+ questionCount: number;
6
+ instruction: string;
7
+ wordBank: string[];
8
+ questions: MatchWordToPictureQuestion[];
9
+ answers: Record<string, string>;
10
+ onAnswerChange: (questionId: string, value: string) => void;
11
+ isReviewMode?: boolean;
12
+ correctAnswerTextMap?: Record<string, string>;
13
+ isCorrectMap?: Record<string, boolean | null>;
14
+ }
15
+ export declare function MoversMatchWordToPictureRenderer({ partNumber, partName, questionCount, instruction, wordBank, questions, answers, onAnswerChange, isReviewMode, correctAnswerTextMap, isCorrectMap, }: MoversMatchWordToPictureRendererProps): import("react").JSX.Element;
16
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MATCH_WORD_TO_PICTURE Renderer
3
+ * Shared word bank chips + picture grid + dropdown (one word used once).
4
+ */
5
+ 'use client';
6
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
7
+ import { Check, ChevronDown, X } from 'lucide-react';
8
+ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
9
+ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
10
+ import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
11
+ function normalizeWord(value) {
12
+ return value.trim().toLowerCase();
13
+ }
14
+ function WordChip({ word, isUsed }) {
15
+ return (_jsx("span", { className: `rounded-full border px-3 py-1 text-sm font-semibold ${isUsed
16
+ ? 'border-emerald-200 bg-emerald-50 text-emerald-700'
17
+ : 'border-violet-200 bg-white text-slate-700'}`, children: word }));
18
+ }
19
+ function AnswerSelector({ value, onChange, disabled, wordBank, usedWords, }) {
20
+ return (_jsxs("div", { className: "group relative min-w-[160px] sm:min-w-[210px]", children: [_jsxs("select", { value: value, onChange: (event) => onChange(event.target.value), disabled: disabled, className: "h-[48px] w-full appearance-none rounded-2xl border border-slate-200 bg-white px-4 pr-11 text-sm font-semibold text-slate-700 shadow-sm outline-none transition-all duration-200 hover:border-violet-300 focus:border-violet-500 focus:ring-4 focus:ring-violet-100/50 disabled:cursor-not-allowed disabled:opacity-60", "aria-label": "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB" }), wordBank.map((word, index) => {
21
+ const isUsedByOther = usedWords.has(normalizeWord(word)) && normalizeWord(word) !== normalizeWord(value);
22
+ return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${index}`));
23
+ })] }), _jsx("div", { className: "pointer-events-none absolute right-2.5 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full bg-slate-100 text-slate-500 transition-colors group-focus-within:bg-violet-100 group-focus-within:text-violet-600", children: _jsx(ChevronDown, { className: "h-3.5 w-3.5", strokeWidth: 2.5 }) })] }));
24
+ }
25
+ function ReviewAnswer({ userAnswer, correctAnswer, status, }) {
26
+ const badgeCls = status === 'correct'
27
+ ? 'border-emerald-200 bg-emerald-50 text-emerald-700'
28
+ : status === 'incorrect'
29
+ ? 'border-red-200 bg-red-50 text-red-700'
30
+ : 'border-slate-200 bg-slate-50 text-slate-700';
31
+ return (_jsxs("div", { className: `flex w-full items-center justify-center gap-2 rounded-xl border px-5 py-2 text-[15px] shadow-sm sm:w-auto ${badgeCls}`, children: [status === 'correct' && (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4 shrink-0", strokeWidth: 3 }), _jsx("span", { className: "font-medium opacity-90", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "ml-1 rounded-md border border-emerald-100 bg-white px-2.5 py-0.5 font-bold shadow-sm", children: userAnswer || 'Trống' })] })), status === 'unanswered' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium opacity-80", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "rounded-md border border-slate-200 bg-white px-2.5 py-0.5 font-bold shadow-sm", children: correctAnswer })] })), status === 'incorrect' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(X, { className: "h-4 w-4 shrink-0", strokeWidth: 3 }), _jsx("span", { className: "font-medium opacity-90", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "rounded-md border border-red-100/50 bg-white/60 px-2 py-0.5 font-semibold line-through decoration-red-400/80 decoration-2 opacity-70", children: userAnswer || 'Trống' }), _jsx("span", { className: "font-bold text-red-400", children: "\u2192" }), _jsx("span", { className: "rounded-md border border-red-200 bg-white px-2.5 py-0.5 font-bold shadow-sm", children: correctAnswer })] }))] }));
32
+ }
33
+ function PictureCard({ question, rawAnswer, isDisabled, isExampleQ, onAnswerChange, isReviewMode, correctAnswer, answerStatus, wordBank, usedWords, }) {
34
+ return (_jsxs("div", { className: [
35
+ 'flex flex-col gap-4 overflow-hidden rounded-[14px] border p-4 shadow-sm transition-all duration-200',
36
+ isExampleQ
37
+ ? 'border-amber-200 bg-gradient-to-br from-amber-50 to-orange-50/50 shadow-amber-100/40'
38
+ : 'border-slate-200/80 bg-white hover:-translate-y-0.5 hover:border-violet-200 hover:shadow-md hover:shadow-violet-100/50',
39
+ ].join(' '), children: [_jsxs("div", { className: "flex items-start gap-3", children: [_jsx("span", { className: `flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-[10px] text-[15px] font-extrabold text-white shadow-sm ${isExampleQ
40
+ ? 'bg-gradient-to-br from-amber-400 to-orange-500'
41
+ : 'bg-gradient-to-br from-violet-500 to-indigo-600'}`, children: question.questionNumber }), isExampleQ && (_jsx("span", { className: "w-fit rounded-full border border-amber-300 bg-amber-100 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-amber-700", children: "Example" }))] }), question.imageUrl && (_jsx("div", { className: "flex w-full justify-center", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: `Q${question.questionNumber}`, className: "h-32 w-auto min-w-[5rem] rounded-xl border border-slate-100 bg-slate-50 object-contain p-1.5 shadow-sm" }) })), _jsx("div", { className: "mt-1 flex w-full items-center justify-end border-t border-slate-100/80 pt-3", children: isReviewMode && !isExampleQ ? (_jsx(ReviewAnswer, { userAnswer: rawAnswer, correctAnswer: correctAnswer, status: answerStatus ?? 'unanswered' })) : (_jsx("div", { className: "w-full sm:w-auto", children: _jsx(AnswerSelector, { value: rawAnswer, onChange: (value) => onAnswerChange(question.id, value), disabled: isDisabled, wordBank: wordBank, usedWords: usedWords }) })) })] }));
42
+ }
43
+ export function MoversMatchWordToPictureRenderer({ partNumber, partName, questionCount, instruction, wordBank, questions, answers, onAnswerChange, isReviewMode = false, correctAnswerTextMap = {}, isCorrectMap = {}, }) {
44
+ const getAnswerStatus = (questionId) => {
45
+ const userAnswer = answers[questionId];
46
+ if (!userAnswer)
47
+ return 'unanswered';
48
+ const isCorrect = isCorrectMap[questionId];
49
+ if (isCorrect === true)
50
+ return 'correct';
51
+ if (isCorrect === false)
52
+ return 'incorrect';
53
+ return 'unanswered';
54
+ };
55
+ const usedWords = new Set();
56
+ Object.values(answers).forEach((value) => {
57
+ if (typeof value === 'string' && value.trim()) {
58
+ usedWords.add(normalizeWord(value));
59
+ }
60
+ });
61
+ questions.forEach((question) => {
62
+ if (question.isExample && question.exampleAnswer) {
63
+ usedWords.add(normalizeWord(question.exampleAnswer));
64
+ }
65
+ });
66
+ const exampleQuestions = questions.filter((question) => question.isExample);
67
+ const actualQuestions = questions.filter((question) => !question.isExample);
68
+ return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-5 overflow-y-auto sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-4 p-3 sm:gap-5 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), wordBank.length > 0 && (_jsxs("div", { className: "rounded-[20px] border border-slate-200/80 bg-gradient-to-br from-white via-white to-slate-50 p-4 shadow-lg shadow-slate-100/70 sm:p-5", children: [_jsx("h3", { className: "mb-4 text-sm font-bold uppercase tracking-[0.18em] text-slate-500", children: "Word bank" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => (_jsx(WordChip, { word: word, isUsed: usedWords.has(normalizeWord(word)) }, `${word}-${index}`))) })] })), exampleQuestions.length > 0 && (_jsx("div", { className: "mt-2 grid grid-cols-1 gap-4 lg:grid-cols-2", children: exampleQuestions.map((question) => {
69
+ const rawAnswer = answers[question.id] || question.exampleAnswer || '';
70
+ return (_jsx("div", { id: `question-${question.id}`, className: "w-full", children: _jsx(PictureCard, { question: question, rawAnswer: rawAnswer, isDisabled: true, isExampleQ: true, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerTextMap[question.id], wordBank: wordBank, usedWords: usedWords }) }, question.id));
71
+ }) }))] }), exampleQuestions.length > 0 && actualQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "my-2 border-t border-slate-200/60" }) })), _jsx("div", { className: "grid grid-cols-1 gap-4 p-3 sm:gap-5 sm:px-6 sm:pb-6 lg:grid-cols-2", children: actualQuestions.map((question) => {
72
+ const rawAnswer = answers[question.id] || '';
73
+ return (_jsx("div", { id: `question-${question.id}`, className: "w-full", children: _jsx(PictureCard, { question: question, rawAnswer: rawAnswer, isDisabled: isReviewMode, isExampleQ: false, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerTextMap[question.id], answerStatus: isReviewMode ? getAnswerStatus(question.id) : undefined, wordBank: wordBank, usedWords: usedWords }) }, question.id));
74
+ }) })] }));
75
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * TRUE_FALSE_CORRECT_GROUP Renderer
3
+ * Tick/Cross each sentence and rewrite false sentences.
4
+ */
5
+ import type { TrueFalseCorrectGroupQuestion, TrueFalseCorrectItemAnswer } from '../../utils/question-transformers';
6
+ interface MoversTrueFalseCorrectGroupRendererProps {
7
+ partNumber: number;
8
+ partName?: string;
9
+ questionCount: number;
10
+ instruction: string;
11
+ questions: TrueFalseCorrectGroupQuestion[];
12
+ answers: Record<string, TrueFalseCorrectItemAnswer | undefined>;
13
+ onAnswerChange: (questionId: string, value: TrueFalseCorrectItemAnswer) => void;
14
+ isReviewMode?: boolean;
15
+ correctAnswerMap?: Record<string, TrueFalseCorrectItemAnswer>;
16
+ }
17
+ export declare function MoversTrueFalseCorrectGroupRenderer({ partNumber, partName, questionCount, instruction, questions, answers, onAnswerChange, isReviewMode, correctAnswerMap, }: MoversTrueFalseCorrectGroupRendererProps): import("react").JSX.Element;
18
+ export {};
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * TRUE_FALSE_CORRECT_GROUP Renderer
4
+ * Tick/Cross each sentence and rewrite false sentences.
5
+ */
6
+ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
7
+ import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
8
+ function normalizeText(value) {
9
+ return value.trim().toLowerCase().replace(/\s+/g, ' ');
10
+ }
11
+ function parseCorrectAnswer(raw) {
12
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
13
+ return undefined;
14
+ }
15
+ const obj = raw;
16
+ if (typeof obj.isTrue !== 'boolean') {
17
+ return undefined;
18
+ }
19
+ return {
20
+ isTrue: obj.isTrue,
21
+ correction: obj.isTrue ? '' : (typeof obj.correction === 'string' ? obj.correction : ''),
22
+ };
23
+ }
24
+ function emptyAnswer() {
25
+ return { isTrue: null, correction: '' };
26
+ }
27
+ function isFullyCorrect(student, correct) {
28
+ if (!student || !correct || student.isTrue === null) {
29
+ return false;
30
+ }
31
+ if (student.isTrue !== correct.isTrue) {
32
+ return false;
33
+ }
34
+ if (correct.isTrue) {
35
+ return true;
36
+ }
37
+ return normalizeText(student.correction) === normalizeText(correct.correction);
38
+ }
39
+ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswer, id, }) {
40
+ const isDisabled = isReviewMode || question.isExample;
41
+ const currentAnswer = question.isExample
42
+ ? (question.exampleCorrectAnswer || emptyAnswer())
43
+ : (answer || emptyAnswer());
44
+ const expected = question.isExample
45
+ ? question.exampleCorrectAnswer
46
+ : parseCorrectAnswer(correctAnswer) || correctAnswer;
47
+ const hasCorrectAnswer = isReviewMode && !!expected && typeof expected.isTrue === 'boolean';
48
+ const isCorrect = hasCorrectAnswer && isFullyCorrect(currentAnswer, expected);
49
+ const isWrong = hasCorrectAnswer && currentAnswer.isTrue !== null && !isCorrect;
50
+ const showCorrection = currentAnswer.isTrue === false || (hasCorrectAnswer && expected?.isTrue === false);
51
+ const setTruth = (isTrue) => {
52
+ onAnswerChange(question.id, {
53
+ isTrue,
54
+ correction: isTrue ? '' : (currentAnswer.correction || ''),
55
+ });
56
+ };
57
+ return (_jsxs("div", { id: id, className: `w-full rounded-2xl border p-4 sm:p-4.5 transition-all duration-200 ${question.isExample
58
+ ? 'border-amber-200 bg-gradient-to-r from-amber-50/70 via-orange-50/30 to-amber-50/50 shadow-sm'
59
+ : isCorrect
60
+ ? 'border-emerald-300 bg-emerald-50/30 shadow-sm'
61
+ : isWrong
62
+ ? 'border-rose-300 bg-rose-50/30 shadow-sm'
63
+ : currentAnswer.isTrue !== null
64
+ ? 'border-blue-200 bg-blue-50/20 shadow-sm'
65
+ : 'border-slate-200/90 bg-white shadow-sm hover:border-blue-200 hover:shadow'}`, children: [_jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-3 sm:gap-4", children: [_jsxs("div", { className: "flex flex-1 items-start sm:items-center gap-3", children: [_jsx("span", { className: `flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-black text-white shadow-sm ${question.isExample
66
+ ? 'bg-gradient-to-br from-amber-400 to-orange-500'
67
+ : isCorrect
68
+ ? 'bg-emerald-500'
69
+ : isWrong
70
+ ? 'bg-rose-500'
71
+ : 'bg-blue-600'}`, children: question.questionNumber }), _jsxs("div", { className: "flex flex-1 flex-col sm:flex-row sm:items-center gap-2 sm:gap-3", children: [question.isExample && (_jsx("span", { className: "self-start sm:self-center rounded-full border border-amber-300 bg-amber-100/90 px-2.5 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800 flex-shrink-0", children: "EXAMPLE" })), _jsx("p", { className: "text-[15.5px] sm:text-[16.5px] font-medium leading-relaxed text-slate-800", children: question.question })] })] }), _jsxs("div", { className: "flex items-center gap-2.5 flex-shrink-0 self-end md:self-center pl-10 md:pl-0", children: [_jsx("button", { type: "button", disabled: isDisabled, onClick: () => setTruth(true), className: `h-9 min-w-[90px] rounded-xl border-2 px-4 text-sm font-bold transition-all flex items-center justify-center gap-1 select-none ${currentAnswer.isTrue === true
72
+ ? isReviewMode && isWrong && !question.isExample
73
+ ? 'border-rose-500 bg-rose-50 text-rose-700'
74
+ : 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm'
75
+ : hasCorrectAnswer && expected?.isTrue === true && !question.isExample
76
+ ? 'border-emerald-500 bg-emerald-50 text-emerald-700 ring-2 ring-emerald-300'
77
+ : 'border-slate-200 bg-slate-50/80 text-slate-700 hover:border-emerald-400 hover:bg-emerald-50/50 hover:text-emerald-700'} ${isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'}`, children: "\u2713 True" }), _jsx("button", { type: "button", disabled: isDisabled, onClick: () => setTruth(false), className: `h-9 min-w-[90px] rounded-xl border-2 px-4 text-sm font-bold transition-all flex items-center justify-center gap-1 select-none ${currentAnswer.isTrue === false
78
+ ? isReviewMode && isWrong && !question.isExample
79
+ ? 'border-rose-500 bg-rose-50 text-rose-700'
80
+ : 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm'
81
+ : hasCorrectAnswer && expected?.isTrue === false && !question.isExample
82
+ ? 'border-emerald-500 bg-emerald-50 text-emerald-700 ring-2 ring-emerald-300'
83
+ : 'border-slate-200 bg-slate-50/80 text-slate-700 hover:border-rose-400 hover:bg-rose-50/50 hover:text-rose-700'} ${isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'}`, children: "\u2717 False" })] })] }), showCorrection && (_jsxs("div", { className: "mt-3 border-t border-slate-100 pt-3", children: [_jsx("label", { className: "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500", children: "Correct the false sentence" }), isDisabled ? (_jsx("p", { className: "rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-800", children: currentAnswer.correction || '(empty)' })) : (_jsx("textarea", { value: currentAnswer.correction, onChange: (event) => onAnswerChange(question.id, {
84
+ isTrue: false,
85
+ correction: event.target.value,
86
+ }), placeholder: "Write the correct sentence...", rows: 2, className: "w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" }))] })), isWrong && hasCorrectAnswer && !question.isExample && (_jsxs("div", { className: "mt-2.5 flex flex-col gap-1 border-t border-slate-100 pt-2 text-xs font-semibold", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-rose-600", children: [_jsxs("span", { children: ["Student: ", currentAnswer.isTrue === null ? '(empty)' : currentAnswer.isTrue ? 'True' : 'False'] }), _jsx("span", { children: "\u2022" }), _jsxs("span", { className: "text-emerald-700", children: ["Correct: ", expected?.isTrue ? 'True' : 'False'] })] }), expected?.isTrue === false && (_jsxs("div", { className: "text-emerald-700", children: ["Correct sentence: ", expected.correction || '(empty)'] }))] }))] }));
87
+ }
88
+ export function MoversTrueFalseCorrectGroupRenderer({ partNumber, partName, questionCount, instruction, questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap = {}, }) {
89
+ const exampleQuestions = questions.filter((question) => question.isExample);
90
+ const regularQuestions = questions.filter((question) => !question.isExample);
91
+ return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-4 overflow-y-auto sm:gap-5 pb-6", children: [_jsxs("div", { className: "flex flex-col gap-3.5 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 }), exampleQuestions.length > 0 && (_jsx("div", { className: "mt-1 flex flex-col gap-3", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap[question.id] }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-dashed border-slate-200" }) })), _jsx("div", { className: "flex flex-col gap-3 px-3 sm:px-6", children: regularQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap[question.id] }, question.id))) })] }));
92
+ }
@@ -11,10 +11,11 @@ interface MoversWritingRendererProps {
11
11
  onAnswerChange: (value: string) => void;
12
12
  isReviewMode?: boolean;
13
13
  guideImages?: string[];
14
+ passage?: string;
14
15
  teacherScore?: number | null;
15
16
  maxScore?: number;
16
17
  teacherFeedback?: string | null;
17
18
  writingAssessment?: WritingAssessment | null;
18
19
  }
19
- export declare function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode, guideImages, teacherScore, maxScore, teacherFeedback, writingAssessment, }: MoversWritingRendererProps): import("react").JSX.Element;
20
+ export declare function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode, guideImages, passage, teacherScore, maxScore, teacherFeedback, writingAssessment, }: MoversWritingRendererProps): import("react").JSX.Element;
20
21
  export {};
@@ -5,14 +5,14 @@ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
5
5
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
6
6
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
7
7
  import { HtmlOrText } from '../../../../../shared/lib/html-content';
8
- export function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode = false, guideImages, teacherScore, maxScore, teacherFeedback, writingAssessment, }) {
8
+ export function MoversWritingRenderer({ partNumber, partName, instruction, prompt, requirements, minWords, maxWords, answer, onAnswerChange, isReviewMode = false, guideImages, passage, teacherScore, maxScore, teacherFeedback, writingAssessment, }) {
9
9
  // Count words by splitting on whitespace and filtering empty strings
10
10
  const wordCount = useMemo(() => {
11
11
  if (!answer || !answer.trim())
12
12
  return 0;
13
13
  return answer.trim().split(/\s+/).filter(Boolean).length;
14
14
  }, [answer]);
15
- return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar relative", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: 1, partName: partName, isReviewMode: isReviewMode }), instruction && _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), _jsx("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-0 sticky top-0 z-20 bg-white/95 backdrop-blur pb-2 shadow-sm border-b border-gray-100", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("div", { className: "rounded-2xl border border-slate-200 bg-white p-5 shadow-sm", children: _jsxs("div", { className: "flex flex-col gap-4 text-base leading-relaxed text-gray-800", children: [_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-lg font-semibold text-slate-900", children: "Writing task" }), _jsx(HtmlOrText, { value: prompt, className: "text-base leading-relaxed text-gray-800" })] }), requirements && requirements.length > 0 && (_jsxs("div", { className: "rounded-xl bg-slate-50 p-4", children: [_jsx("p", { className: "text-sm font-bold uppercase tracking-wide text-slate-700", children: "Include" }), _jsx("ul", { className: "mt-3 space-y-2", children: requirements.map((req, index) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-slate-700", children: [_jsx("span", { className: "mt-1.5 h-2 w-2 rounded-full bg-blue-500" }), _jsx(HtmlOrText, { value: req, as: "span" })] }, index))) })] })), (minWords || maxWords) && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [minWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700", children: ["Minimum: ", minWords, " words"] })), maxWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-sm font-medium text-slate-600", children: ["Suggested max: ", maxWords, " words"] }))] }))] }) }), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-600", children: "Story pictures" }), _jsx("p", { className: "text-xs text-slate-400", children: "Read the sequence from left to right" })] }), _jsx("div", { className: `grid gap-4 ${guideImages.length === 1 ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}`, children: guideImages.map((src, idx) => (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50/80 p-2 shadow-sm", children: [_jsx(ResolvedImage, { src: src, alt: `Picture ${idx + 1}`, className: `w-full rounded-lg bg-white object-contain ${guideImages.length === 1 ? 'max-h-[400px]' : 'aspect-[4/3]'}` }), _jsx("div", { className: "pt-2 text-center text-[13px] font-medium text-slate-500", children: guideImages.length > 1 ? `Picture ${idx + 1}` : 'Reference Image' })] }, idx))) })] }))] }) }), _jsxs("div", { className: "flex flex-col gap-3 p-3 sm:px-6 sm:pb-6", children: [_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-slate-100 px-5 py-3 bg-slate-50/50 rounded-t-2xl", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-500", children: "Your answer" }), _jsx("p", { className: "text-xs text-slate-400", children: "Write clearly and organise your ideas in order." })] }), _jsxs("div", { className: `rounded-full px-3 py-1 text-sm font-semibold ${minWords && wordCount < minWords
15
+ return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar relative", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: 1, partName: partName, isReviewMode: isReviewMode }), instruction && _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), _jsx("div", { className: "flex flex-col gap-4 p-3 sm:px-6 sm:pt-0 sticky top-0 z-20 bg-white/95 backdrop-blur pb-2 shadow-sm border-b border-gray-100", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("div", { className: "rounded-2xl border border-slate-200 bg-white p-5 shadow-sm", children: _jsxs("div", { className: "flex flex-col gap-4 text-base leading-relaxed text-gray-800", children: [_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-lg font-semibold text-slate-900", children: "Writing task" }), _jsx(HtmlOrText, { value: prompt, className: "text-base leading-relaxed text-gray-800" })] }), passage?.trim() && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50/60 p-4", children: [_jsx("p", { className: "text-sm font-bold uppercase tracking-wide text-indigo-800", children: "Read" }), _jsx(HtmlOrText, { value: passage, className: "mt-3 text-sm leading-relaxed text-slate-800" })] })), requirements && requirements.length > 0 && (_jsxs("div", { className: "rounded-xl bg-slate-50 p-4", children: [_jsx("p", { className: "text-sm font-bold uppercase tracking-wide text-slate-700", children: "Include" }), _jsx("ul", { className: "mt-3 space-y-2", children: requirements.map((req, index) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-slate-700", children: [_jsx("span", { className: "mt-1.5 h-2 w-2 rounded-full bg-blue-500" }), _jsx(HtmlOrText, { value: req, as: "span" })] }, index))) })] })), (minWords || maxWords) && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [minWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700", children: ["Minimum: ", minWords, " words"] })), maxWords && (_jsxs("div", { className: "inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-sm font-medium text-slate-600", children: ["Suggested max: ", maxWords, " words"] }))] }))] }) }), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-600", children: "Story pictures" }), _jsx("p", { className: "text-xs text-slate-400", children: "Read the sequence from left to right" })] }), _jsx("div", { className: `grid gap-4 ${guideImages.length === 1 ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}`, children: guideImages.map((src, idx) => (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50/80 p-2 shadow-sm", children: [_jsx(ResolvedImage, { src: src, alt: `Picture ${idx + 1}`, className: `w-full rounded-lg bg-white object-contain ${guideImages.length === 1 ? 'max-h-[400px]' : 'aspect-[4/3]'}` }), _jsx("div", { className: "pt-2 text-center text-[13px] font-medium text-slate-500", children: guideImages.length > 1 ? `Picture ${idx + 1}` : 'Reference Image' })] }, idx))) })] }))] }) }), _jsxs("div", { className: "flex flex-col gap-3 p-3 sm:px-6 sm:pb-6", children: [_jsxs("div", { className: "rounded-2xl border border-slate-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-slate-100 px-5 py-3 bg-slate-50/50 rounded-t-2xl", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-slate-500", children: "Your answer" }), _jsx("p", { className: "text-xs text-slate-400", children: "Write clearly and organise your ideas in order." })] }), _jsxs("div", { className: `rounded-full px-3 py-1 text-sm font-semibold ${minWords && wordCount < minWords
16
16
  ? 'border border-amber-200 bg-amber-50 text-amber-700'
17
17
  : 'border border-emerald-200 bg-emerald-50 text-emerald-700'}`, children: [wordCount, " words"] })] }), _jsxs("div", { className: "p-4", children: [_jsx("textarea", { id: "writing-answer", name: "writing-answer", value: answer, onChange: (e) => {
18
18
  const newValue = e.target.value;
@@ -10,12 +10,14 @@ export { MoversLabelThePictureRenderer } from './MoversLabelThePictureRenderer';
10
10
  export { MoversFillWordHintRenderer } from './MoversFillWordHintRenderer';
11
11
  export { MoversGridFillRenderer } from './MoversGridFillRenderer';
12
12
  export { MoversTrueFalseRenderer } from './MoversTrueFalseRenderer';
13
+ export { MoversTrueFalseCorrectGroupRenderer } from './MoversTrueFalseCorrectGroupRenderer';
13
14
  export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRenderer';
14
15
  export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
15
16
  export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
16
17
  export { MoversWordOrderingRenderer } from './MoversWordOrderingRenderer';
17
18
  export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
18
19
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
20
+ export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
19
21
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
20
22
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
21
23
  export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
@@ -14,12 +14,14 @@ export { MoversFillWordHintRenderer } from './MoversFillWordHintRenderer';
14
14
  export { MoversGridFillRenderer } from './MoversGridFillRenderer';
15
15
  // Starters & Movers renderers
16
16
  export { MoversTrueFalseRenderer } from './MoversTrueFalseRenderer';
17
+ export { MoversTrueFalseCorrectGroupRenderer } from './MoversTrueFalseCorrectGroupRenderer';
17
18
  export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRenderer';
18
19
  export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
19
20
  export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
20
21
  export { MoversWordOrderingRenderer } from './MoversWordOrderingRenderer';
21
22
  export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
22
23
  export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
24
+ export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
23
25
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
24
26
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
25
27
  export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
@@ -113,6 +113,7 @@ export declare function transformWriteShortLetter(question: ExamTakingQuestion |
113
113
  minWords: number | undefined;
114
114
  maxWords: number | undefined;
115
115
  guideImages: string[];
116
+ passage: string | undefined;
116
117
  };
117
118
  export declare function transformWordFillStructuredForm(questions: ExamTakingQuestion[]): {
118
119
  instruction: string;
@@ -61,7 +61,7 @@ export function transformWordFillParagraph(questions) {
61
61
  */
62
62
  export function transformWriteShortLetter(question) {
63
63
  if (!question) {
64
- return { prompt: '', requirements: [], minWords: undefined, maxWords: undefined, guideImages: [] };
64
+ return { prompt: '', requirements: [], minWords: undefined, maxWords: undefined, guideImages: [], passage: undefined };
65
65
  }
66
66
  const content = asRecord(question.content);
67
67
  const instructions = asRecord(content.instructions);
@@ -72,6 +72,12 @@ export function transformWriteShortLetter(question) {
72
72
  const guideImages = readStringArray(question.content, 'guideImages');
73
73
  const minWords = readNumber(question.content, 'minWords');
74
74
  const maxWords = readNumber(question.content, 'maxWords');
75
+ const passageRaw = content.passage ?? content.passages;
76
+ const passage = typeof passageRaw === 'string' && passageRaw.trim()
77
+ ? passageRaw
78
+ : Array.isArray(passageRaw)
79
+ ? passageRaw.filter((item) => typeof item === 'string' && item.trim().length > 0).join('\n\n') || undefined
80
+ : undefined;
75
81
  // New structure: use instructions.mainInstruction as prompt, informations as requirements
76
82
  if (mainInstruction || informations.length) {
77
83
  return {
@@ -80,6 +86,7 @@ export function transformWriteShortLetter(question) {
80
86
  minWords,
81
87
  maxWords,
82
88
  guideImages,
89
+ passage,
83
90
  };
84
91
  }
85
92
  // Legacy structure: build prompt from contexts array
@@ -99,6 +106,7 @@ export function transformWriteShortLetter(question) {
99
106
  minWords,
100
107
  maxWords,
101
108
  guideImages,
109
+ passage,
102
110
  };
103
111
  }
104
112
  export function transformWordFillStructuredForm(questions) {
@@ -115,6 +115,7 @@ export interface ChooseBestAnswerOption {
115
115
  id: string;
116
116
  label: string;
117
117
  text: string;
118
+ imageUrl?: string;
118
119
  }
119
120
  export interface ChooseBestAnswerQuestion {
120
121
  id: string;
@@ -123,7 +124,7 @@ export interface ChooseBestAnswerQuestion {
123
124
  options: ChooseBestAnswerOption[];
124
125
  correctOptionId?: string;
125
126
  correctOptionIds?: string[];
126
- imageUrl?: string;
127
+ imageUrl?: string | string[];
127
128
  audioUrl?: string;
128
129
  optionType?: 'text' | 'image';
129
130
  multipleAnswers?: boolean;
@@ -135,7 +136,7 @@ export interface ChooseBestAnswerExample {
135
136
  options: ChooseBestAnswerOption[];
136
137
  correctOptionId: string;
137
138
  optionType?: 'text' | 'image';
138
- imageUrl?: string;
139
+ imageUrl?: string | string[];
139
140
  audioUrl?: string;
140
141
  }
141
142
  export interface MoversChooseBestAnswerRendererProps {
@@ -87,6 +87,7 @@ function findParentQuestion(subQuestionId, parts) {
87
87
  if (part.questionType === 'CHOOSE_THE_CORRECT_ANSWER_GROUP' ||
88
88
  part.questionType === 'CROSS_OUT_WORD_GROUP' ||
89
89
  part.questionType === 'TRUE_FALSE_GROUP' ||
90
+ part.questionType === 'TRUE_FALSE_CORRECT_GROUP' ||
90
91
  part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
91
92
  part.questionType === 'SPONTANEOUS_QA_GROUP' ||
92
93
  part.questionType === 'ANSWER_THE_QUESTION_GROUP') {
@@ -369,6 +370,17 @@ export function transformAnswersForSubmission(answers, parts) {
369
370
  if (parentType === 'CROSS_OUT_WORD_GROUP') {
370
371
  return normalizeCrossOutWordGroupAnswer(raw);
371
372
  }
373
+ if (parentType === 'TRUE_FALSE_CORRECT_GROUP') {
374
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
375
+ const obj = raw;
376
+ const isTrue = obj.isTrue === true ? true : obj.isTrue === false ? false : null;
377
+ return {
378
+ isTrue,
379
+ correction: typeof obj.correction === 'string' ? obj.correction : '',
380
+ };
381
+ }
382
+ return { isTrue: null, correction: '' };
383
+ }
372
384
  if (parentType === 'WORD_ORDER_AND_MATCH_GROUP') {
373
385
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
374
386
  const obj = raw;