@tinyweb_dev/oe-exam-sdk 0.2.4 → 0.2.6

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 (153) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +19 -3
  3. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
  4. package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
  6. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
  7. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
  8. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
  9. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
  10. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
  11. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
  12. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
  13. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
  14. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
  15. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
  16. package/dist/components/exams/take/components/question-renderers/index.d.ts +4 -0
  17. package/dist/components/exams/take/components/question-renderers/index.js +4 -0
  18. package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
  19. package/dist/components/exams/take/exam-taking.utils.js +9 -1
  20. package/dist/components/exams/take/types.d.ts +3 -2
  21. package/dist/components/exams/take/utils/answer-transformers.js +28 -0
  22. package/dist/components/exams/take/utils/question-transformers.d.ts +73 -1
  23. package/dist/components/exams/take/utils/question-transformers.js +188 -0
  24. package/dist/components/questions/_shared/config/question-types.config.js +24 -0
  25. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
  26. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
  27. package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
  28. package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
  29. package/dist/components/questions/_shared/types/index.d.ts +4 -0
  30. package/dist/components/questions/_shared/types/index.js +4 -0
  31. package/dist/components/questions/_shared/types/match-word-to-picture.type.d.ts +62 -0
  32. package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
  33. package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
  34. package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
  35. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
  36. package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
  37. package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
  38. package/dist/components/questions/components/QuestionSearchDropdown.js +6 -0
  39. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  40. package/dist/components/questions/creator/question-type-registry.js +8 -0
  41. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
  42. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
  43. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
  44. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
  45. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
  46. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
  47. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
  48. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  49. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
  50. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
  51. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
  52. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
  53. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
  54. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
  55. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
  56. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
  57. package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
  58. package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
  59. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
  60. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
  61. package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
  62. package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
  63. package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
  64. package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
  65. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
  66. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
  67. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
  68. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
  69. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
  70. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
  71. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
  72. package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
  73. package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
  74. package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
  75. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
  76. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
  77. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
  78. package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
  79. package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
  80. package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
  81. package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
  82. package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
  83. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
  84. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
  85. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
  86. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
  87. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
  88. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
  89. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
  90. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
  91. package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
  92. package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
  93. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
  94. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
  95. package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
  96. package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
  97. package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
  98. package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
  99. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
  100. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
  101. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
  102. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
  103. package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
  104. package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
  105. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
  106. package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
  107. package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
  108. package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
  109. package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
  110. package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
  111. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +11 -1
  112. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
  113. package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
  114. package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
  115. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
  116. package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
  117. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
  118. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
  119. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
  120. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
  121. package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
  122. package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
  123. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
  124. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
  125. package/dist/components/questions/viewer/QuestionViewer.js +24 -3
  126. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
  127. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
  128. package/dist/components/questions/viewer/index.d.ts +5 -0
  129. package/dist/components/questions/viewer/index.js +5 -0
  130. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
  131. package/dist/components/results/renderers/review-question-body-movers.js +74 -3
  132. package/dist/components/results/review-data.js +59 -0
  133. package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
  134. package/dist/index.d.ts +1 -1
  135. package/dist/index.js +1 -1
  136. package/dist/shared/constants/question-skills.js +8 -0
  137. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
  138. package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
  139. package/dist/shared/lib/utils/question-reverse-transform.js +142 -0
  140. package/dist/shared/lib/utils/question-transform.js +8 -0
  141. package/dist/shared/types/common.types.d.ts +1 -1
  142. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  143. package/dist/shared/types/questions/fill-in-blank-group.d.ts +45 -0
  144. package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
  145. package/dist/shared/types/questions/index.d.ts +4 -0
  146. package/dist/shared/types/questions/index.js +8 -0
  147. package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
  148. package/dist/shared/types/questions/match-word-to-picture.js +5 -0
  149. package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
  150. package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
  151. package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
  152. package/dist/shared/types/questions/true-false-correct-group.js +1 -0
  153. package/package.json +7 -2
@@ -0,0 +1,104 @@
1
+ import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.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 nextRightId(existing) {
12
+ let n = existing.length + 1;
13
+ const used = new Set(existing);
14
+ while (used.has(`r${n}`))
15
+ n += 1;
16
+ return `r${n}`;
17
+ }
18
+ export function createEmptyMatchingPair(questionNumber, rightId) {
19
+ return {
20
+ rightItem: { id: rightId, text: '' },
21
+ item: {
22
+ imageUrl: '',
23
+ correctAnswer: rightId,
24
+ isExample: false,
25
+ points: 1,
26
+ questionNumber,
27
+ },
28
+ };
29
+ }
30
+ export function mapMatchingWithLinesGroupItem(item, index, fallbackAnswer) {
31
+ const record = asRecord(item);
32
+ const content = asRecord(record.content);
33
+ const correctAnswer = asRecord(record.correctAnswer);
34
+ const isExample = record.isExample === true;
35
+ const answer = asString(correctAnswer.answer) ||
36
+ (typeof fallbackAnswer === 'string' ? fallbackAnswer : '');
37
+ return {
38
+ imageUrl: asString(content.imageUrl) || asString(record.imageUrl),
39
+ correctAnswer: answer,
40
+ isExample,
41
+ points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
42
+ questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
43
+ };
44
+ }
45
+ function mapRightItems(raw) {
46
+ if (!Array.isArray(raw))
47
+ return [];
48
+ return raw.map((item, index) => {
49
+ const record = asRecord(item);
50
+ return {
51
+ id: asString(record.id, `r${index + 1}`),
52
+ text: asString(record.text),
53
+ };
54
+ });
55
+ }
56
+ export function mapQuestionToMatchingWithLinesGroupData(question) {
57
+ const source = asRecord(question);
58
+ const content = asRecord(source.content);
59
+ const answer = asRecord(source.answer);
60
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
61
+ const fallbackAnswers = Array.isArray(correctAnswer.answer)
62
+ ? correctAnswer.answer
63
+ : Array.isArray(answer.answer)
64
+ ? answer.answer
65
+ : [];
66
+ const explanation = asString(source.explanation) || asString(answer.explanation);
67
+ const meta = asRecord(content.meta ?? answer.meta);
68
+ if (Array.isArray(content.items)) {
69
+ const items = content.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
70
+ return {
71
+ title: asString(meta.title) || asString(answer.title),
72
+ instruction: asString(meta.instruction) ||
73
+ asString(answer.instruction) ||
74
+ MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
75
+ rightItems: mapRightItems(content.rightItems),
76
+ items,
77
+ explanation,
78
+ points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
79
+ };
80
+ }
81
+ if (Array.isArray(answer.items)) {
82
+ const items = answer.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
83
+ return {
84
+ title: asString(answer.title),
85
+ instruction: asString(answer.instruction) || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
86
+ rightItems: mapRightItems(answer.rightItems),
87
+ items,
88
+ explanation,
89
+ points: typeof answer.points === 'number'
90
+ ? answer.points
91
+ : items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
92
+ };
93
+ }
94
+ const first = createEmptyMatchingPair(1, 'r1');
95
+ const second = createEmptyMatchingPair(2, 'r2');
96
+ return {
97
+ title: '',
98
+ instruction: MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
99
+ rightItems: [first.rightItem, second.rightItem],
100
+ items: [first.item, second.item],
101
+ explanation,
102
+ points: 2,
103
+ };
104
+ }
@@ -0,0 +1,3 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export { createEmptyMatchingPair, mapMatchingWithLinesGroupItem, mapQuestionToMatchingWithLinesGroupData, nextRightId, } from './map-matching-with-lines-group-data';
3
+ export declare const matchingWithLinesGroupRegistration: QuestionTypeRegistration;
@@ -0,0 +1,37 @@
1
+ import { MatchingWithLinesGroupCreator } from './MatchingWithLinesGroupCreator';
2
+ import { mapQuestionToMatchingWithLinesGroupData } from './map-matching-with-lines-group-data';
3
+ export { createEmptyMatchingPair, mapMatchingWithLinesGroupItem, mapQuestionToMatchingWithLinesGroupData, nextRightId, } from './map-matching-with-lines-group-data';
4
+ export const matchingWithLinesGroupRegistration = {
5
+ component: MatchingWithLinesGroupCreator,
6
+ transformInitialData: (initialData) => mapQuestionToMatchingWithLinesGroupData(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 transformMatchingWithLinesGroup: TransformHandler;
@@ -0,0 +1,44 @@
1
+ import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
2
+ function sumGradablePoints(items) {
3
+ return items.reduce((total, item) => {
4
+ if (item.isExample)
5
+ return total;
6
+ const points = typeof item.points === 'number' ? item.points : 1;
7
+ return total + points;
8
+ }, 0);
9
+ }
10
+ export const transformMatchingWithLinesGroup = (question) => {
11
+ const answerData = question.answer;
12
+ const items = Array.isArray(answerData?.items) ? answerData.items : [];
13
+ const rightItems = Array.isArray(answerData?.rightItems) ? answerData.rightItems : [];
14
+ if (items.length === 0) {
15
+ return { apiContent: { rightItems: [], items: [] }, apiCorrectAnswer: { answer: [] } };
16
+ }
17
+ const apiItems = items.map((item, index) => ({
18
+ ...(item.isExample ? { isExample: true } : {}),
19
+ content: {
20
+ imageUrl: item.imageUrl || '',
21
+ },
22
+ correctAnswer: { answer: item.correctAnswer || '' },
23
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
24
+ questionNumber: item.questionNumber || index + 1,
25
+ }));
26
+ const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
27
+ return {
28
+ apiContent: {
29
+ meta: {
30
+ instruction: answerData?.instruction || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
31
+ ...(title ? { title } : {}),
32
+ },
33
+ rightItems: rightItems.map((item) => ({
34
+ id: item.id || '',
35
+ text: item.text || '',
36
+ })),
37
+ items: apiItems,
38
+ },
39
+ apiCorrectAnswer: {
40
+ answer: apiItems.map((item) => item.correctAnswer.answer),
41
+ },
42
+ totalPoints: sumGradablePoints(items),
43
+ };
44
+ };
@@ -0,0 +1,2 @@
1
+ import type { TrueFalseCorrectGroupClientProps } from '../../_shared/types/true-false-correct-group.type';
2
+ export declare function TrueFalseCorrectGroupClient({ questionData, isReviewMode, userAnswers, }: TrueFalseCorrectGroupClientProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
4
+ import { cn } from '../../../../shared/lib/utils';
5
+ export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
6
+ const items = questionData.items || [];
7
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => {
8
+ const userValue = userAnswers[itemIndex];
9
+ const correctIsTrue = item.isTrue === true;
10
+ return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.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: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "flex items-center gap-3", children: [
11
+ { value: true, label: 'True' },
12
+ { value: false, label: 'False' },
13
+ ].map((opt) => {
14
+ const isCorrect = opt.value === correctIsTrue;
15
+ const isSelected = userValue?.isTrue === opt.value;
16
+ return (_jsxs("div", { className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', isReviewMode && isCorrect
17
+ ? 'border-green-500 bg-green-50 text-green-700'
18
+ : isReviewMode && isSelected && !isCorrect
19
+ ? 'border-red-500 bg-red-50 text-red-700'
20
+ : isSelected
21
+ ? 'border-blue-500 bg-blue-50 text-blue-700'
22
+ : 'border-gray-200 bg-white text-gray-700'), children: [opt.value ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(X, { className: "h-4 w-4" }), _jsx("span", { children: opt.label })] }, String(opt.value)));
23
+ }) }), !correctIsTrue && (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50/70 px-3 py-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-amber-800", children: "C\u00E2u \u0111\u00FAng" }), _jsx("p", { className: "mt-1 text-sm text-amber-900", children: item.correction || 'Chưa nhập câu sửa' }), isReviewMode && userValue && userValue.isTrue === false && (_jsxs("p", { className: "mt-2 text-xs text-slate-600", children: ["H\u1ECDc sinh: ", userValue.correction || '(trống)'] }))] }))] })] }, `${item.questionNumber}-${itemIndex}`));
24
+ }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { TrueFalseCorrectGroupCreatorProps } from '../../_shared/types/true-false-correct-group.type';
2
+ export declare function TrueFalseCorrectGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: TrueFalseCorrectGroupCreatorProps): import("react").JSX.Element;
@@ -0,0 +1,144 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { Button } from '../../../../components/ui/button';
5
+ import { Label } from '../../../../components/ui/label';
6
+ import { Input } from '../../../../components/ui/input';
7
+ import { Textarea } from '../../../../components/ui/textarea';
8
+ import { PointsInput } from '../../../../components/ui/points-input';
9
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
10
+ import { Switch } from '../../../../components/ui/switch';
11
+ import { useDebouncedCallback } from '../../../../shared/lib/hooks';
12
+ import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
13
+ import { TrueFalseCorrectGroupClient } from './TrueFalseCorrectGroupClient';
14
+ import { TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/true-false-correct-group.type';
15
+ function parsePointsValue(raw, allowZero = false) {
16
+ if (raw === '')
17
+ return allowZero ? 0 : 1;
18
+ const parsed = parseInt(raw.replace(/[^0-9]/g, ''), 10);
19
+ if (Number.isNaN(parsed))
20
+ return allowZero ? 0 : 1;
21
+ const min = allowZero ? 0 : 1;
22
+ return Math.max(min, parsed);
23
+ }
24
+ function createEmptyItem(questionNumber) {
25
+ return {
26
+ question: '',
27
+ isTrue: true,
28
+ correction: '',
29
+ isExample: false,
30
+ points: 1,
31
+ questionNumber,
32
+ };
33
+ }
34
+ function normalizeItems(rawItems) {
35
+ if (!Array.isArray(rawItems) || rawItems.length === 0) {
36
+ return [createEmptyItem(1)];
37
+ }
38
+ return rawItems.map((item, index) => {
39
+ const isTrue = item.isTrue === true;
40
+ return {
41
+ question: item.question || '',
42
+ isTrue,
43
+ correction: isTrue ? '' : (item.correction || ''),
44
+ isExample: item.isExample === true,
45
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
46
+ questionNumber: item.questionNumber || index + 1,
47
+ };
48
+ });
49
+ }
50
+ function sumPoints(items) {
51
+ return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
52
+ }
53
+ export function TrueFalseCorrectGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
54
+ const [instruction, setInstruction] = useState(initialData?.instruction || TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION);
55
+ const [items, setItems] = useState(() => normalizeItems(initialData?.items));
56
+ const [explanation, setExplanation] = useState(initialData?.explanation || '');
57
+ const [isClientMode, setIsClientMode] = useState(false);
58
+ const [errors, setErrors] = useState([]);
59
+ const onChangeRef = useRef(onChange);
60
+ const previousPayloadRef = useRef('');
61
+ useEffect(() => {
62
+ onChangeRef.current = onChange;
63
+ }, [onChange]);
64
+ const buildPayload = (nextInstruction = instruction, nextItems = items, nextExplanation = explanation) => ({
65
+ instruction: nextInstruction,
66
+ items: nextItems,
67
+ explanation: nextExplanation,
68
+ points: sumPoints(nextItems),
69
+ });
70
+ const debouncedOnChange = useDebouncedCallback((payload) => {
71
+ onChangeRef.current?.(payload);
72
+ }, 300);
73
+ useEffect(() => {
74
+ const payload = buildPayload();
75
+ const serialized = JSON.stringify(payload);
76
+ if (serialized !== previousPayloadRef.current) {
77
+ previousPayloadRef.current = serialized;
78
+ debouncedOnChange(payload);
79
+ onUnsavedChangesChange?.(true);
80
+ }
81
+ }, [instruction, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
82
+ const validate = () => {
83
+ const nextErrors = [];
84
+ if (!instruction.trim()) {
85
+ nextErrors.push('Hướng dẫn (Instruction) không được để trống.');
86
+ }
87
+ if (items.length === 0) {
88
+ nextErrors.push('Cần ít nhất một câu hỏi trong nhóm.');
89
+ }
90
+ items.forEach((item, index) => {
91
+ if (!item.question.trim()) {
92
+ nextErrors.push(`Câu ${index + 1}: Nội dung câu hỏi không được để trống.`);
93
+ }
94
+ if (item.isTrue === false && !item.correction.trim()) {
95
+ nextErrors.push(`Câu ${index + 1}: Câu False cần nhập câu sửa đúng.`);
96
+ }
97
+ });
98
+ setErrors(nextErrors);
99
+ return nextErrors.length === 0;
100
+ };
101
+ useEffect(() => {
102
+ if (validationRef) {
103
+ validationRef.current = {
104
+ triggerValidation: async () => validate(),
105
+ getFormData: () => buildPayload(),
106
+ };
107
+ }
108
+ }, [validationRef, instruction, items, explanation]);
109
+ const updateItem = (index, patch) => {
110
+ setItems((prev) => {
111
+ const next = [...prev];
112
+ const current = next[index];
113
+ if (!current)
114
+ return prev;
115
+ next[index] = { ...current, ...patch };
116
+ return next;
117
+ });
118
+ };
119
+ const setItemTruth = (index, isTrue) => {
120
+ updateItem(index, {
121
+ isTrue,
122
+ correction: isTrue ? '' : items[index]?.correction || '',
123
+ });
124
+ };
125
+ const addItem = () => {
126
+ setItems((prev) => [...prev, createEmptyItem(prev.length + 1)]);
127
+ };
128
+ const removeItem = (index) => {
129
+ setItems((prev) => {
130
+ if (prev.length <= 1)
131
+ return prev;
132
+ const filtered = prev.filter((_, i) => i !== index);
133
+ return filtered.map((item, i) => ({ ...item, questionNumber: i + 1 }));
134
+ });
135
+ };
136
+ const currentPayload = buildPayload();
137
+ const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
138
+ return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-medium text-gray-900", children: "T\u1EA1o c\u00E2u h\u1ECFi \u0110\u00FAng / Sai + S\u1EEDa c\u00E2u (Nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "Tick/Cross t\u1EEBng c\u00E2u. C\u00E2u False b\u1EAFt bu\u1ED9c vi\u1EBFt l\u1EA1i c\u00E2u \u0111\u00FAng." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode((prev) => !prev), className: "gap-2", children: [isClientMode ? _jsx(Pencil, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }), isClientMode ? 'Chế độ chỉnh sửa' : 'Xem trước học sinh'] })] }), displayErrors.length > 0 && (_jsx("div", { className: "rounded-lg bg-red-50 p-4 border border-red-200", children: _jsx("div", { className: "flex", children: _jsxs("div", { className: "ml-3", children: [_jsx("h3", { className: "text-sm font-medium text-red-800", children: "C\u1EA7n s\u1EEDa c\u00E1c l\u1ED7i sau:" }), _jsx("div", { className: "mt-2 text-sm text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-5", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })] }) }) })), isClientMode ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Xem tr\u01B0\u1EDBc hi\u1EC3n th\u1ECB h\u1ECDc sinh" }) }), _jsx(CardContent, { children: _jsx(TrueFalseCorrectGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "H\u01B0\u1EDBng d\u1EABn chung (Instruction)" }) }), _jsx(CardContent, { className: "space-y-4", children: _jsxs("div", { children: [_jsx(Label, { htmlFor: "instruction", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION, className: "mt-1" })] }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["Danh s\u00E1ch c\u00E2u h\u1ECFi (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u00E2u h\u1ECFi"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-4 rounded-lg border border-gray-200 bg-gray-50/50 p-4 transition-colors hover:border-gray-300", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700", children: item.questionNumber || index + 1 }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u h\u1ECFi ", index + 1] })] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, {
139
+ isExample: checked,
140
+ points: checked ? 0 : 1,
141
+ }) }), _jsxs(Label, { htmlFor: `example-${index}`, className: "flex items-center gap-1 text-xs cursor-pointer", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "C\u00E2u m\u1EABu (Example)"] })] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
142
+ points: parsePointsValue(event.target.value, true),
143
+ }) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(index), className: "text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `question-${index}`, children: "N\u1ED9i dung c\u00E2u h\u1ECFi" }), _jsx(Textarea, { id: `question-${index}`, value: item.question, onChange: (e) => updateItem(index, { question: e.target.value }), placeholder: "Nh\u1EADp n\u1ED9i dung c\u00E2u h\u1ECFi...", className: "mt-1 bg-white", rows: 2 })] }), _jsxs("div", { children: [_jsx(Label, { className: "mb-2 block text-xs font-medium text-gray-700", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Button, { type: "button", variant: item.isTrue === true ? 'default' : 'outline', size: "sm", onClick: () => setItemTruth(index, true), className: "w-24", children: "True" }), _jsx(Button, { type: "button", variant: item.isTrue === false ? 'default' : 'outline', size: "sm", onClick: () => setItemTruth(index, false), className: "w-24", children: "False" })] })] }), item.isTrue === false && (_jsxs("div", { children: [_jsx(Label, { htmlFor: `correction-${index}`, children: "C\u00E2u s\u1EEDa \u0111\u00FAng" }), _jsx(Textarea, { id: `correction-${index}`, value: item.correction, onChange: (e) => updateItem(index, { correction: e.target.value }), placeholder: "Vi\u1EBFt l\u1EA1i c\u00E2u \u0111\u00FAng...", className: "mt-1 bg-white", rows: 2 })] }))] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "explanation", value: explanation, onChange: (event) => setExplanation(event.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3 }) })] })] }))] }));
144
+ }
@@ -0,0 +1,5 @@
1
+ export * from './TrueFalseCorrectGroupCreator';
2
+ export * from './TrueFalseCorrectGroupClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-true-false-correct-group-data';
@@ -0,0 +1,5 @@
1
+ export * from './TrueFalseCorrectGroupCreator';
2
+ export * from './TrueFalseCorrectGroupClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-true-false-correct-group-data';
@@ -0,0 +1,4 @@
1
+ import type { TrueFalseCorrectGroupData, TrueFalseCorrectGroupItemData } from '../../_shared/types/true-false-correct-group.type';
2
+ export declare function createEmptyTrueFalseCorrectGroupItem(questionNumber: number): TrueFalseCorrectGroupItemData;
3
+ export declare function mapTrueFalseCorrectGroupItem(item: unknown, index: number, fallbackAnswer?: unknown): TrueFalseCorrectGroupItemData;
4
+ export declare function mapQuestionToTrueFalseCorrectGroupData(question: Record<string, unknown> | null | undefined): TrueFalseCorrectGroupData;
@@ -0,0 +1,79 @@
1
+ import { TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/true-false-correct-group.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
+ function parseItemAnswer(raw) {
12
+ if (!isRecord(raw)) {
13
+ return { isTrue: true, correction: '' };
14
+ }
15
+ const isTrue = raw.isTrue === true;
16
+ return {
17
+ isTrue,
18
+ correction: isTrue ? '' : asString(raw.correction).trim(),
19
+ };
20
+ }
21
+ export function createEmptyTrueFalseCorrectGroupItem(questionNumber) {
22
+ return {
23
+ question: '',
24
+ isTrue: true,
25
+ correction: '',
26
+ isExample: false,
27
+ points: 1,
28
+ questionNumber,
29
+ };
30
+ }
31
+ export function mapTrueFalseCorrectGroupItem(item, index, fallbackAnswer) {
32
+ const record = asRecord(item);
33
+ const content = asRecord(record.content);
34
+ const correctAnswer = asRecord(record.correctAnswer);
35
+ const isExample = record.isExample === true;
36
+ const parsed = parseItemAnswer(correctAnswer.answer ?? fallbackAnswer);
37
+ return {
38
+ question: asString(content.question) || asString(record.question),
39
+ isTrue: parsed.isTrue,
40
+ correction: parsed.correction,
41
+ isExample,
42
+ points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
43
+ questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
44
+ };
45
+ }
46
+ export function mapQuestionToTrueFalseCorrectGroupData(question) {
47
+ const source = asRecord(question);
48
+ const content = asRecord(source.content);
49
+ const answer = asRecord(source.answer);
50
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
51
+ const explanation = String(source.explanation || answer.explanation || '');
52
+ const fallbackAnswers = Array.isArray(correctAnswer.answer)
53
+ ? correctAnswer.answer
54
+ : [];
55
+ if (Array.isArray(content.items)) {
56
+ const items = content.items.map((item, index) => mapTrueFalseCorrectGroupItem(item, index, fallbackAnswers[index]));
57
+ return {
58
+ instruction: asString(asRecord(content.meta).instruction)
59
+ || TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION,
60
+ items,
61
+ explanation,
62
+ points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
63
+ };
64
+ }
65
+ if (Array.isArray(answer.items)) {
66
+ return {
67
+ instruction: asString(answer.instruction, TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION),
68
+ items: answer.items.map((item, index) => mapTrueFalseCorrectGroupItem(item, index, fallbackAnswers[index])),
69
+ explanation,
70
+ points: typeof answer.points === 'number' ? answer.points : undefined,
71
+ };
72
+ }
73
+ return {
74
+ instruction: TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION,
75
+ items: [createEmptyTrueFalseCorrectGroupItem(1)],
76
+ explanation,
77
+ points: 1,
78
+ };
79
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export declare const trueFalseCorrectGroupRegistration: QuestionTypeRegistration;
@@ -0,0 +1,36 @@
1
+ import { TrueFalseCorrectGroupCreator } from './TrueFalseCorrectGroupCreator';
2
+ import { mapQuestionToTrueFalseCorrectGroupData } from './map-true-false-correct-group-data';
3
+ export const trueFalseCorrectGroupRegistration = {
4
+ component: TrueFalseCorrectGroupCreator,
5
+ transformInitialData: (initialData) => mapQuestionToTrueFalseCorrectGroupData(initialData),
6
+ getExtraProps: (ctx) => ({
7
+ onUnsavedChangesChange: ctx.onUnsavedChangesChange,
8
+ validationRef: ctx.validationRef,
9
+ }),
10
+ wrapOnSave: (data, ctx) => {
11
+ const payload = data;
12
+ return {
13
+ type: ctx.questionType,
14
+ content: ctx.state.content,
15
+ points: payload.points ?? ctx.state.points ?? 1,
16
+ level: ctx.state.level,
17
+ difficulty: ctx.state.difficulty,
18
+ skill: ctx.state.skill,
19
+ answer: payload,
20
+ explanation: payload.explanation || '',
21
+ };
22
+ },
23
+ wrapOnChange: (data, ctx) => {
24
+ const payload = data;
25
+ return {
26
+ type: ctx.questionType,
27
+ content: ctx.state.content,
28
+ points: payload.points ?? ctx.state.points ?? 1,
29
+ level: ctx.state.level,
30
+ difficulty: ctx.state.difficulty,
31
+ skill: ctx.state.skill,
32
+ answer: payload,
33
+ explanation: payload.explanation || '',
34
+ };
35
+ },
36
+ };
@@ -0,0 +1,2 @@
1
+ import type { TransformHandler } from '../../../../shared/lib/utils/question-transform-types';
2
+ export declare const transformTrueFalseCorrectGroup: TransformHandler;
@@ -0,0 +1,49 @@
1
+ import { TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION, TRUE_FALSE_CORRECT_ITEM_TYPE, } from '../../_shared/types/true-false-correct-group.type';
2
+ function sumGradablePoints(items) {
3
+ return items.reduce((total, item) => {
4
+ if (item.isExample) {
5
+ return total;
6
+ }
7
+ const points = typeof item.points === 'number' ? item.points : 1;
8
+ return total + points;
9
+ }, 0);
10
+ }
11
+ function toItemAnswer(item) {
12
+ const isTrue = item.isTrue === true;
13
+ return {
14
+ isTrue,
15
+ correction: isTrue ? '' : (item.correction || '').trim(),
16
+ };
17
+ }
18
+ export const transformTrueFalseCorrectGroup = (question) => {
19
+ const answerData = question.answer;
20
+ const items = Array.isArray(answerData?.items) ? answerData.items : [];
21
+ if (items.length === 0) {
22
+ return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
23
+ }
24
+ const apiItems = items.map((item, index) => {
25
+ const correctAnswer = toItemAnswer(item);
26
+ return {
27
+ questionType: TRUE_FALSE_CORRECT_ITEM_TYPE,
28
+ ...(item.isExample ? { isExample: true } : {}),
29
+ content: {
30
+ question: item.question || '',
31
+ },
32
+ correctAnswer: { answer: correctAnswer },
33
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
34
+ questionNumber: item.questionNumber || index + 1,
35
+ };
36
+ });
37
+ return {
38
+ apiContent: {
39
+ meta: {
40
+ instruction: answerData?.instruction || TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION,
41
+ },
42
+ items: apiItems,
43
+ },
44
+ apiCorrectAnswer: {
45
+ answer: apiItems.map((item) => item.correctAnswer.answer),
46
+ },
47
+ totalPoints: sumGradablePoints(items),
48
+ };
49
+ };
@@ -6,6 +6,15 @@ import { cn } from '../../../../shared/lib/utils';
6
6
  import { HtmlOrText } from '../../../../shared/lib/html-content';
7
7
  import { FileText, PenLine, Check, AlertCircle, BookOpen, Lightbulb, ImageIcon } from 'lucide-react';
8
8
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks/usePresignedFileUrl';
9
+ function normalizePassage(raw) {
10
+ if (typeof raw === 'string' && raw.trim()) {
11
+ return [raw];
12
+ }
13
+ if (Array.isArray(raw)) {
14
+ return raw.filter((item) => typeof item === 'string' && item.trim().length > 0);
15
+ }
16
+ return [];
17
+ }
9
18
  // Component to render a single guide image in client mode
10
19
  function GuideImageDisplay({ imageKey }) {
11
20
  const { previewUrl } = usePresignedFileUrl(imageKey);
@@ -33,6 +42,7 @@ export function WriteAShortLetterClient({ questionData, onSubmit, isReviewMode =
33
42
  : Array.isArray(questionData.images)
34
43
  ? questionData.images
35
44
  : [];
45
+ const passageList = normalizePassage(questionData.passage ?? questionData.passages);
36
46
  // Initialize answer state - ensure it's always a string
37
47
  const [answer, setAnswer] = useState(typeof userAnswer === 'string' ? userAnswer : '');
38
48
  // Sync userAnswer when it changes (e.g., when loading from server in review mode)
@@ -75,7 +85,7 @@ export function WriteAShortLetterClient({ questionData, onSubmit, isReviewMode =
75
85
  const target = minWords;
76
86
  return Math.min((wordCount / target) * 100, 100);
77
87
  }, [wordCount, minWords]);
78
- return (_jsxs("div", { className: "space-y-5", children: [instruction && (_jsx("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 shadow-sm", children: _jsxs("div", { className: "flex items-center gap-3 px-5 py-4", children: [_jsx("div", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm", children: _jsx(BookOpen, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-blue-900", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(HtmlOrText, { value: instruction, className: "text-sm text-blue-700" })] })] }) })), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 text-white shadow-sm", children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "\u1EA2nh h\u01B0\u1EDBng d\u1EABn" })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "flex flex-col gap-4", children: guideImages.map((imageKey, index) => (_jsx(GuideImageDisplay, { imageKey: imageKey }, `${imageKey}-${index}`))) }) })] })), informations && informations.filter((i) => i?.trim()).length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 text-white shadow-sm", children: _jsx(FileText, { className: "h-4 w-4" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["Th\u00F4ng tin c\u1EA7n bao g\u1ED3m (", informations.length, ")"] })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "space-y-3", children: informations.filter((i) => i?.trim()).map((info, index) => (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-gray-100 bg-gradient-to-r from-white to-gray-50 p-3 transition-all hover:border-gray-200 hover:shadow-sm", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-teal-500 text-xs font-bold text-white shadow-sm", children: index + 1 }), _jsx(HtmlOrText, { value: info, className: "text-sm text-gray-700 leading-relaxed pt-0.5" })] }, index))) }) })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 text-white shadow-sm", children: _jsx(PenLine, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Vi\u1EBFt th\u01B0 c\u1EE7a b\u1EA1n" })] }), _jsxs("div", { className: cn('flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-semibold transition-all', isExceeded
88
+ return (_jsxs("div", { className: "space-y-5", children: [instruction && (_jsx("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 shadow-sm", children: _jsxs("div", { className: "flex items-center gap-3 px-5 py-4", children: [_jsx("div", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm", children: _jsx(BookOpen, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-blue-900", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(HtmlOrText, { value: instruction, className: "text-sm text-blue-700" })] })] }) })), passageList.length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-indigo-100 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-indigo-50 bg-gradient-to-r from-indigo-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-white shadow-sm", children: _jsx(BookOpen, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-indigo-800", children: "B\u00E0i \u0111\u1ECDc" })] }), _jsx("div", { className: "space-y-3 p-5", children: passageList.map((item, index) => (_jsx(HtmlOrText, { value: item, className: "text-sm leading-relaxed text-gray-800" }, index))) })] })), guideImages && guideImages.length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 text-white shadow-sm", children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "\u1EA2nh h\u01B0\u1EDBng d\u1EABn" })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "flex flex-col gap-4", children: guideImages.map((imageKey, index) => (_jsx(GuideImageDisplay, { imageKey: imageKey }, `${imageKey}-${index}`))) }) })] })), informations && informations.filter((i) => i?.trim()).length > 0 && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 text-white shadow-sm", children: _jsx(FileText, { className: "h-4 w-4" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["Th\u00F4ng tin c\u1EA7n bao g\u1ED3m (", informations.length, ")"] })] }), _jsx("div", { className: "p-5", children: _jsx("div", { className: "space-y-3", children: informations.filter((i) => i?.trim()).map((info, index) => (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-gray-100 bg-gradient-to-r from-white to-gray-50 p-3 transition-all hover:border-gray-200 hover:shadow-sm", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-teal-500 text-xs font-bold text-white shadow-sm", children: index + 1 }), _jsx(HtmlOrText, { value: info, className: "text-sm text-gray-700 leading-relaxed pt-0.5" })] }, index))) }) })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 text-white shadow-sm", children: _jsx(PenLine, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Vi\u1EBFt th\u01B0 c\u1EE7a b\u1EA1n" })] }), _jsxs("div", { className: cn('flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-semibold transition-all', isExceeded
79
89
  ? 'bg-red-100 text-red-700'
80
90
  : isValidWordCount
81
91
  ? 'bg-green-100 text-green-700'