@tinyweb_dev/oe-exam-sdk 0.2.13 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +12 -0
  2. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  3. package/dist/components/exams/take/components/QuestionRenderer.js +13 -3
  4. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts +14 -0
  5. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +32 -0
  6. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +1 -1
  7. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +10 -2
  8. package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.d.ts +16 -0
  9. package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
  10. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  11. package/dist/components/exams/take/components/question-renderers/index.js +2 -0
  12. package/dist/components/exams/take/utils/answer-transformers.js +34 -1
  13. package/dist/components/exams/take/utils/question-transformers.d.ts +42 -3
  14. package/dist/components/exams/take/utils/question-transformers.js +71 -1
  15. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  16. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +5 -1
  17. package/dist/components/questions/_shared/types/answer-the-question-group.type.js +7 -0
  18. package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
  19. package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
  20. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
  21. package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
  22. package/dist/components/questions/_shared/types/index.d.ts +2 -0
  23. package/dist/components/questions/_shared/types/index.js +2 -0
  24. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +9 -3
  25. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
  26. package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
  27. package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
  28. package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
  29. package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
  30. package/dist/components/questions/creator/question-type-registry.js +4 -0
  31. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
  32. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
  33. package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
  34. package/dist/components/questions/types/answer-the-question/transform.js +9 -3
  35. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +42 -9
  36. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -5
  37. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +18 -4
  38. package/dist/components/questions/types/answer-the-question-group/transform.js +8 -2
  39. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -2
  40. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
  41. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
  42. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
  43. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
  44. package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
  45. package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
  46. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
  47. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
  48. package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
  49. package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
  50. package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
  51. package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -0
  52. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +39 -10
  53. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +15 -8
  54. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +8 -3
  55. package/dist/components/questions/types/match-word-to-picture-group/transform.js +21 -13
  56. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
  57. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
  58. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
  59. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
  60. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
  61. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
  62. package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
  63. package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
  64. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
  65. package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
  66. package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
  67. package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
  68. package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
  69. package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
  70. package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
  71. package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
  72. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
  73. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
  74. package/dist/components/questions/viewer/QuestionViewer.js +13 -2
  75. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
  76. package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
  77. package/dist/components/questions/viewer/index.d.ts +3 -0
  78. package/dist/components/questions/viewer/index.js +3 -0
  79. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
  80. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
  81. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  82. package/dist/components/results/renderers/review-question-body-movers.js +24 -2
  83. package/dist/components/results/review-data.js +28 -0
  84. package/dist/index.d.ts +1 -1
  85. package/dist/index.js +1 -1
  86. package/dist/shared/constants/question-skills.js +4 -0
  87. package/dist/shared/lib/utils/question-reverse-transform.js +98 -2
  88. package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
  89. package/dist/shared/lib/utils/question-transform.js +4 -0
  90. package/dist/shared/types/common.types.d.ts +1 -1
  91. package/dist/shared/types/questions/answer-the-question-group.d.ts +3 -0
  92. package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
  93. package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
  94. package/dist/shared/types/questions/index.d.ts +2 -0
  95. package/dist/shared/types/questions/index.js +3 -0
  96. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +6 -2
  97. package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
  98. package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
  99. package/package.json +1 -1
@@ -0,0 +1,153 @@
1
+ import { SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION } from '../../_shared/types/sort-words-into-categories.type';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function asRecord(value) {
6
+ return isRecord(value) ? value : {};
7
+ }
8
+ function asString(value, fallback = '') {
9
+ return typeof value === 'string' ? value : fallback;
10
+ }
11
+ export function nextPrefixedId(prefix, existing) {
12
+ let n = existing.length + 1;
13
+ const used = new Set(existing);
14
+ while (used.has(`${prefix}${n}`))
15
+ n += 1;
16
+ return `${prefix}${n}`;
17
+ }
18
+ export function sumGradablePoints(words) {
19
+ return words.reduce((total, word) => {
20
+ if (word.isExample)
21
+ return total;
22
+ return total + (typeof word.points === 'number' ? word.points : 1);
23
+ }, 0);
24
+ }
25
+ export function buildPlacements(words) {
26
+ return words.map((word) => ({
27
+ wordId: word.id,
28
+ categoryId: word.categoryId || '',
29
+ }));
30
+ }
31
+ export function readPlacements(value) {
32
+ const source = Array.isArray(value)
33
+ ? value
34
+ : isRecord(value) && Array.isArray(value.placements)
35
+ ? value.placements
36
+ : null;
37
+ if (!source)
38
+ return undefined;
39
+ return source.map((item) => {
40
+ const record = asRecord(item);
41
+ return {
42
+ wordId: asString(record.wordId),
43
+ categoryId: asString(record.categoryId),
44
+ };
45
+ });
46
+ }
47
+ export function padPlacements(words, raw) {
48
+ const source = readPlacements(raw) ?? [];
49
+ const byWord = new Map();
50
+ source.forEach((item) => {
51
+ const record = asRecord(item);
52
+ const wordId = asString(record.wordId);
53
+ if (!wordId || byWord.has(wordId))
54
+ return;
55
+ byWord.set(wordId, asString(record.categoryId));
56
+ });
57
+ return words.map((word) => ({
58
+ wordId: word.id,
59
+ categoryId: byWord.get(word.id) ?? '',
60
+ }));
61
+ }
62
+ export function createEmptyCategory(existingIds) {
63
+ return {
64
+ id: nextPrefixedId('cat_', existingIds),
65
+ name: '',
66
+ };
67
+ }
68
+ export function createEmptyWord(existingIds, categoryId) {
69
+ return {
70
+ id: nextPrefixedId('w', existingIds),
71
+ text: '',
72
+ categoryId,
73
+ isExample: false,
74
+ points: 1,
75
+ };
76
+ }
77
+ function mapCategories(raw) {
78
+ if (!Array.isArray(raw))
79
+ return [];
80
+ return raw.map((item, index) => {
81
+ const record = asRecord(item);
82
+ return {
83
+ id: asString(record.id, `cat_${index + 1}`),
84
+ name: asString(record.name),
85
+ };
86
+ });
87
+ }
88
+ function mapWords(rawWords, placements) {
89
+ if (!Array.isArray(rawWords))
90
+ return [];
91
+ const placementMap = new Map(placements.map((item) => [item.wordId, item.categoryId]));
92
+ return rawWords.map((item, index) => {
93
+ const record = asRecord(item);
94
+ const id = asString(record.id, `w${index + 1}`);
95
+ const isExample = record.isExample === true;
96
+ return {
97
+ id,
98
+ text: asString(record.text),
99
+ categoryId: asString(record.categoryId) || placementMap.get(id) || '',
100
+ isExample,
101
+ points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
102
+ };
103
+ });
104
+ }
105
+ export function mapQuestionToSortWordsIntoCategoriesData(question) {
106
+ const source = asRecord(question);
107
+ const content = asRecord(source.content);
108
+ const answer = asRecord(source.answer);
109
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
110
+ const explanation = asString(source.explanation) || asString(answer.explanation);
111
+ const meta = asRecord(content.meta ?? answer.meta);
112
+ const placements = padPlacements(Array.isArray(content.words)
113
+ ? content.words.map((item, index) => ({ id: asString(asRecord(item).id, `w${index + 1}`) }))
114
+ : Array.isArray(answer.words)
115
+ ? answer.words.map((item, index) => ({ id: asString(asRecord(item).id, `w${index + 1}`) }))
116
+ : [], correctAnswer.placements ?? answer.placements);
117
+ if (Array.isArray(content.words) || Array.isArray(content.categories)) {
118
+ const words = mapWords(content.words, placements);
119
+ return {
120
+ title: asString(meta.title) || asString(answer.title),
121
+ instruction: asString(meta.instruction) ||
122
+ asString(answer.instruction) ||
123
+ SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
124
+ categories: mapCategories(content.categories),
125
+ words,
126
+ explanation,
127
+ points: sumGradablePoints(words),
128
+ };
129
+ }
130
+ if (Array.isArray(answer.words) || Array.isArray(answer.categories)) {
131
+ const words = mapWords(answer.words, placements);
132
+ return {
133
+ title: asString(answer.title),
134
+ instruction: asString(answer.instruction) || SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
135
+ categories: mapCategories(answer.categories),
136
+ words,
137
+ explanation,
138
+ points: typeof answer.points === 'number' ? answer.points : sumGradablePoints(words),
139
+ };
140
+ }
141
+ const firstCategory = createEmptyCategory([]);
142
+ const secondCategory = createEmptyCategory([firstCategory.id]);
143
+ const firstWord = createEmptyWord([], firstCategory.id);
144
+ const secondWord = createEmptyWord([firstWord.id], secondCategory.id);
145
+ return {
146
+ title: '',
147
+ instruction: SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
148
+ categories: [firstCategory, secondCategory],
149
+ words: [firstWord, secondWord],
150
+ explanation,
151
+ points: 2,
152
+ };
153
+ }
@@ -0,0 +1,3 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export { buildPlacements, createEmptyCategory, createEmptyWord, mapQuestionToSortWordsIntoCategoriesData, padPlacements, readPlacements, sumGradablePoints, } from './map-sort-words-into-categories-data';
3
+ export declare const sortWordsIntoCategoriesRegistration: QuestionTypeRegistration;
@@ -0,0 +1,37 @@
1
+ import { SortWordsIntoCategoriesCreator } from './SortWordsIntoCategoriesCreator';
2
+ import { mapQuestionToSortWordsIntoCategoriesData } from './map-sort-words-into-categories-data';
3
+ export { buildPlacements, createEmptyCategory, createEmptyWord, mapQuestionToSortWordsIntoCategoriesData, padPlacements, readPlacements, sumGradablePoints, } from './map-sort-words-into-categories-data';
4
+ export const sortWordsIntoCategoriesRegistration = {
5
+ component: SortWordsIntoCategoriesCreator,
6
+ transformInitialData: (initialData) => mapQuestionToSortWordsIntoCategoriesData(initialData),
7
+ getExtraProps: (ctx) => ({
8
+ onUnsavedChangesChange: ctx.onUnsavedChangesChange,
9
+ validationRef: ctx.validationRef,
10
+ }),
11
+ wrapOnSave: (data, ctx) => {
12
+ const payload = data;
13
+ return {
14
+ type: ctx.questionType,
15
+ content: ctx.state.content,
16
+ points: payload.points ?? ctx.state.points ?? 1,
17
+ level: ctx.state.level,
18
+ difficulty: ctx.state.difficulty,
19
+ skill: ctx.state.skill,
20
+ answer: payload,
21
+ explanation: payload.explanation || '',
22
+ };
23
+ },
24
+ wrapOnChange: (data, ctx) => {
25
+ const payload = data;
26
+ return {
27
+ type: ctx.questionType,
28
+ content: ctx.state.content,
29
+ points: payload.points ?? ctx.state.points ?? 1,
30
+ level: ctx.state.level,
31
+ difficulty: ctx.state.difficulty,
32
+ skill: ctx.state.skill,
33
+ answer: payload,
34
+ explanation: payload.explanation || '',
35
+ };
36
+ },
37
+ };
@@ -0,0 +1,2 @@
1
+ import type { TransformHandler } from '../../../../shared/lib/utils/question-transform-types';
2
+ export declare const transformSortWordsIntoCategories: TransformHandler;
@@ -0,0 +1,33 @@
1
+ import { SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION } from '../../_shared/types/sort-words-into-categories.type';
2
+ import { sumGradablePoints } from './map-sort-words-into-categories-data';
3
+ export const transformSortWordsIntoCategories = (question) => {
4
+ const answerData = question.answer;
5
+ const categories = Array.isArray(answerData?.categories) ? answerData.categories : [];
6
+ const words = Array.isArray(answerData?.words) ? answerData.words : [];
7
+ const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
8
+ return {
9
+ apiContent: {
10
+ meta: {
11
+ instruction: answerData?.instruction || SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
12
+ ...(title ? { title } : {}),
13
+ },
14
+ categories: categories.map((item) => ({
15
+ id: item.id || '',
16
+ name: item.name || '',
17
+ })),
18
+ words: words.map((item) => ({
19
+ id: item.id || '',
20
+ text: item.text || '',
21
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
22
+ ...(item.isExample ? { isExample: true } : {}),
23
+ })),
24
+ },
25
+ apiCorrectAnswer: {
26
+ placements: words.map((item) => ({
27
+ wordId: item.id || '',
28
+ categoryId: item.categoryId || '',
29
+ })),
30
+ },
31
+ totalPoints: sumGradablePoints(words),
32
+ };
33
+ };
@@ -0,0 +1,9 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ type AnswerTheQuestionViewerProps = {
3
+ question: Question;
4
+ isReviewMode?: boolean;
5
+ userAnswer?: string;
6
+ onSubmitAnswer?: (answer: string) => void;
7
+ };
8
+ export declare function AnswerTheQuestionViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: AnswerTheQuestionViewerProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { AnswerTheQuestionClient } from '../types/answer-the-question/AnswerTheQuestionClient';
4
+ import { mapQuestionToAnswerTheQuestionData } from '../types/answer-the-question/map-answer-the-question-data';
5
+ export function AnswerTheQuestionViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
+ const questionData = mapQuestionToAnswerTheQuestionData(question);
7
+ return (_jsx(AnswerTheQuestionClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswer: userAnswer, autoFillCorrectAnswers: isReviewMode && !userAnswer, explanation: question.explanation || questionData.explanation, onSubmit: isReviewMode ? undefined : onSubmitAnswer }));
8
+ }
@@ -0,0 +1,10 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ import type { ChooseThenAnswerStudentItemAnswer } from '../_shared/types/choose-then-answer-group.type';
3
+ export declare function ChooseThenAnswerGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
4
+ question: Question;
5
+ isReviewMode?: boolean;
6
+ userAnswer?: unknown;
7
+ onSubmitAnswer?: (answer: {
8
+ answer: ChooseThenAnswerStudentItemAnswer[];
9
+ }) => void;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { ChooseThenAnswerGroupClient } from '../types/choose-then-answer-group/ChooseThenAnswerGroupClient';
5
+ import { mapQuestionToChooseThenAnswerGroupData, parseStudentAnswer, } from '../types/choose-then-answer-group/map-choose-then-answer-group-data';
6
+ function readStudentAnswers(value, itemCount) {
7
+ const record = value && typeof value === 'object' && !Array.isArray(value)
8
+ ? value
9
+ : null;
10
+ const raw = Array.isArray(record?.answer)
11
+ ? record.answer
12
+ : Array.isArray(value)
13
+ ? value
14
+ : null;
15
+ if (!raw)
16
+ return undefined;
17
+ return Array.from({ length: itemCount }, (_, index) => parseStudentAnswer(raw[index]));
18
+ }
19
+ export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
20
+ const questionData = mapQuestionToChooseThenAnswerGroupData(question);
21
+ const seed = questionData.items.map((item) => ({
22
+ optionId: item.isExample ? item.optionId : '',
23
+ text: '',
24
+ }));
25
+ const parsedUser = readStudentAnswers(userAnswer, questionData.items.length);
26
+ const questionKey = `${question.id}:${question.type}:${questionData.items.length}`;
27
+ const [cachedKey, setCachedKey] = useState(questionKey);
28
+ const [localAnswers, setLocalAnswers] = useState(() => parsedUser ?? seed);
29
+ if (cachedKey !== questionKey) {
30
+ setCachedKey(questionKey);
31
+ setLocalAnswers(parsedUser ?? seed);
32
+ }
33
+ const handleChange = (index, next) => {
34
+ const updated = localAnswers.map((item, itemIndex) => itemIndex === index ? next : item);
35
+ setLocalAnswers(updated);
36
+ onSubmitAnswer?.({ answer: updated });
37
+ };
38
+ return (_jsx(ChooseThenAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localAnswers), onAnswerChange: isReviewMode ? undefined : handleChange }));
39
+ }
@@ -21,6 +21,8 @@ import { FillMissingWordsInGridClient } from '../types/fill-missing-words-in-gri
21
21
  import { WordFillStructuredFormClient } from '../types/word-fill-structured-form/WordFillStructuredFormClient';
22
22
  import { CrosswordPuzzleClient } from '../types/crossword-puzzle/CrosswordPuzzleClient';
23
23
  import { FindWordsInMatrixClient } from '../types/find-words-in-matrix/FindWordsInMatrixClient';
24
+ import { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
25
+ import { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
24
26
  import { parseFillBlankStemLine } from '../_shared/utils/parseFillBlankStem';
25
27
  import { WordFillStructuredFormViewer } from './WordFillStructuredFormViewer';
26
28
  import { ChooseTheCorrectAnswerGroupViewer } from './ChooseTheCorrectAnswerGroupViewer';
@@ -31,6 +33,7 @@ import { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
31
33
  import { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
32
34
  import { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
33
35
  import { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
36
+ import { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
34
37
  import { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
35
38
  import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
36
39
  import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
@@ -647,6 +650,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
647
650
  return (_jsx(MatchColumnsToMakeSentencesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
648
651
  case 'CROSS_OUT_WORD_GROUP':
649
652
  return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
653
+ case 'ANSWER_THE_QUESTION':
654
+ return (_jsx(AnswerTheQuestionViewer, { question: question, isReviewMode: isReviewMode, userAnswer: typeof userAnswer === 'string' ? userAnswer : undefined, onSubmitAnswer: onSubmitAnswer }));
650
655
  case 'ANSWER_THE_QUESTION_GROUP':
651
656
  return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
652
657
  case 'SPONTANEOUS_QA_GROUP':
@@ -703,6 +708,10 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
703
708
  return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.answer, isReviewMode: isReviewMode, userAnswers: userAnswer }));
704
709
  case 'FIND_WORDS_IN_MATRIX':
705
710
  return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.answer, showSolution: isReviewMode }));
711
+ case 'SORT_WORDS_INTO_CATEGORIES':
712
+ return (_jsx(SortWordsIntoCategoriesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
713
+ case 'CHOOSE_THEN_ANSWER_GROUP':
714
+ return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
706
715
  default:
707
716
  return _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
708
717
  }
@@ -716,7 +725,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
716
725
  // WFSF and Crossword show title/instruction inside the client — no outer card label.
717
726
  if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
718
727
  question.type === 'CROSSWORD_PUZZLE' ||
719
- question.type === 'MATCH_WORD_TO_PICTURE') {
728
+ question.type === 'MATCH_WORD_TO_PICTURE' ||
729
+ question.type === 'ANSWER_THE_QUESTION') {
720
730
  return null;
721
731
  }
722
732
  // Modern fill stems live entirely in the body; title is a short label.
@@ -741,7 +751,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
741
751
  // WFSF and Crossword already have their own bordered shell — avoid double Card border.
742
752
  if (question.type === 'WORD_FILL_STRUCTURED_FORM' ||
743
753
  question.type === 'CROSSWORD_PUZZLE' ||
744
- question.type === 'MATCH_WORD_TO_PICTURE') {
754
+ question.type === 'MATCH_WORD_TO_PICTURE' ||
755
+ question.type === 'ANSWER_THE_QUESTION') {
745
756
  return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
746
757
  }
747
758
  return (_jsxs(Card, { children: [cardTitle ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsxs("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: [question.points, " \u0111i\u1EC3m"] })] }) })) : null, _jsx(CardContent, { className: cardTitle ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
@@ -0,0 +1,10 @@
1
+ import type { Question } from '../../../shared/types/entities/question.types';
2
+ import type { SortWordsPlacement } from '../_shared/types/sort-words-into-categories.type';
3
+ export declare function SortWordsIntoCategoriesViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
4
+ question: Question;
5
+ isReviewMode?: boolean;
6
+ userAnswer?: unknown;
7
+ onSubmitAnswer?: (answer: {
8
+ placements: SortWordsPlacement[];
9
+ }) => void;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { SortWordsIntoCategoriesClient } from '../types/sort-words-into-categories/SortWordsIntoCategoriesClient';
5
+ import { mapQuestionToSortWordsIntoCategoriesData, readPlacements, } from '../types/sort-words-into-categories/map-sort-words-into-categories-data';
6
+ export function SortWordsIntoCategoriesViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
7
+ const questionData = mapQuestionToSortWordsIntoCategoriesData(question);
8
+ const parsedUser = readPlacements(userAnswer);
9
+ const questionKey = `${question.id}:${question.type}:${questionData.words.map((word) => word.id).join(',')}`;
10
+ const [cachedKey, setCachedKey] = useState(questionKey);
11
+ const [localPlacements, setLocalPlacements] = useState(() => parsedUser ?? questionData.words.map((word) => ({
12
+ wordId: word.id,
13
+ categoryId: word.isExample ? word.categoryId : '',
14
+ })));
15
+ if (cachedKey !== questionKey) {
16
+ setCachedKey(questionKey);
17
+ setLocalPlacements(parsedUser ?? questionData.words.map((word) => ({
18
+ wordId: word.id,
19
+ categoryId: word.isExample ? word.categoryId : '',
20
+ })));
21
+ }
22
+ const handleChange = (next) => {
23
+ setLocalPlacements(next);
24
+ onSubmitAnswer?.({ placements: next });
25
+ };
26
+ return (_jsx(SortWordsIntoCategoriesClient, { questionData: questionData, isReviewMode: isReviewMode, userPlacements: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localPlacements), onPlacementsChange: isReviewMode ? undefined : handleChange }));
27
+ }
@@ -9,8 +9,11 @@ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
9
9
  export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
10
10
  export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
11
11
  export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
12
+ export { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
12
13
  export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
13
14
  export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
14
15
  export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
15
16
  export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
16
17
  export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
18
+ export { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
19
+ export { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
@@ -9,8 +9,11 @@ export { MatchingWithLinesGroupViewer } from './MatchingWithLinesGroupViewer';
9
9
  export { CrossOutWordGroupViewer } from './CrossOutWordGroupViewer';
10
10
  export { SpontaneousQaGroupViewer } from './SpontaneousQaGroupViewer';
11
11
  export { WordOrderAndMatchGroupViewer } from './WordOrderAndMatchGroupViewer';
12
+ export { AnswerTheQuestionViewer } from './AnswerTheQuestionViewer';
12
13
  export { AnswerTheQuestionGroupViewer } from './AnswerTheQuestionGroupViewer';
13
14
  export { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
14
15
  export { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
15
16
  export { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
16
17
  export { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
18
+ export { SortWordsIntoCategoriesViewer } from './SortWordsIntoCategoriesViewer';
19
+ export { ChooseThenAnswerGroupViewer } from './ChooseThenAnswerGroupViewer';
@@ -11,9 +11,10 @@ interface ReviewAnswerTheQuestionGroupRendererProps {
11
11
  questionCount: number;
12
12
  instruction: string;
13
13
  groupContent?: string;
14
+ imageUrl?: string | string[];
14
15
  questions: AnswerTheQuestionQuestion[];
15
16
  answers: Record<string, string>;
16
17
  itemMeta: Record<string, ReviewAnswerTheQuestionGroupItemMeta>;
17
18
  }
18
- export declare function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, questions, answers, itemMeta, }: ReviewAnswerTheQuestionGroupRendererProps): import("react").JSX.Element;
19
+ export declare function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, imageUrl, questions, answers, itemMeta, }: ReviewAnswerTheQuestionGroupRendererProps): import("react").JSX.Element;
19
20
  export {};
@@ -1,10 +1,19 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { BookOpen, Check, Sparkles, Star, X } from 'lucide-react';
4
+ import { ResolvedImage } from '../../../components/common/ResolvedImage';
4
5
  import CambridgeYlePartBanner from '../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
5
6
  import CambridgeYleInstructionBanner from '../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
6
- export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, questions, answers, itemMeta, }) {
7
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), groupContent && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })] })), questions.map((question) => {
7
+ import { toAnswerTheQuestionImageUrls } from '../../questions/_shared/types/answer-the-question.type';
8
+ function ReviewImages({ imageUrl, alt, }) {
9
+ const urls = toAnswerTheQuestionImageUrls(imageUrl);
10
+ if (urls.length === 0) {
11
+ return null;
12
+ }
13
+ return (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: urls.map((url, imageIndex) => (_jsx(ResolvedImage, { src: url, alt: `${alt} ${imageIndex + 1}`, className: "max-h-56 w-auto rounded-lg object-contain" }, `${alt}-${imageIndex}`))) }));
14
+ }
15
+ export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, questionCount, instruction, groupContent, imageUrl, questions, answers, itemMeta, }) {
16
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx(ReviewImages, { imageUrl: imageUrl, alt: "Shared illustration" }), groupContent && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: groupContent } })] })), questions.map((question) => {
8
17
  const meta = itemMeta[question.id];
9
18
  const studentAnswer = answers[question.id] || '';
10
19
  const expectedAnswers = meta?.expectedAnswers || [];
@@ -16,7 +25,7 @@ export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, que
16
25
  ? 'bg-green-500'
17
26
  : isCorrect === false
18
27
  ? 'bg-red-500'
19
- : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber] }), question.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] })), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), isCorrect === true && _jsx(Check, { className: "h-4 w-4 text-green-600" }), isCorrect === false && _jsx(X, { className: "h-4 w-4 text-red-600" })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: question.question }), _jsx("div", { className: `rounded-lg border px-3 py-2 text-sm ${isCorrect === true
28
+ : 'bg-blue-500'}`, children: question.questionNumber }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber] }), question.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] })), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), isCorrect === true && _jsx(Check, { className: "h-4 w-4 text-green-600" }), isCorrect === false && _jsx(X, { className: "h-4 w-4 text-red-600" })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx(ReviewImages, { imageUrl: question.imageUrl, alt: `Hình ảnh câu ${question.questionNumber}` }), _jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: question.question }), _jsx("div", { className: `rounded-lg border px-3 py-2 text-sm ${isCorrect === true
20
29
  ? 'border-green-200 bg-green-50 text-green-800'
21
30
  : isCorrect === false
22
31
  ? 'border-red-200 bg-red-50 text-red-800'
@@ -115,7 +115,7 @@ export function renderDedicatedReviewBody(ctx) {
115
115
  feedbackVi: typeof feedback.vi === 'string' ? feedback.vi : undefined,
116
116
  }];
117
117
  }));
118
- return (_jsx(ReviewAnswerTheQuestionGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: data.questions, answers: answers, itemMeta: itemMeta }));
118
+ return (_jsx(ReviewAnswerTheQuestionGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, imageUrl: data.imageUrl, questions: data.questions, answers: answers, itemMeta: itemMeta }));
119
119
  }
120
120
  case 'SPONTANEOUS_QA_GROUP': {
121
121
  const parent = questions[0];
@@ -4,8 +4,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  * Movers fallback review cases (isReviewMode=true).
5
5
  * Source: oe-exam-fe ReviewQuestionRenderer.tsx
6
6
  */
7
- import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, } from '../../../components/exams/take/components/question-renderers';
8
- import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, } from '../../../components/exams/take/utils/question-transformers';
7
+ import { MoversAnswerTheQuestionRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, ChooseThenAnswerGroupRenderer, SortWordsIntoCategoriesRenderer, } from '../../../components/exams/take/components/question-renderers';
8
+ import { transformAnswerTheQuestion, transformFillWordHint, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, transformChooseThenAnswerGroup, transformSortWordsIntoCategories, } from '../../../components/exams/take/utils/question-transformers';
9
9
  import { normalizeCrossOutWordGroupAnswer, transformCrossOutWordGroup, } from '../../../components/exams/take/utils/cross-out-word-group.transformer';
10
10
  function asApiQuestions(questions) {
11
11
  return questions;
@@ -110,6 +110,28 @@ export function renderMoversReviewBody(ctx) {
110
110
  });
111
111
  return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerMap: correctAnswerMap }));
112
112
  }
113
+ case 'CHOOSE_THEN_ANSWER_GROUP': {
114
+ const data = transformChooseThenAnswerGroup(apiQuestions);
115
+ const gradableCount = data.questions.filter((item) => !item.isExample).length;
116
+ return (_jsx(ChooseThenAnswerGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: () => undefined, isReviewMode: true }));
117
+ }
118
+ case 'SORT_WORDS_INTO_CATEGORIES': {
119
+ const data = transformSortWordsIntoCategories(apiQuestions);
120
+ const firstQuestion = apiQuestions[0];
121
+ const stored = firstQuestion?.id ? answers[firstQuestion.id] : undefined;
122
+ const storedRecord = stored && typeof stored === 'object' && !Array.isArray(stored)
123
+ ? stored
124
+ : undefined;
125
+ const storedPlacements = Array.isArray(storedRecord?.placements)
126
+ ? storedRecord.placements
127
+ : Array.isArray(stored)
128
+ ? stored
129
+ : [];
130
+ const gradableCount = data.words.filter((word) => !word.isExample).length;
131
+ return (_jsx(SortWordsIntoCategoriesRenderer, { partNumber: partNo, partName: part.name, questionCount: gradableCount || questionCount, instruction: part.instructions || data.instruction, data: data, answers: {
132
+ [data.questionId]: { placements: storedPlacements },
133
+ }, onAnswerChange: () => undefined, isReviewMode: true }));
134
+ }
113
135
  case 'TRUE_FALSE_CORRECT_GROUP': {
114
136
  const data = transformTrueFalseCorrectGroup(apiQuestions);
115
137
  const correctAnswerMap = {};
@@ -9,6 +9,7 @@ const FLATTENED_ANSWER_TYPES = new Set([
9
9
  'ANSWER_THE_QUESTION_GROUP',
10
10
  'CROSS_OUT_WORD_GROUP',
11
11
  'TRUE_FALSE_CORRECT_GROUP',
12
+ 'CHOOSE_THEN_ANSWER_GROUP',
12
13
  'FILL_IN_BLANK_GROUP',
13
14
  'MATCHING_WITH_LINES_GROUP',
14
15
  ]);
@@ -186,6 +187,26 @@ function transformStudentAnswer(questionId, questionType, value) {
186
187
  normalizeCrossOutWordGroupAnswer(item),
187
188
  ]));
188
189
  }
190
+ if (questionType === 'CHOOSE_THEN_ANSWER_GROUP') {
191
+ const answer = Array.isArray(record.answer)
192
+ ? record.answer
193
+ : Array.isArray(value)
194
+ ? value
195
+ : [];
196
+ return Object.fromEntries(answer.map((item, index) => {
197
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
198
+ const obj = item;
199
+ return [
200
+ `${questionId}-item-${index}`,
201
+ {
202
+ optionId: typeof obj.optionId === 'string' ? obj.optionId : '',
203
+ text: typeof obj.text === 'string' ? obj.text : '',
204
+ },
205
+ ];
206
+ }
207
+ return [`${questionId}-item-${index}`, { optionId: '', text: '' }];
208
+ }));
209
+ }
189
210
  if (questionType === 'TRUE_FALSE_CORRECT_GROUP') {
190
211
  const answer = Array.isArray(record.answer)
191
212
  ? record.answer
@@ -282,6 +303,13 @@ function transformStudentAnswer(questionId, questionType, value) {
282
303
  }
283
304
  if (questionType === 'READ_AND_COLOR_OBJECTS' || questionType === 'IMAGE_OBJECT_MATCHING' || questionType === 'CROSSWORD_PUZZLE')
284
305
  return record.cells ?? record.answers ?? value;
306
+ if (questionType === 'SORT_WORDS_INTO_CATEGORIES') {
307
+ if (Array.isArray(record.placements))
308
+ return record;
309
+ if (Array.isArray(value))
310
+ return { placements: value };
311
+ return value;
312
+ }
285
313
  if (questionType === 'WRITE_A_SHORT_LETTER')
286
314
  return typeof value === 'string' ? value : record.answer ?? '';
287
315
  return record.answer ?? value;
package/dist/index.d.ts CHANGED
@@ -26,4 +26,4 @@ export { validateQuestion } from './components/questions/_shared/utils/question-
26
26
  export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
27
27
  export type { QuestionFormState } from './components/questions/_shared/hooks/useQuestionFormState';
28
28
  export * from './components/questions/_shared/types';
29
- export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
29
+ export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
package/dist/index.js CHANGED
@@ -21,4 +21,4 @@ export { QUESTION_TYPE_CONFIG, QUESTION_TYPES_WITH_DEDICATED_COMPONENTS } from '
21
21
  export { validateQuestion } from './components/questions/_shared/utils/question-validation';
22
22
  export { buildAnswerPayload } from './components/questions/_shared/utils/answer-builder';
23
23
  export * from './components/questions/_shared/types';
24
- export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, } from './components/questions/viewer';
24
+ export { QuestionViewer, QuestionGroupViewer, WordFillStructuredFormViewer, ChooseTheCorrectAnswerGroupViewer, TrueFalseGroupViewer, TrueFalseCorrectGroupViewer, FillInBlankGroupViewer, MatchingWithLinesGroupViewer, CrossOutWordGroupViewer, SpontaneousQaGroupViewer, WordOrderAndMatchGroupViewer, AnswerTheQuestionGroupViewer, MatchWordToPictureViewer, MatchWordToPictureGroupViewer, MatchColumnsToMakeSentencesViewer, WordOrderingGroupViewer, SortWordsIntoCategoriesViewer, ChooseThenAnswerGroupViewer, } from './components/questions/viewer';
@@ -84,6 +84,8 @@ export const QUESTION_TYPE_SKILLS = {
84
84
  'WORD_FILL_STRUCTURED_FORM': SkillEnum.READING,
85
85
  'CROSSWORD_PUZZLE': SkillEnum.READING,
86
86
  'FIND_WORDS_IN_MATRIX': SkillEnum.READING,
87
+ 'SORT_WORDS_INTO_CATEGORIES': SkillEnum.READING,
88
+ 'CHOOSE_THEN_ANSWER_GROUP': SkillEnum.READING,
87
89
  'MATCH_COLUMNS_TO_MAKE_SENTENCES': SkillEnum.READING,
88
90
  'MATCH_WORD_TO_PICTURE_GROUP': SkillEnum.READING,
89
91
  'WORD_ORDERING_GROUP': SkillEnum.READING,
@@ -219,6 +221,8 @@ export const QUESTION_TYPE_LABELS = {
219
221
  'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
220
222
  'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
221
223
  'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
224
+ 'SORT_WORDS_INTO_CATEGORIES': 'Sắp xếp từ vào nhóm',
225
+ 'CHOOSE_THEN_ANSWER_GROUP': 'Chọn đáp án rồi trả lời',
222
226
  'MATCH_COLUMNS_TO_MAKE_SENTENCES': 'Ghép cột thành câu',
223
227
  'MATCH_WORD_TO_PICTURE_GROUP': 'Nối từ với tranh (nhóm)',
224
228
  'WORD_ORDERING_GROUP': 'Sắp xếp từ (nhóm)',