@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,68 @@
1
+ import { FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION, FILL_IN_BLANK_ITEM_TYPE, } from '../../_shared/types/fill-in-blank-group.type';
2
+ import { blankSlotCount, extractPlaceholderIndices, normalizeAnswersMatrix, } from './blank-utils';
3
+ function sumGradablePoints(items) {
4
+ return items.reduce((total, item) => {
5
+ if (item.isExample)
6
+ return total;
7
+ const points = typeof item.points === 'number' ? item.points : 1;
8
+ return total + points;
9
+ }, 0);
10
+ }
11
+ function toItemAnswer(item) {
12
+ const indices = extractPlaceholderIndices(item.question || '');
13
+ const requiredLength = blankSlotCount(item.question || '');
14
+ return {
15
+ answers: normalizeAnswersMatrix(item.answers, requiredLength).map((alts, index) => {
16
+ if (!indices.includes(index))
17
+ return [];
18
+ return alts.length > 0 ? alts : [''];
19
+ }),
20
+ caseSensitive: item.caseSensitive === true,
21
+ };
22
+ }
23
+ export const transformFillInBlankGroup = (question) => {
24
+ const answerData = question.answer;
25
+ const items = Array.isArray(answerData?.items) ? answerData.items : [];
26
+ if (items.length === 0) {
27
+ return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
28
+ }
29
+ const apiItems = items.map((item, index) => {
30
+ const correctAnswer = toItemAnswer(item);
31
+ const hints = Array.isArray(item.hints)
32
+ ? item.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
33
+ : [];
34
+ return {
35
+ questionType: FILL_IN_BLANK_ITEM_TYPE,
36
+ ...(item.isExample ? { isExample: true } : {}),
37
+ content: {
38
+ question: item.question || '',
39
+ ...(item.imageUrl ? { imageUrl: item.imageUrl } : {}),
40
+ ...(item.audioUrl ? { audioUrl: item.audioUrl } : {}),
41
+ ...(item.showHints ? { showHints: true } : {}),
42
+ ...(hints.length > 0 ? { hints } : {}),
43
+ },
44
+ correctAnswer,
45
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
46
+ questionNumber: item.questionNumber || index + 1,
47
+ };
48
+ });
49
+ const groupHints = Array.isArray(answerData?.hints)
50
+ ? answerData.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
51
+ : [];
52
+ return {
53
+ apiContent: {
54
+ meta: {
55
+ instruction: answerData?.instruction || FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION,
56
+ ...(answerData?.audioUrl ? { audioUrl: answerData.audioUrl } : {}),
57
+ ...(answerData?.imageUrl ? { imageUrl: answerData.imageUrl } : {}),
58
+ ...(answerData?.showHints ? { showHints: true } : {}),
59
+ ...(groupHints.length > 0 ? { hints: groupHints } : {}),
60
+ },
61
+ items: apiItems,
62
+ },
63
+ apiCorrectAnswer: {
64
+ answer: apiItems.map((item) => item.correctAnswer),
65
+ },
66
+ totalPoints: sumGradablePoints(items),
67
+ };
68
+ };
@@ -0,0 +1,2 @@
1
+ import { MatchWordToPictureClientProps } from '../../_shared/types/match-word-to-picture.type';
2
+ export declare function MatchWordToPictureClient({ questionData, onSubmit, isReviewMode, userAnswer, autoFillCorrectAnswers, explanation, }: MatchWordToPictureClientProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Card, CardContent } from '../../../../components/ui/card';
4
+ import { Label } from '../../../../components/ui/label';
5
+ import { BookOpen, CheckCircle2, XCircle } from 'lucide-react';
6
+ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
7
+ import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture.type';
8
+ import { useState } from 'react';
9
+ export function MatchWordToPictureClient({ questionData, onSubmit, isReviewMode = false, userAnswer, autoFillCorrectAnswers = false, explanation, }) {
10
+ const [answer, setAnswer] = useState(userAnswer || '');
11
+ const { previewUrl: questionImageUrl } = usePresignedFileUrl(questionData.imageUrl || '');
12
+ const wordBank = questionData.wordBank || [];
13
+ if (!questionData.imageUrl) {
14
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "p-6", children: _jsx("p", { className: "text-gray-500", children: "Kh\u00F4ng c\u00F3 d\u1EEF li\u1EC7u c\u00E2u h\u1ECFi" }) }) }));
15
+ }
16
+ const isExample = questionData.isExample || false;
17
+ const displayedAnswer = autoFillCorrectAnswers || isExample
18
+ ? questionData.correctAnswer
19
+ : answer;
20
+ const isCorrect = isReviewMode && userAnswer !== undefined
21
+ ? userAnswer.trim().toLowerCase() === questionData.correctAnswer.trim().toLowerCase()
22
+ : undefined;
23
+ return (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-violet-400 to-indigo-500" }), _jsxs(CardContent, { className: "space-y-6 p-6", children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-violet-700", children: questionData.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION }), wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-violet-200 bg-gradient-to-r from-violet-50 to-indigo-50 p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-violet-700", children: "Word bank:" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => (_jsx("span", { className: "rounded-full border border-violet-200 bg-white px-3 py-1 text-sm font-semibold text-slate-700", children: word }, `${word}-${index}`))) })] })), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5", children: [isExample && (_jsxs("div", { className: "mb-2 inline-flex items-center gap-1.5 rounded-full border border-amber-300 bg-amber-100 px-3 py-1", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5 text-amber-600" }), _jsx("span", { className: "text-xs font-semibold uppercase tracking-wide text-amber-700", children: "Example" })] })), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "relative h-24 w-24 flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm", children: _jsx("img", { src: questionImageUrl || questionData.imageUrl, alt: questionData.correctAnswer || 'Picture', className: "h-full w-full object-contain" }) }), _jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Label, { className: "text-base font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("select", { value: displayedAnswer, onChange: (event) => {
24
+ setAnswer(event.target.value);
25
+ onSubmit?.(event.target.value);
26
+ }, disabled: isReviewMode || isExample, className: `h-10 max-w-xs rounded-md border-2 bg-white px-3 text-sm font-medium ${isReviewMode
27
+ ? isCorrect
28
+ ? 'border-green-400 bg-green-50 text-green-700'
29
+ : 'border-red-400 bg-red-50 text-red-700'
30
+ : 'border-violet-300 focus:border-violet-500 focus:ring-violet-200'}`, children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, index) => (_jsx("option", { value: word, children: word }, `${word}-${index}`)))] }), isReviewMode && isCorrect !== undefined && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(CheckCircle2, { className: "h-5 w-5 text-green-500" })) : (_jsx(XCircle, { className: "h-5 w-5 text-red-500" })) }))] })] })] }), isReviewMode && isCorrect === false && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: questionData.correctAnswer })] })] }))] }), explanation && isReviewMode && (_jsxs("div", { className: "rounded-xl border border-blue-200 bg-blue-50 p-4", children: [_jsx("p", { className: "mb-1 text-sm font-semibold text-blue-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "text-sm text-blue-700", children: explanation })] }))] })] }));
31
+ }
@@ -0,0 +1,2 @@
1
+ import { MatchWordToPictureCreatorProps } from '../../_shared/types/match-word-to-picture.type';
2
+ export declare function MatchWordToPictureCreator(props: MatchWordToPictureCreatorProps): import("react").JSX.Element;
@@ -0,0 +1,267 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ImagePreview } from '../../../../components/ui/image-preview';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { Button } from '../../../../components/ui/button';
6
+ import { Label } from '../../../../components/ui/label';
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 { BookOpen, Eye, HelpCircle, ImageIcon, Pencil } from 'lucide-react';
11
+ import { FileUpload } from '../../../../components/ui/file-upload';
12
+ import { Checkbox } from '../../../../components/ui/checkbox';
13
+ import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture.type';
14
+ import { MatchWordToPictureClient } from './MatchWordToPictureClient';
15
+ import { WordBankEditor } from './WordBankEditor';
16
+ import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
17
+ import { useExamQuestionStore } from '../../../../shared/lib/stores/examQuestionStore';
18
+ function getGroupNumberForIndex(questionIndex, groups) {
19
+ if (!groups || groups.length === 0) {
20
+ return 1;
21
+ }
22
+ let currentIndex = 0;
23
+ for (const group of groups) {
24
+ if (questionIndex < currentIndex + group.questionCount) {
25
+ return group.number;
26
+ }
27
+ currentIndex += group.questionCount;
28
+ }
29
+ return groups[groups.length - 1]?.number || 1;
30
+ }
31
+ function normalizeWordBank(wordBank) {
32
+ if (!Array.isArray(wordBank)) {
33
+ return [];
34
+ }
35
+ return wordBank.filter((word) => typeof word === 'string');
36
+ }
37
+ function MatchWordToPictureContent({ initialData, onChange, onUnsavedChangesChange, externalErrors, groups, questionIndexInPart = 0, partId = '', validationRef, }) {
38
+ const setSharedPassageData = useExamQuestionStore((state) => state.setSharedPassageData);
39
+ const getSharedPassageData = useExamQuestionStore((state) => state.getSharedPassageData);
40
+ const initialGroupNum = getGroupNumberForIndex(questionIndexInPart, groups);
41
+ const [imageUrl, setImageUrl] = useState(initialData?.imageUrl || '');
42
+ const [correctAnswer, setCorrectAnswer] = useState(initialData?.correctAnswer || '');
43
+ const [points, setPoints] = useState(initialData?.points || 1);
44
+ const [isExample, setIsExample] = useState(initialData?.isExample || false);
45
+ const [groupNumber, setGroupNumber] = useState(initialData?.groupNumber || initialGroupNum);
46
+ const [explanation, setExplanation] = useState(initialData?.explanation || '');
47
+ const [wordBank, setWordBank] = useState(() => {
48
+ if (partId) {
49
+ const storedData = getSharedPassageData(partId, initialGroupNum);
50
+ if (Array.isArray(storedData?.wordBank)) {
51
+ return storedData.wordBank;
52
+ }
53
+ }
54
+ return initialData?.wordBank || [];
55
+ });
56
+ const [instruction, setInstruction] = useState(() => {
57
+ if (partId) {
58
+ const storedData = getSharedPassageData(partId, initialGroupNum);
59
+ if (storedData?.instruction) {
60
+ return storedData.instruction;
61
+ }
62
+ }
63
+ return initialData?.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
64
+ });
65
+ const [isClientMode, setIsClientMode] = useState(false);
66
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
67
+ const [imagePreviewUrl, setImagePreviewUrl] = useState('');
68
+ const [validationErrors, setValidationErrors] = useState({});
69
+ const savedStateRef = useRef('');
70
+ const previousQuestionIndexRef = useRef(questionIndexInPart);
71
+ const isInitializingRef = useRef(false);
72
+ const hasInitializedRef = useRef(false);
73
+ const onChangeRef = useRef(onChange);
74
+ const previousPayloadRef = useRef('');
75
+ const onUnsavedChangesChangeRef = useRef(onUnsavedChangesChange);
76
+ const currentGroupNumber = groupNumber || 1;
77
+ const { previewUrl: generatedPreviewUrl } = usePresignedFileUrl(imageUrl);
78
+ const displayPreviewUrl = imagePreviewUrl || generatedPreviewUrl || '';
79
+ const usableWords = wordBank.map((word) => word.trim()).filter((word) => word.length > 0);
80
+ useEffect(() => {
81
+ onUnsavedChangesChangeRef.current = onUnsavedChangesChange;
82
+ }, [onUnsavedChangesChange]);
83
+ useEffect(() => {
84
+ onChangeRef.current = onChange;
85
+ }, [onChange]);
86
+ const debouncedOnChange = useDebouncedCallback((data) => {
87
+ onChangeRef.current?.(data);
88
+ }, 300);
89
+ const getCurrentStateString = () => JSON.stringify({ imageUrl, correctAnswer, points, isExample, wordBank, instruction });
90
+ useEffect(() => {
91
+ if (isInitializingRef.current)
92
+ return;
93
+ const timer = setTimeout(() => {
94
+ savedStateRef.current = getCurrentStateString();
95
+ setHasUnsavedChanges(false);
96
+ }, 150);
97
+ return () => clearTimeout(timer);
98
+ }, [questionIndexInPart]);
99
+ useEffect(() => {
100
+ if (isInitializingRef.current || !savedStateRef.current)
101
+ return;
102
+ const hasChanges = getCurrentStateString() !== savedStateRef.current;
103
+ if (hasChanges !== hasUnsavedChanges) {
104
+ setHasUnsavedChanges(hasChanges);
105
+ onUnsavedChangesChangeRef.current?.(hasChanges);
106
+ }
107
+ }, [imageUrl, correctAnswer, points, isExample, wordBank, instruction, hasUnsavedChanges]);
108
+ useEffect(() => {
109
+ if (!validationRef)
110
+ return;
111
+ validationRef.current = {
112
+ triggerValidation: async () => {
113
+ const errors = {};
114
+ let isValid = true;
115
+ if (!imageUrl.trim()) {
116
+ errors.imageUrl = 'Vui lòng upload hình ảnh';
117
+ isValid = false;
118
+ }
119
+ if (usableWords.length === 0) {
120
+ errors.wordBank = 'Vui lòng thêm word bank';
121
+ isValid = false;
122
+ }
123
+ if (!correctAnswer.trim()) {
124
+ errors.correctAnswer = 'Vui lòng chọn đáp án';
125
+ isValid = false;
126
+ }
127
+ else if (usableWords.length > 0 &&
128
+ !usableWords.some((word) => word.toLowerCase() === correctAnswer.trim().toLowerCase())) {
129
+ errors.correctAnswer = 'Đáp án phải nằm trong word bank';
130
+ isValid = false;
131
+ }
132
+ setValidationErrors(errors);
133
+ return isValid;
134
+ },
135
+ getFormData: () => ({
136
+ imageUrl,
137
+ correctAnswer,
138
+ points,
139
+ groupNumber: currentGroupNumber,
140
+ isExample,
141
+ wordBank,
142
+ instruction,
143
+ explanation,
144
+ groups,
145
+ }),
146
+ };
147
+ }, [
148
+ validationRef,
149
+ imageUrl,
150
+ correctAnswer,
151
+ points,
152
+ currentGroupNumber,
153
+ isExample,
154
+ wordBank,
155
+ instruction,
156
+ explanation,
157
+ groups,
158
+ usableWords,
159
+ ]);
160
+ useEffect(() => {
161
+ const questionIndexChanged = questionIndexInPart !== previousQuestionIndexRef.current;
162
+ const shouldSyncForFirstInit = !hasInitializedRef.current && !!initialData?.imageUrl;
163
+ if (!shouldSyncForFirstInit && !questionIndexChanged)
164
+ return;
165
+ isInitializingRef.current = true;
166
+ previousQuestionIndexRef.current = questionIndexInPart;
167
+ hasInitializedRef.current = true;
168
+ const groupNum = getGroupNumberForIndex(questionIndexInPart, groups);
169
+ setImageUrl(initialData?.imageUrl || '');
170
+ setCorrectAnswer(initialData?.correctAnswer || '');
171
+ setPoints(initialData?.points || 1);
172
+ setIsExample(initialData?.isExample || false);
173
+ setGroupNumber(groupNum);
174
+ setExplanation(initialData?.explanation || '');
175
+ let sharedWordBank = [];
176
+ let sharedInstruction = MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
177
+ if (partId) {
178
+ const storedData = getSharedPassageData(partId, groupNum);
179
+ if (Array.isArray(storedData?.wordBank)) {
180
+ sharedWordBank = storedData.wordBank;
181
+ sharedInstruction = storedData.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
182
+ }
183
+ else {
184
+ sharedWordBank = initialData?.wordBank || [];
185
+ sharedInstruction = initialData?.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
186
+ if (sharedWordBank.length > 0 || sharedInstruction) {
187
+ setSharedPassageData(partId, groupNum, {
188
+ wordBank: sharedWordBank,
189
+ instruction: sharedInstruction,
190
+ });
191
+ }
192
+ }
193
+ }
194
+ else {
195
+ sharedWordBank = initialData?.wordBank || [];
196
+ sharedInstruction = initialData?.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
197
+ }
198
+ setWordBank(normalizeWordBank(sharedWordBank));
199
+ setInstruction(sharedInstruction);
200
+ setTimeout(() => {
201
+ isInitializingRef.current = false;
202
+ }, 100);
203
+ }, [initialData, groups, questionIndexInPart, partId, getSharedPassageData, setSharedPassageData]);
204
+ useEffect(() => {
205
+ if (isInitializingRef.current)
206
+ return;
207
+ const payload = {
208
+ imageUrl,
209
+ correctAnswer,
210
+ points: points || 1,
211
+ groupNumber: currentGroupNumber,
212
+ isExample,
213
+ wordBank,
214
+ instruction,
215
+ explanation,
216
+ groups,
217
+ };
218
+ const payloadString = JSON.stringify(payload);
219
+ if (previousPayloadRef.current === payloadString)
220
+ return;
221
+ previousPayloadRef.current = payloadString;
222
+ if (partId) {
223
+ setSharedPassageData(partId, currentGroupNumber, { wordBank, instruction });
224
+ }
225
+ debouncedOnChange(payload);
226
+ }, [
227
+ imageUrl,
228
+ correctAnswer,
229
+ points,
230
+ currentGroupNumber,
231
+ isExample,
232
+ wordBank,
233
+ instruction,
234
+ explanation,
235
+ groups,
236
+ partId,
237
+ setSharedPassageData,
238
+ debouncedOnChange,
239
+ ]);
240
+ if (isClientMode) {
241
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Match word to picture (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc - Hi\u1EC3n th\u1ECB \u0111\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(MatchWordToPictureClient, { questionData: {
242
+ imageUrl: displayPreviewUrl || imageUrl,
243
+ correctAnswer,
244
+ points,
245
+ groupNumber: currentGroupNumber,
246
+ isExample,
247
+ wordBank: usableWords,
248
+ instruction,
249
+ }, isReviewMode: true, autoFillCorrectAnswers: true, explanation: explanation })] }));
250
+ }
251
+ return (_jsxs("div", { className: "space-y-4", children: [groups && groups.length > 0 && (_jsx(WordBankEditor, { wordBank: wordBank, instruction: instruction, onWordBankChange: setWordBank, onInstructionChange: setInstruction })), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-violet-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "N\u1ED1i t\u1EEB v\u1EDBi tranh" }), _jsx("p", { className: "text-xs text-gray-500", children: "Upload tranh v\u00E0 ch\u1ECDn t\u1EEB trong word bank" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !imageUrl, className: "gap-2 border-violet-200 text-violet-600 hover:bg-violet-50", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }) }), _jsxs(CardContent, { className: "space-y-4 px-4 pb-4", children: [_jsxs("div", { className: "flex items-center gap-6", children: [_jsx(PointsInput, { value: points || 1, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "mwtp-isExample", checked: isExample || false, onCheckedChange: (checked) => setIsExample(!!checked) }), _jsx(Label, { htmlFor: "mwtp-isExample", className: "cursor-pointer text-sm font-medium text-gray-700", children: "\u0110\u00E1nh d\u1EA5u l\u00E0 c\u00E2u h\u1ECFi Example" })] })] }), (!groups || groups.length === 0) && (_jsx(WordBankEditor, { wordBank: wordBank, instruction: instruction, onWordBankChange: setWordBank, onInstructionChange: setInstruction })), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-violet-500" }), "H\u00ECnh \u1EA3nh ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "mwtp-question-image", label: "", accept: "image/*", value: imageUrl || '', onChange: (url) => {
252
+ setImageUrl(url);
253
+ if (validationErrors.imageUrl) {
254
+ setValidationErrors((prev) => ({ ...prev, imageUrl: undefined }));
255
+ }
256
+ }, onPresignedUrlChange: setImagePreviewUrl, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), displayPreviewUrl && (_jsx(ImagePreview, { src: displayPreviewUrl, alt: "Preview", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" })), validationErrors.imageUrl && _jsx("p", { className: "text-sm text-red-600", children: validationErrors.imageUrl })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "mwtp-correctAnswer", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(HelpCircle, { className: "h-4 w-4 text-green-500" }), "\u0110\u00E1p \u00E1n \u0111\u00FAng ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsxs("select", { id: "mwtp-correctAnswer", value: correctAnswer, onChange: (event) => {
257
+ setCorrectAnswer(event.target.value);
258
+ if (validationErrors.correctAnswer) {
259
+ setValidationErrors((prev) => ({ ...prev, correctAnswer: undefined }));
260
+ }
261
+ }, className: `flex h-10 w-full rounded-md border bg-white px-3 py-2 text-sm ${validationErrors.correctAnswer
262
+ ? 'border-red-400 focus:border-red-500'
263
+ : 'border-gray-200 focus:border-green-300'}`, children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB trong word bank --" }), usableWords.map((word, index) => (_jsx("option", { value: word, children: word }, `${word}-${index}`)))] }), validationErrors.correctAnswer && (_jsx("p", { className: "text-sm text-red-600", children: validationErrors.correctAnswer })), validationErrors.wordBank && _jsx("p", { className: "text-sm text-red-600", children: validationErrors.wordBank })] }), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc", children: externalErrors.map((error, index) => (_jsx("li", { children: error }, index))) })] }))] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", 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-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-3.5 w-3.5 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { className: "px-4 pb-4", children: _jsx(Textarea, { id: "mwtp-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, className: "min-h-[80px] resize-none border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200" }) })] })] }));
264
+ }
265
+ export function MatchWordToPictureCreator(props) {
266
+ return _jsx(MatchWordToPictureContent, { ...props });
267
+ }
@@ -0,0 +1,2 @@
1
+ import type { MatchWordToPictureGroupClientProps } from '../../_shared/types/match-word-to-picture.type';
2
+ export declare function MatchWordToPictureGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: MatchWordToPictureGroupClientProps): import("react").JSX.Element;
@@ -0,0 +1,65 @@
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
+ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
+ function normalizeWord(value) {
7
+ return value.trim().toLowerCase();
8
+ }
9
+ function ClientImageItem({ url, alt }) {
10
+ const { previewUrl, isLoading } = usePresignedFileUrl(url);
11
+ const src = previewUrl ||
12
+ (typeof url === 'string' &&
13
+ (url.startsWith('http') || url.startsWith('blob:') || url.startsWith('data:'))
14
+ ? url
15
+ : '');
16
+ if (!url) {
17
+ return (_jsx("div", { className: "flex h-28 w-28 items-center justify-center rounded-lg border border-dashed border-gray-300 bg-gray-50 text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 \u1EA3nh" }));
18
+ }
19
+ if (!src && !isLoading) {
20
+ return (_jsx("div", { className: "flex h-28 w-28 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-xs text-gray-400", children: "Kh\u00F4ng t\u1EA3i \u0111\u01B0\u1EE3c \u1EA3nh" }));
21
+ }
22
+ return (_jsx("div", { className: "relative h-28 w-28 flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: isLoading ? (_jsx("div", { className: "flex h-full w-full items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-violet-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Picture', className: "h-full w-full object-contain" })) }));
23
+ }
24
+ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
25
+ const items = questionData.items || [];
26
+ const wordBank = questionData.wordBank || [];
27
+ const handleSelect = (itemIndex, value) => {
28
+ const item = items[itemIndex];
29
+ if (isReviewMode || item?.isExample || !onAnswerChange) {
30
+ return;
31
+ }
32
+ const next = items.map((entry, index) => entry.isExample ? entry.correctAnswer : (userAnswers[index] ?? ''));
33
+ next[itemIndex] = value;
34
+ onAnswerChange(next);
35
+ };
36
+ const usedWords = new Set();
37
+ items.forEach((item, index) => {
38
+ const value = item.isExample ? item.correctAnswer : (userAnswers[index] ?? '');
39
+ if (value.trim()) {
40
+ usedWords.add(normalizeWord(value));
41
+ }
42
+ });
43
+ 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 })), wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-violet-200 bg-gradient-to-r from-violet-50 to-indigo-50 p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-violet-700", children: "Word bank:" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
44
+ const isUsed = usedWords.has(normalizeWord(word));
45
+ return (_jsx("span", { className: cn('rounded-full border px-3 py-1 text-sm font-semibold', isUsed
46
+ ? 'border-emerald-200 bg-emerald-50 text-emerald-700'
47
+ : 'border-violet-200 bg-white text-slate-700'), children: word }, `${word}-${index}`));
48
+ }) })] })), items.map((item, itemIndex) => {
49
+ const selectedWord = item.isExample
50
+ ? item.correctAnswer
51
+ : (userAnswers[itemIndex] ?? '');
52
+ const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
53
+ const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
54
+ Boolean(item.correctAnswer);
55
+ 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-violet-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: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center", children: [_jsx(ClientImageItem, { url: item.imageUrl, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}` }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("select", { value: selectedWord, disabled: !canSelect, onChange: (event) => handleSelect(itemIndex, event.target.value), className: cn('h-10 max-w-xs rounded-md border-2 bg-white px-3 text-sm font-medium', isReviewMode && isCorrect
56
+ ? 'border-green-400 bg-green-50 text-green-700'
57
+ : isReviewMode && selectedWord && !isCorrect
58
+ ? 'border-red-400 bg-red-50 text-red-700'
59
+ : 'border-violet-300 focus:border-violet-500 focus:ring-violet-200', !canSelect && 'cursor-default opacity-80'), children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, wordIndex) => {
60
+ const isUsedByOther = usedWords.has(normalizeWord(word)) &&
61
+ normalizeWord(word) !== normalizeWord(selectedWord);
62
+ return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${wordIndex}`));
63
+ })] }), isReviewMode && !item.isExample && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(Check, { className: "h-5 w-5 text-green-500" })) : (_jsx(X, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] })] }, `${item.questionNumber}-${itemIndex}`));
64
+ }), 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] }))] }));
65
+ }
@@ -0,0 +1,8 @@
1
+ interface WordBankEditorProps {
2
+ wordBank: string[];
3
+ instruction: string;
4
+ onWordBankChange: (wordBank: string[]) => void;
5
+ onInstructionChange: (instruction: string) => void;
6
+ }
7
+ export declare function WordBankEditor({ wordBank, instruction, onWordBankChange, onInstructionChange, }: WordBankEditorProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button } from '../../../../components/ui/button';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
5
+ import { Input } from '../../../../components/ui/input';
6
+ import { Label } from '../../../../components/ui/label';
7
+ import { Plus, Trash2, Type } from 'lucide-react';
8
+ export function WordBankEditor({ wordBank, instruction, onWordBankChange, onInstructionChange, }) {
9
+ const updateWord = (index, value) => {
10
+ const next = [...wordBank];
11
+ next[index] = value;
12
+ onWordBankChange(next);
13
+ };
14
+ return (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-violet-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-violet-600 shadow-sm", children: _jsx(Type, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "Word bank nh\u00F3m" }), _jsx("p", { className: "text-xs text-gray-500", children: "C\u00E1c t\u1EEB d\u00F9ng chung cho m\u1ECDi tranh trong nh\u00F3m" })] })] }) }), _jsxs(CardContent, { className: "space-y-4 px-4 pb-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-instruction", className: "text-sm font-medium text-gray-700", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-instruction", value: instruction, onChange: (event) => onInstructionChange(event.target.value), placeholder: "Look and complete." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { className: "text-sm font-medium text-gray-700", children: "Danh s\u00E1ch t\u1EEB" }), _jsx("div", { className: "space-y-2", children: wordBank.map((word, index) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: word, onChange: (event) => updateWord(index, event.target.value), placeholder: `Từ ${index + 1}` }), _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: () => onWordBankChange(wordBank.filter((_, wordIndex) => wordIndex !== index)), "aria-label": `Xóa từ ${index + 1}`, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, index))) }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onWordBankChange([...wordBank, '']), className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm t\u1EEB"] })] })] })] }));
15
+ }
@@ -0,0 +1,6 @@
1
+ export { MatchWordToPictureCreator } from './MatchWordToPictureCreator';
2
+ export { MatchWordToPictureClient } from './MatchWordToPictureClient';
3
+ export { MatchWordToPictureGroupClient } from './MatchWordToPictureGroupClient';
4
+ export { matchWordToPictureRegistration } from './register';
5
+ export { mapQuestionToMatchWordToPictureData } from './map-match-word-to-picture-data';
6
+ export { mapQuestionToMatchWordToPictureGroupData } from './map-match-word-to-picture-group-data';
@@ -0,0 +1,6 @@
1
+ export { MatchWordToPictureCreator } from './MatchWordToPictureCreator';
2
+ export { MatchWordToPictureClient } from './MatchWordToPictureClient';
3
+ export { MatchWordToPictureGroupClient } from './MatchWordToPictureGroupClient';
4
+ export { matchWordToPictureRegistration } from './register';
5
+ export { mapQuestionToMatchWordToPictureData } from './map-match-word-to-picture-data';
6
+ export { mapQuestionToMatchWordToPictureGroupData } from './map-match-word-to-picture-group-data';
@@ -0,0 +1,2 @@
1
+ import { type MatchWordToPictureData } from '../../_shared/types/match-word-to-picture.type';
2
+ export declare function mapQuestionToMatchWordToPictureData(question: Record<string, unknown> | null | undefined): MatchWordToPictureData;
@@ -0,0 +1,53 @@
1
+ import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture.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 asStringList(value) {
12
+ if (!Array.isArray(value)) {
13
+ return [];
14
+ }
15
+ return value
16
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
17
+ .filter(Boolean);
18
+ }
19
+ export function mapQuestionToMatchWordToPictureData(question) {
20
+ const source = asRecord(question);
21
+ const content = asRecord(source.content);
22
+ const meta = asRecord(content.meta);
23
+ const answer = asRecord(source.answer ?? source.correctAnswer ?? source.correct_answer);
24
+ const questionGroup = asRecord(source.questionGroup);
25
+ const groupPayload = asRecord(questionGroup.payload);
26
+ const correctAnswer = asString(answer.answer) || asString(answer.correctAnswer);
27
+ const wordBank = asStringList(groupPayload.wordBank).length
28
+ ? asStringList(groupPayload.wordBank)
29
+ : asStringList(meta.wordBank).length
30
+ ? asStringList(meta.wordBank)
31
+ : asStringList(answer.wordBank).length
32
+ ? asStringList(answer.wordBank)
33
+ : correctAnswer
34
+ ? [correctAnswer]
35
+ : [];
36
+ return {
37
+ imageUrl: asString(content.imageUrl) || asString(answer.imageUrl),
38
+ correctAnswer,
39
+ points: typeof source.points === 'number' ? source.points : 1,
40
+ groupNumber: typeof questionGroup.groupNumber === 'number'
41
+ ? questionGroup.groupNumber
42
+ : typeof answer.groupNumber === 'number'
43
+ ? answer.groupNumber
44
+ : 1,
45
+ isExample: source.isExample === true || answer.isExample === true,
46
+ wordBank,
47
+ instruction: asString(groupPayload.instruction) ||
48
+ asString(meta.instruction) ||
49
+ asString(answer.instruction) ||
50
+ MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION,
51
+ explanation: asString(source.explanation) || asString(answer.explanation) || undefined,
52
+ };
53
+ }
@@ -0,0 +1,4 @@
1
+ import { type MatchWordToPictureGroupData, type MatchWordToPictureGroupItemData } from '../../_shared/types/match-word-to-picture.type';
2
+ export declare function createEmptyMatchWordToPictureGroupItem(questionNumber: number): MatchWordToPictureGroupItemData;
3
+ export declare function mapMatchWordToPictureGroupItem(item: unknown, index: number, fallbackAnswer?: string): MatchWordToPictureGroupItemData;
4
+ export declare function mapQuestionToMatchWordToPictureGroupData(question: Record<string, unknown> | null | undefined): MatchWordToPictureGroupData;