@tinyweb_dev/oe-exam-sdk 0.2.4 → 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 (104) 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 +11 -1
  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/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
  75. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
  76. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
  77. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
  78. package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
  79. package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
  80. package/dist/components/questions/viewer/QuestionViewer.js +18 -3
  81. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
  82. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
  83. package/dist/components/questions/viewer/index.d.ts +3 -0
  84. package/dist/components/questions/viewer/index.js +3 -0
  85. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
  86. package/dist/components/results/renderers/review-question-body-movers.js +39 -3
  87. package/dist/components/results/review-data.js +22 -0
  88. package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.js +1 -1
  91. package/dist/shared/constants/question-skills.js +4 -0
  92. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
  93. package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
  94. package/dist/shared/lib/utils/question-reverse-transform.js +56 -0
  95. package/dist/shared/lib/utils/question-transform.js +4 -0
  96. package/dist/shared/types/common.types.d.ts +1 -1
  97. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  98. package/dist/shared/types/questions/index.d.ts +2 -0
  99. package/dist/shared/types/questions/index.js +4 -0
  100. package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
  101. package/dist/shared/types/questions/match-word-to-picture.js +5 -0
  102. package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
  103. package/dist/shared/types/questions/true-false-correct-group.js +1 -0
  104. package/package.json +7 -2
@@ -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' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | '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' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | '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';
9
9
  export interface ApiQuestion {
10
10
  id: string;
11
11
  content: any;
@@ -184,6 +184,7 @@ export declare function transformWriteShortLetter(question: ApiQuestion): {
184
184
  minWords?: number;
185
185
  maxWords?: number;
186
186
  guideImages?: string[];
187
+ passage?: string;
187
188
  };
188
189
  export declare function transformColorObjects(questions: ApiQuestion[]): {
189
190
  instruction: string;
@@ -269,6 +270,21 @@ export declare function transformTrueFalseGroup(questions: ApiQuestion[]): {
269
270
  imageUrl?: string;
270
271
  groupContent?: string;
271
272
  };
273
+ export interface TrueFalseCorrectItemAnswer {
274
+ isTrue: boolean | null;
275
+ correction: string;
276
+ }
277
+ export interface TrueFalseCorrectGroupQuestion {
278
+ id: string;
279
+ questionNumber: number;
280
+ question: string;
281
+ isExample: boolean;
282
+ exampleCorrectAnswer?: TrueFalseCorrectItemAnswer;
283
+ }
284
+ export declare function transformTrueFalseCorrectGroup(questions: ApiQuestion[]): {
285
+ questions: TrueFalseCorrectGroupQuestion[];
286
+ instruction: string;
287
+ };
272
288
  export interface WordOrderingQuestion {
273
289
  id: string;
274
290
  questionNumber: number;
@@ -321,6 +337,18 @@ export declare function transformMatchByWritingAnswer(questions: ApiQuestion[]):
321
337
  askerName?: string;
322
338
  answererName?: string;
323
339
  };
340
+ export interface MatchWordToPictureQuestion {
341
+ id: string;
342
+ questionNumber: number;
343
+ imageUrl?: string;
344
+ isExample: boolean;
345
+ exampleAnswer?: string;
346
+ }
347
+ export declare function transformMatchWordToPicture(questions: ApiQuestion[]): {
348
+ questions: MatchWordToPictureQuestion[];
349
+ wordBank: string[];
350
+ instruction: string;
351
+ };
324
352
  export declare function transformWriteSentences(questions: ApiQuestion[]): {
325
353
  questions: Array<{
326
354
  id: string;
@@ -428,23 +428,43 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
428
428
  ...(isObj && opt.imageUrl ? { imageUrl: opt.imageUrl } : {}),
429
429
  };
430
430
  });
431
+ const toQuestionImageUrl = (value) => {
432
+ if (typeof value === 'string') {
433
+ const trimmed = value.trim();
434
+ return trimmed !== '' ? trimmed : undefined;
435
+ }
436
+ if (!Array.isArray(value)) {
437
+ return undefined;
438
+ }
439
+ const urls = value
440
+ .filter((url) => typeof url === 'string')
441
+ .map((url) => url.trim())
442
+ .filter((url) => url !== '');
443
+ if (urls.length === 0) {
444
+ return undefined;
445
+ }
446
+ return urls.length === 1 ? urls[0] : urls;
447
+ };
431
448
  let example;
432
449
  if (exampleItem) {
433
450
  const exampleIndex = items.indexOf(exampleItem);
434
451
  const exOptions = mapOptions(exampleItem.content?.options, `${parent.id}-ex`);
435
452
  const exCorrect = exampleItem.correctAnswer?.answer || parentCorrect[exampleIndex] || exOptions[0]?.id || '';
453
+ const exampleImageUrl = toQuestionImageUrl(exampleItem.content?.imageUrl);
436
454
  example = {
437
455
  questionNumber: exampleItem.questionNumber || exampleIndex + 1,
438
456
  questionText: exampleItem.content?.question || '',
439
457
  options: exOptions,
440
458
  correctOptionId: exCorrect,
441
459
  optionType: (exampleItem.content?.optionType || 'text'),
460
+ ...(exampleImageUrl !== undefined ? { imageUrl: exampleImageUrl } : {}),
442
461
  };
443
462
  }
444
463
  const transformed = regularItems.map((item) => {
445
464
  const itemIndex = items.indexOf(item);
446
465
  const options = mapOptions(item.content?.options, `${parent.id}-item-${itemIndex}`);
447
466
  const correctId = item.correctAnswer?.answer || parentCorrect[itemIndex] || '';
467
+ const itemImageUrl = toQuestionImageUrl(item.content?.imageUrl);
448
468
  return {
449
469
  id: `${parent.id}-item-${itemIndex}`,
450
470
  questionNumber: item.questionNumber || itemIndex + 1,
@@ -453,6 +473,7 @@ export function transformChooseTheCorrectAnswerGroup(questions) {
453
473
  correctOptionId: correctId,
454
474
  correctOptionIds: correctId ? [correctId] : [],
455
475
  optionType: (item.content?.optionType || 'text'),
476
+ ...(itemImageUrl !== undefined ? { imageUrl: itemImageUrl } : {}),
456
477
  };
457
478
  });
458
479
  return {
@@ -486,8 +507,19 @@ export function transformChooseCorrectAdjective(questions) {
486
507
  instruction: 'CHOOSE THE CORRECT ADJECTIVE',
487
508
  };
488
509
  }
510
+ function extractWriteLetterPassage(content) {
511
+ const raw = content?.passage ?? content?.passages;
512
+ if (typeof raw === 'string' && raw.trim())
513
+ return raw;
514
+ if (Array.isArray(raw)) {
515
+ const parts = raw.filter((item) => typeof item === 'string' && item.trim().length > 0);
516
+ return parts.length ? parts.join('\n\n') : undefined;
517
+ }
518
+ return undefined;
519
+ }
489
520
  export function transformWriteShortLetter(question) {
490
521
  const content = question.content;
522
+ const passage = extractWriteLetterPassage(content);
491
523
  // New structure: use instructions.mainInstruction as prompt, informations as requirements
492
524
  if (content.instructions?.mainInstruction || content.informations?.length) {
493
525
  return {
@@ -496,6 +528,7 @@ export function transformWriteShortLetter(question) {
496
528
  minWords: content.minWords,
497
529
  maxWords: content.maxWords,
498
530
  guideImages: content.guideImages,
531
+ passage,
499
532
  };
500
533
  }
501
534
  // Legacy structure: build prompt from contexts array
@@ -508,6 +541,7 @@ export function transformWriteShortLetter(question) {
508
541
  minWords: content.minWords,
509
542
  maxWords: content.maxWords,
510
543
  guideImages: content.guideImages,
544
+ passage,
511
545
  };
512
546
  }
513
547
  export function transformColorObjects(questions) {
@@ -889,6 +923,45 @@ export function transformTrueFalseGroup(questions) {
889
923
  instruction: content?.meta?.instruction || 'READ AND CHOOSE TRUE OR FALSE',
890
924
  };
891
925
  }
926
+ function parseTrueFalseCorrectAnswer(raw) {
927
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
928
+ return undefined;
929
+ }
930
+ const obj = raw;
931
+ if (typeof obj.isTrue !== 'boolean') {
932
+ return undefined;
933
+ }
934
+ return {
935
+ isTrue: obj.isTrue,
936
+ correction: obj.isTrue ? '' : (typeof obj.correction === 'string' ? obj.correction : ''),
937
+ };
938
+ }
939
+ export function transformTrueFalseCorrectGroup(questions) {
940
+ const parent = questions[0];
941
+ const content = parent?.content;
942
+ const items = Array.isArray(content?.items) ? content.items : [];
943
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
944
+ ? parent.correct_answer.answer
945
+ : [];
946
+ const transformed = items.map((item, index) => {
947
+ const isExample = item.isExample === true;
948
+ const exampleCorrectAnswer = isExample
949
+ ? parseTrueFalseCorrectAnswer(item.correctAnswer?.answer ?? parentCorrect[index])
950
+ : undefined;
951
+ return {
952
+ id: `${parent.id}-item-${index}`,
953
+ questionNumber: item.questionNumber || index + 1,
954
+ question: item.content?.question || '',
955
+ isExample,
956
+ exampleCorrectAnswer,
957
+ };
958
+ });
959
+ return {
960
+ questions: transformed,
961
+ instruction: content?.meta?.instruction
962
+ || 'Tick (✓) the true and cross (✗) the false sentences. Correct the false sentences.',
963
+ };
964
+ }
892
965
  export function transformWordOrdering(questions) {
893
966
  const transformed = questions.map((q) => {
894
967
  const content = q.content;
@@ -993,6 +1066,31 @@ export function transformMatchByWritingAnswer(questions) {
993
1066
  answererName: firstConversationQ?.answererName,
994
1067
  };
995
1068
  }
1069
+ export function transformMatchWordToPicture(questions) {
1070
+ const groupPayload = questions[0]?.questionGroup?.payload;
1071
+ const wordBank = Array.isArray(groupPayload?.wordBank)
1072
+ ? groupPayload.wordBank.filter((word) => typeof word === 'string' && word.trim().length > 0)
1073
+ : [];
1074
+ const instruction = typeof groupPayload?.instruction === 'string' && groupPayload.instruction.trim()
1075
+ ? groupPayload.instruction.trim()
1076
+ : 'Look and complete.';
1077
+ const transformed = questions.map((q) => {
1078
+ const content = q.content;
1079
+ const isExample = q.is_example || content.isExample || false;
1080
+ return {
1081
+ id: q.id,
1082
+ questionNumber: q.question_number,
1083
+ imageUrl: content.imageUrl,
1084
+ isExample,
1085
+ exampleAnswer: isExample ? q.correct_answer?.answer : undefined,
1086
+ };
1087
+ });
1088
+ return {
1089
+ questions: transformed,
1090
+ wordBank,
1091
+ instruction,
1092
+ };
1093
+ }
996
1094
  export function transformWriteSentences(questions) {
997
1095
  const firstQuestion = questions[0];
998
1096
  const content = firstQuestion?.content;
@@ -172,6 +172,11 @@ export const QUESTION_TYPE_CONFIG = {
172
172
  requiresContent: true,
173
173
  hasDedicatedComponent: true,
174
174
  },
175
+ TRUE_FALSE_CORRECT_GROUP: {
176
+ label: 'Đúng/Sai + sửa câu (nhóm)',
177
+ requiresContent: true,
178
+ hasDedicatedComponent: true,
179
+ },
175
180
  CROSS_OUT_WORD_GROUP: {
176
181
  label: 'Cross out word (group)',
177
182
  requiresContent: true,
@@ -227,6 +232,11 @@ export const QUESTION_TYPE_CONFIG = {
227
232
  requiresContent: true,
228
233
  hasDedicatedComponent: true,
229
234
  },
235
+ MATCH_WORD_TO_PICTURE: {
236
+ label: 'Nối từ với tranh',
237
+ requiresContent: true,
238
+ hasDedicatedComponent: true,
239
+ },
230
240
  WRITE_SENTENCES: {
231
241
  label: 'Viết câu',
232
242
  requiresContent: true,
@@ -343,6 +353,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
343
353
  'CHOOSE_THE_CORRECT_ANSWER',
344
354
  'CHOOSE_THE_CORRECT_ANSWER_GROUP',
345
355
  'TRUE_FALSE_GROUP',
356
+ 'TRUE_FALSE_CORRECT_GROUP',
346
357
  'CROSS_OUT_WORD_GROUP',
347
358
  'WRITE_A_SHORT_LETTER',
348
359
  'WRITE_CORRECT_VERB_FORM',
@@ -353,6 +364,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
353
364
  'WORD_ORDERING',
354
365
  'WORD_FILL_PARAGRAPH',
355
366
  'MATCH_BY_WRITING_ANSWER',
367
+ 'MATCH_WORD_TO_PICTURE',
356
368
  'WRITE_SENTENCES',
357
369
  'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP',
358
370
  'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE',
@@ -1,7 +1,14 @@
1
1
  export interface ChooseTheCorrectAnswerGroupOptionData {
2
2
  id: string;
3
3
  text: string;
4
+ imageUrl?: string;
4
5
  }
6
+ export declare function getChooseAnswerGroupOptionImageSrc(option: Pick<ChooseTheCorrectAnswerGroupOptionData, 'imageUrl' | 'text'>): string;
7
+ export declare function isChooseAnswerGroupOptionFilled(option: Pick<ChooseTheCorrectAnswerGroupOptionData, 'imageUrl' | 'text'>, optionType?: 'text' | 'image'): boolean;
8
+ export type ChooseTheCorrectAnswerGroupImageUrl = string | string[];
9
+ export declare function toChooseAnswerGroupImageUrls(value?: ChooseTheCorrectAnswerGroupImageUrl): string[];
10
+ export declare function toEditableChooseAnswerGroupImageUrls(value?: ChooseTheCorrectAnswerGroupImageUrl): string[];
11
+ export declare function fromChooseAnswerGroupImageUrls(urls: string[]): ChooseTheCorrectAnswerGroupImageUrl | undefined;
5
12
  export interface ChooseTheCorrectAnswerGroupItemData {
6
13
  question: string;
7
14
  optionType: 'text' | 'image';
@@ -10,6 +17,7 @@ export interface ChooseTheCorrectAnswerGroupItemData {
10
17
  isExample?: boolean;
11
18
  points: number;
12
19
  questionNumber: number;
20
+ imageUrl?: ChooseTheCorrectAnswerGroupImageUrl;
13
21
  }
14
22
  export interface ChooseTheCorrectAnswerGroupData {
15
23
  instruction: string;
@@ -34,4 +42,5 @@ export interface ChooseTheCorrectAnswerGroupClientProps {
34
42
  questionData: ChooseTheCorrectAnswerGroupData;
35
43
  isReviewMode?: boolean;
36
44
  userAnswers?: string[];
45
+ onAnswerChange?: (answers: string[]) => void;
37
46
  }
@@ -1 +1,49 @@
1
- export {};
1
+ export function getChooseAnswerGroupOptionImageSrc(option) {
2
+ if (typeof option.imageUrl === 'string' && option.imageUrl.trim() !== '') {
3
+ return option.imageUrl.trim();
4
+ }
5
+ if (typeof option.text === 'string' && option.text.trim() !== '') {
6
+ return option.text.trim();
7
+ }
8
+ return '';
9
+ }
10
+ export function isChooseAnswerGroupOptionFilled(option, optionType = 'text') {
11
+ if (optionType === 'image') {
12
+ return getChooseAnswerGroupOptionImageSrc(option) !== '';
13
+ }
14
+ return typeof option.text === 'string' && option.text.trim() !== '';
15
+ }
16
+ export function toChooseAnswerGroupImageUrls(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
+ export function toEditableChooseAnswerGroupImageUrls(value) {
29
+ if (Array.isArray(value)) {
30
+ return value.filter((url) => typeof url === 'string');
31
+ }
32
+ if (typeof value === 'string') {
33
+ return [value];
34
+ }
35
+ return [];
36
+ }
37
+ export function fromChooseAnswerGroupImageUrls(urls) {
38
+ const cleaned = urls
39
+ .filter((url) => typeof url === 'string')
40
+ .map((url) => url.trim())
41
+ .filter((url) => url !== '');
42
+ if (cleaned.length === 0) {
43
+ return undefined;
44
+ }
45
+ if (cleaned.length === 1) {
46
+ return cleaned[0];
47
+ }
48
+ return cleaned;
49
+ }
@@ -11,4 +11,6 @@ export * from './read-and-color-objects.type';
11
11
  export * from './fill-in-blank.type';
12
12
  export * from './read-passage-and-answer-questions.type';
13
13
  export * from './word-fill-structured-form.type';
14
+ export * from './true-false-correct-group.type';
15
+ export * from './match-word-to-picture.type';
14
16
  export * from './spontaneous-qa-group.type';
@@ -11,4 +11,6 @@ export * from './read-and-color-objects.type';
11
11
  export * from './fill-in-blank.type';
12
12
  export * from './read-passage-and-answer-questions.type';
13
13
  export * from './word-fill-structured-form.type';
14
+ export * from './true-false-correct-group.type';
15
+ export * from './match-word-to-picture.type';
14
16
  export * from './spontaneous-qa-group.type';
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import type { ValidationRef } from './look-and-choose-answer.type';
3
+ export type { ValidationRef };
4
+ export declare const MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION = "Look and complete.";
5
+ export interface MatchWordToPictureGroup {
6
+ number: number;
7
+ questionCount: number;
8
+ }
9
+ export interface MatchWordToPictureData {
10
+ imageUrl: string;
11
+ correctAnswer: string;
12
+ points?: number;
13
+ groupNumber?: number;
14
+ isExample?: boolean;
15
+ wordBank: string[];
16
+ instruction?: string;
17
+ groups?: MatchWordToPictureGroup[];
18
+ explanation?: string;
19
+ }
20
+ export interface MatchWordToPictureCreatorProps {
21
+ initialData?: MatchWordToPictureData;
22
+ onSave?: (data: MatchWordToPictureData) => void;
23
+ onSaveToQuestionBank?: (data: MatchWordToPictureData) => void;
24
+ onCancel?: () => void;
25
+ onChange?: (data: MatchWordToPictureData) => void;
26
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
27
+ externalErrors?: string[];
28
+ groups?: MatchWordToPictureGroup[];
29
+ groupConfig?: string[];
30
+ questionConfig?: string[];
31
+ questionIndexInPart?: number;
32
+ partId?: string;
33
+ validationRef?: React.MutableRefObject<ValidationRef | null>;
34
+ }
35
+ export interface MatchWordToPictureClientProps {
36
+ questionData: MatchWordToPictureData;
37
+ onSubmit?: (answer: string) => void;
38
+ isReviewMode?: boolean;
39
+ userAnswer?: string;
40
+ autoFillCorrectAnswers?: boolean;
41
+ explanation?: string;
42
+ }
43
+ export interface MatchWordToPictureGroupItemData {
44
+ imageUrl: string;
45
+ correctAnswer: string;
46
+ isExample?: boolean;
47
+ points: number;
48
+ questionNumber: number;
49
+ }
50
+ export interface MatchWordToPictureGroupData {
51
+ instruction: string;
52
+ wordBank: string[];
53
+ items: MatchWordToPictureGroupItemData[];
54
+ explanation?: string;
55
+ points?: number;
56
+ }
57
+ export interface MatchWordToPictureGroupClientProps {
58
+ questionData: MatchWordToPictureGroupData;
59
+ isReviewMode?: boolean;
60
+ userAnswers?: string[];
61
+ onAnswerChange?: (answers: string[]) => void;
62
+ }
@@ -0,0 +1 @@
1
+ export const MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION = 'Look and complete.';
@@ -0,0 +1,42 @@
1
+ import type { MutableRefObject } from 'react';
2
+ export declare const TRUE_FALSE_CORRECT_ITEM_TYPE: "TRUE_FALSE_CORRECT";
3
+ export declare const TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION = "Tick (\u2713) the true and cross (\u2717) the false sentences. Correct the false sentences.";
4
+ export interface TrueFalseCorrectItemAnswer {
5
+ isTrue: boolean;
6
+ correction: string;
7
+ }
8
+ export interface TrueFalseCorrectStudentItemAnswer {
9
+ isTrue: boolean | null;
10
+ correction: string;
11
+ }
12
+ export interface TrueFalseCorrectGroupItemData {
13
+ question: string;
14
+ isTrue: boolean;
15
+ correction: string;
16
+ isExample?: boolean;
17
+ points: number;
18
+ questionNumber: number;
19
+ }
20
+ export interface TrueFalseCorrectGroupData {
21
+ instruction: string;
22
+ items: TrueFalseCorrectGroupItemData[];
23
+ explanation?: string;
24
+ points?: number;
25
+ }
26
+ export interface TrueFalseCorrectGroupCreatorProps {
27
+ initialData?: TrueFalseCorrectGroupData;
28
+ onSave?: (data: TrueFalseCorrectGroupData) => void;
29
+ onCancel?: () => void;
30
+ onChange?: (data: TrueFalseCorrectGroupData) => void;
31
+ externalErrors?: string[];
32
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
33
+ validationRef?: MutableRefObject<{
34
+ triggerValidation: () => Promise<boolean>;
35
+ getFormData?: () => TrueFalseCorrectGroupData;
36
+ } | null>;
37
+ }
38
+ export interface TrueFalseCorrectGroupClientProps {
39
+ questionData: TrueFalseCorrectGroupData;
40
+ isReviewMode?: boolean;
41
+ userAnswers?: TrueFalseCorrectStudentItemAnswer[];
42
+ }
@@ -0,0 +1,2 @@
1
+ export const TRUE_FALSE_CORRECT_ITEM_TYPE = 'TRUE_FALSE_CORRECT';
2
+ export const TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION = 'Tick (✓) the true and cross (✗) the false sentences. Correct the false sentences.';
@@ -7,6 +7,8 @@ export interface WriteAShortLetterData {
7
7
  maxWords?: number;
8
8
  instructions?: WriteAShortLetterInstructions;
9
9
  guideImages?: string[];
10
+ /** Optional HTML/text article the student must read before writing. */
11
+ passage?: string;
10
12
  points?: number;
11
13
  }
12
14
  export interface WriteAShortLetterCreatorProps {
@@ -26,6 +28,8 @@ export interface WriteAShortLetterQuestionData {
26
28
  maxWords?: number;
27
29
  instructions?: WriteAShortLetterInstructions;
28
30
  guideImages?: string[];
31
+ /** Optional HTML/text article the student must read before writing. */
32
+ passage?: string;
29
33
  }
30
34
  export interface WriteAShortLetterClientProps {
31
35
  questionData: WriteAShortLetterQuestionData;
@@ -27,6 +27,7 @@ const IMAGE_BASED_TYPES = [
27
27
  'READ_AND_COLOR_OBJECTS',
28
28
  'IMAGE_OBJECT_MATCHING',
29
29
  'FILL_IN_BLANK',
30
+ 'MATCH_WORD_TO_PICTURE',
30
31
  ];
31
32
  // Passage-based question types
32
33
  const PASSAGE_BASED_TYPES = [
@@ -62,6 +63,7 @@ const getQuestionTypeName = (type) => {
62
63
  'WRITE_CORRECT_VERB_FORM': 'Viết dạng động từ',
63
64
  'FILL_IN_BLANK': 'Điền vào chỗ trống',
64
65
  'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu',
66
+ 'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
65
67
  };
66
68
  return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
67
69
  };
@@ -26,6 +26,7 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
26
26
  'ANSWER_THE_QUESTION',
27
27
  'WORD_ORDERING',
28
28
  'MATCH_BY_WRITING_ANSWER',
29
+ 'MATCH_WORD_TO_PICTURE',
29
30
  'WRITE_SENTENCES',
30
31
  ];
31
32
  return (_jsxs(Select, { value: value, onValueChange: onValueChange, disabled: disabled, children: [_jsx(SelectTrigger, { id: "questionType", className: "mt-1", children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn lo\u1EA1i c\u00E2u h\u1ECFi" }) }), _jsx(SelectContent, { children: questionTypes.map((type) => (_jsx(SelectItem, { value: type, children: getQuestionTypeLabel(type) }, type))) })] }));
@@ -13,6 +13,7 @@ import { readPassageAndAnswerQuestionsRegistration } from '../types/read-passage
13
13
  import { chooseTheCorrectAnswerRegistration } from '../types/choose-the-correct-answer/register';
14
14
  import { chooseTheCorrectAnswerGroupRegistration } from '../types/choose-the-correct-answer-group/register';
15
15
  import { trueFalseGroupRegistration } from '../types/true-false-group/register';
16
+ import { trueFalseCorrectGroupRegistration } from '../types/true-false-correct-group/register';
16
17
  import { crossOutWordGroupRegistration } from '../types/cross-out-word-group/register';
17
18
  import { writeAShortLetterRegistration } from '../types/write-a-short-letter/register';
18
19
  import { writeCorrectVerbFormRegistration } from '../types/write-correct-verb-form/register';
@@ -23,6 +24,7 @@ import { trueFalseRegistration } from '../types/true-false/register';
23
24
  import { wordOrderingRegistration } from '../types/word-ordering/register';
24
25
  import { wordFillParagraphRegistration } from '../types/word-fill-paragraph/register';
25
26
  import { matchByWritingAnswerRegistration } from '../types/match-by-writing-answer/register';
27
+ import { matchWordToPictureRegistration } from '../types/match-word-to-picture/register';
26
28
  import { writeSentencesRegistration } from '../types/write-sentences/register';
27
29
  import { listenAndChooseFromAnswerGroupRegistration } from '../types/listen-and-choose-from-answer-group/register';
28
30
  import { listenAndChooseObjectsInSceneRegistration } from '../types/listen-and-choose-objects-in-scene/register';
@@ -50,6 +52,7 @@ export const questionTypeRegistry = {
50
52
  CHOOSE_THE_CORRECT_ANSWER: chooseTheCorrectAnswerRegistration,
51
53
  CHOOSE_THE_CORRECT_ANSWER_GROUP: chooseTheCorrectAnswerGroupRegistration,
52
54
  TRUE_FALSE_GROUP: trueFalseGroupRegistration,
55
+ TRUE_FALSE_CORRECT_GROUP: trueFalseCorrectGroupRegistration,
53
56
  CROSS_OUT_WORD_GROUP: crossOutWordGroupRegistration,
54
57
  WORD_ORDER_AND_MATCH_GROUP: wordOrderAndMatchGroupRegistration,
55
58
  FILL_IN_BLANK: fillInBlankRegistration,
@@ -72,6 +75,7 @@ export const questionTypeRegistry = {
72
75
  WORD_ORDERING: wordOrderingRegistration,
73
76
  WORD_FILL_PARAGRAPH: wordFillParagraphRegistration, // Will need to handle viewMode mapping either here or in wrapper
74
77
  MATCH_BY_WRITING_ANSWER: matchByWritingAnswerRegistration,
78
+ MATCH_WORD_TO_PICTURE: matchWordToPictureRegistration,
75
79
  WRITE_SENTENCES: writeSentencesRegistration,
76
80
  LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP: listenAndChooseFromAnswerGroupRegistration,
77
81
  LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE: listenAndChooseObjectsInSceneRegistration,
@@ -0,0 +1,13 @@
1
+ export declare function ChooseAnswerGroupImageUploadItem({ index, itemIndex, imageUrl, onChange, onRemove, }: {
2
+ index: number;
3
+ itemIndex: number;
4
+ imageUrl: string;
5
+ onChange: (url: string) => void;
6
+ onRemove: () => void;
7
+ }): import("react").JSX.Element;
8
+ export declare function ChooseAnswerGroupOptionImageField({ itemIndex, optionIndex, imageUrl, onChange, }: {
9
+ itemIndex: number;
10
+ optionIndex: number;
11
+ imageUrl: string;
12
+ onChange: (url: string) => void;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { Button } from '../../../../components/ui/button';
5
+ import { FileUpload } from '../../../../components/ui/file-upload';
6
+ import { ImagePreview } from '../../../../components/ui/image-preview';
7
+ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
8
+ import { Trash2 } from 'lucide-react';
9
+ function resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl) {
10
+ if (previewOverride) {
11
+ return previewOverride;
12
+ }
13
+ if (fetchedPreviewUrl) {
14
+ return fetchedPreviewUrl;
15
+ }
16
+ if (imageUrl.startsWith('http') || imageUrl.startsWith('blob:')) {
17
+ return imageUrl;
18
+ }
19
+ return '';
20
+ }
21
+ export function ChooseAnswerGroupImageUploadItem({ index, itemIndex, imageUrl, onChange, onRemove, }) {
22
+ const [previewOverride, setPreviewOverride] = useState('');
23
+ const { previewUrl: fetchedPreviewUrl } = usePresignedFileUrl(imageUrl);
24
+ const displayUrl = resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl);
25
+ return (_jsxs("div", { className: "space-y-2 rounded-lg border border-gray-200 bg-gray-50/40 p-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-teal-100 text-xs font-semibold text-teal-700", children: index + 1 }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `choose-answer-group-image-${itemIndex}-${index}`, label: "", accept: "image/*", value: imageUrl, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: onRemove, title: "Xo\u00E1 \u1EA3nh n\u00E0y", className: "h-8 w-8 text-gray-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), displayUrl && (_jsx("div", { className: "pl-8", children: _jsx(ImagePreview, { src: displayUrl, alt: `Hình ảnh ${index + 1}`, className: "h-40 w-full max-w-sm rounded-lg border border-gray-200 bg-white object-contain" }) }))] }));
26
+ }
27
+ export function ChooseAnswerGroupOptionImageField({ itemIndex, optionIndex, imageUrl, onChange, }) {
28
+ const [previewOverride, setPreviewOverride] = useState('');
29
+ const { previewUrl: fetchedPreviewUrl } = usePresignedFileUrl(imageUrl);
30
+ const displayUrl = resolveDisplayUrl(imageUrl, previewOverride, fetchedPreviewUrl);
31
+ return (_jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(FileUpload, { id: `choose-answer-group-option-${itemIndex}-${optionIndex}`, label: "", accept: "image/*", value: imageUrl, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: 5, placeholder: `Upload ảnh đáp án ${optionIndex + 1}`, autoUpload: true, prefix: "questions/options" }), displayUrl && (_jsx(ImagePreview, { src: displayUrl, alt: `Đáp án ${optionIndex + 1}`, className: "h-28 w-28 rounded-lg border border-gray-200 bg-white object-contain" }))] }));
32
+ }
@@ -1,2 +1,2 @@
1
1
  import type { ChooseTheCorrectAnswerGroupClientProps } from '../../_shared/types/choose-the-correct-answer-group.type';
2
- export declare function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode, userAnswers, }: ChooseTheCorrectAnswerGroupClientProps): import("react").JSX.Element;
2
+ export declare function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: ChooseTheCorrectAnswerGroupClientProps): import("react").JSX.Element;