@tinyweb_dev/oe-exam-sdk 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +7 -3
  3. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -4
  4. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
  6. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  7. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  8. package/dist/components/exams/take/utils/answer-transformers.js +8 -1
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
  10. package/dist/components/exams/take/utils/question-transformers.js +49 -4
  11. package/dist/components/index.d.ts +1 -0
  12. package/dist/components/index.js +1 -0
  13. package/dist/components/questions/_shared/config/question-types.config.js +18 -0
  14. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
  15. package/dist/components/questions/_shared/types/index.d.ts +1 -0
  16. package/dist/components/questions/_shared/types/index.js +1 -0
  17. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
  18. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
  19. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
  20. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
  21. package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
  22. package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
  23. package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
  24. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  25. package/dist/components/questions/creator/question-type-registry.js +6 -0
  26. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +6 -4
  27. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +16 -4
  28. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -3
  29. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +6 -4
  30. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
  31. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  32. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
  33. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
  34. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
  35. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
  36. package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
  37. package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
  38. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
  39. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
  40. package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
  41. package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
  42. package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
  43. package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
  44. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
  45. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
  46. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
  47. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
  48. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
  49. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
  50. package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
  51. package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
  52. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
  53. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
  54. package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
  55. package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
  56. package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
  57. package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
  58. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
  59. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
  60. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
  61. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +106 -0
  62. package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
  63. package/dist/components/questions/types/word-ordering-group/index.js +5 -0
  64. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
  65. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
  66. package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
  67. package/dist/components/questions/types/word-ordering-group/register.js +36 -0
  68. package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
  69. package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
  70. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
  71. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
  72. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
  73. package/dist/components/questions/viewer/QuestionViewer.js +6 -0
  74. package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
  75. package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
  76. package/dist/components/questions/viewer/index.d.ts +2 -0
  77. package/dist/components/questions/viewer/index.js +2 -0
  78. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +3 -2
  79. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  80. package/dist/components/results/renderers/review-question-body-movers.js +6 -2
  81. package/dist/components/results/review-data.js +13 -0
  82. package/dist/components/shared/RichTextEditor.d.ts +4 -2
  83. package/dist/components/shared/RichTextEditor.js +34 -11
  84. package/dist/components/shared/RichTextHtml.d.ts +8 -0
  85. package/dist/components/shared/RichTextHtml.js +9 -0
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/shared/constants/question-skills.js +6 -0
  89. package/dist/shared/lib/rich-text.d.ts +3 -0
  90. package/dist/shared/lib/rich-text.js +24 -0
  91. package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
  92. package/dist/shared/lib/utils/question-transform.js +6 -0
  93. package/dist/shared/lib/validation/choose-correct-answer.validation.js +4 -2
  94. package/dist/shared/types/common.types.d.ts +1 -1
  95. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  96. package/dist/shared/types/questions/index.d.ts +3 -0
  97. package/dist/shared/types/questions/index.js +6 -0
  98. package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
  99. package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
  100. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
  101. package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
  102. package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
  103. package/dist/shared/types/questions/word-ordering-group.js +1 -0
  104. package/package.json +1 -1
@@ -0,0 +1,222 @@
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 { PointsInput } from '../../../../components/ui/points-input';
8
+ import { Textarea } from '../../../../components/ui/textarea';
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 { MatchColumnsToMakeSentencesClient } from './MatchColumnsToMakeSentencesClient';
14
+ import { MATCH_COLUMNS_DEFAULT_INSTRUCTION } from '../../_shared/types/match-columns-to-make-sentences.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
+ import { assembleMatchColumnsSentence, createEmptyMatchColumnsColumns, createEmptyMatchColumnsItem, getMatchColumnsChoiceColumns, getMatchColumnsStemColumn, mapQuestionToMatchColumnsGroupData, nextMatchColumnsFragmentId, } from './map-match-columns-to-make-sentences-data';
25
+ function sumPoints(items) {
26
+ return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
27
+ }
28
+ function syncItemAnswers(items, columns) {
29
+ const choiceCount = getMatchColumnsChoiceColumns(columns).length;
30
+ return items.map((item, index) => {
31
+ const stemFragmentId = item.stemFragmentId || getMatchColumnsStemColumn(columns)?.fragments[0]?.id || '';
32
+ const fragmentIds = [
33
+ stemFragmentId,
34
+ ...Array.from({ length: choiceCount }, (_, choiceIndex) => item.fragmentIds[choiceIndex + 1] || ''),
35
+ ];
36
+ return {
37
+ ...item,
38
+ stemFragmentId,
39
+ fragmentIds,
40
+ sentence: assembleMatchColumnsSentence(fragmentIds, columns),
41
+ questionNumber: item.questionNumber || index + 1,
42
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
43
+ };
44
+ });
45
+ }
46
+ export function MatchColumnsToMakeSentencesCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
47
+ const seeded = mapQuestionToMatchColumnsGroupData(initialData ? { answer: initialData } : null);
48
+ const [title, setTitle] = useState(initialData?.title ?? seeded.title ?? '');
49
+ const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || MATCH_COLUMNS_DEFAULT_INSTRUCTION);
50
+ const [columns, setColumns] = useState(() => (initialData?.columns?.length ? initialData.columns : seeded.columns.length ? seeded.columns : createEmptyMatchColumnsColumns()));
51
+ const [items, setItems] = useState(() => (initialData?.items?.length ? syncItemAnswers(initialData.items, initialData.columns || columns) : seeded.items));
52
+ const [explanation, setExplanation] = useState(initialData?.explanation || '');
53
+ const [isClientMode, setIsClientMode] = useState(false);
54
+ const [errors, setErrors] = useState([]);
55
+ const onChangeRef = useRef(onChange);
56
+ const previousPayloadRef = useRef('');
57
+ useEffect(() => {
58
+ onChangeRef.current = onChange;
59
+ }, [onChange]);
60
+ const buildPayload = (nextTitle = title, nextInstruction = instruction, nextColumns = columns, nextItems = items, nextExplanation = explanation) => ({
61
+ title: nextTitle,
62
+ instruction: nextInstruction,
63
+ columns: nextColumns,
64
+ items: syncItemAnswers(nextItems, nextColumns),
65
+ explanation: nextExplanation,
66
+ points: sumPoints(nextItems),
67
+ });
68
+ const debouncedOnChange = useDebouncedCallback((payload) => {
69
+ onChangeRef.current?.(payload);
70
+ }, 300);
71
+ useEffect(() => {
72
+ const payload = buildPayload();
73
+ const serialized = JSON.stringify(payload);
74
+ if (serialized !== previousPayloadRef.current) {
75
+ previousPayloadRef.current = serialized;
76
+ debouncedOnChange(payload);
77
+ onUnsavedChangesChange?.(true);
78
+ }
79
+ }, [title, instruction, columns, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
80
+ const validate = () => {
81
+ const nextErrors = [];
82
+ if (!instruction.trim())
83
+ nextErrors.push('Hướng dẫn (Instruction) không được để trống.');
84
+ if (columns.length < 2)
85
+ nextErrors.push('Cần ít nhất 2 cột (1 stem + 1 choice).');
86
+ const stems = columns.filter((column) => column.role === 'stem');
87
+ if (stems.length !== 1)
88
+ nextErrors.push('Cần đúng 1 cột stem.');
89
+ if (getMatchColumnsChoiceColumns(columns).length < 1)
90
+ nextErrors.push('Cần ít nhất 1 cột choice.');
91
+ columns.forEach((column, index) => {
92
+ if (!column.fragments.length) {
93
+ nextErrors.push(`Cột ${index + 1}: cần ít nhất một fragment.`);
94
+ }
95
+ column.fragments.forEach((fragment, fragmentIndex) => {
96
+ if (!fragment.id.trim() || !fragment.text.trim()) {
97
+ nextErrors.push(`Cột ${index + 1}, fragment ${fragmentIndex + 1}: id và text bắt buộc.`);
98
+ }
99
+ });
100
+ });
101
+ if (items.length < 1)
102
+ nextErrors.push('Cần ít nhất một câu hỏi.');
103
+ const expectedCount = 1 + getMatchColumnsChoiceColumns(columns).length;
104
+ items.forEach((item, index) => {
105
+ if (!item.stemFragmentId) {
106
+ nextErrors.push(`Câu ${index + 1}: chưa chọn stem.`);
107
+ }
108
+ if (item.fragmentIds.filter(Boolean).length !== expectedCount) {
109
+ nextErrors.push(`Câu ${index + 1}: cần chọn đủ ${expectedCount} fragment.`);
110
+ }
111
+ });
112
+ setErrors(nextErrors);
113
+ return nextErrors.length === 0;
114
+ };
115
+ useEffect(() => {
116
+ if (validationRef) {
117
+ validationRef.current = {
118
+ triggerValidation: async () => validate(),
119
+ getFormData: () => buildPayload(),
120
+ };
121
+ }
122
+ }, [validationRef, title, instruction, columns, items, explanation]);
123
+ const updateColumn = (index, patch) => {
124
+ setColumns((prev) => prev.map((column, i) => (i === index ? { ...column, ...patch } : column)));
125
+ };
126
+ const updateFragment = (columnIndex, fragmentIndex, patch) => {
127
+ setColumns((prev) => prev.map((column, i) => {
128
+ if (i !== columnIndex)
129
+ return column;
130
+ return {
131
+ ...column,
132
+ fragments: column.fragments.map((fragment, fi) => (fi === fragmentIndex ? { ...fragment, ...patch } : fragment)),
133
+ };
134
+ }));
135
+ };
136
+ const addFragment = (columnIndex) => {
137
+ setColumns((prev) => prev.map((column, i) => {
138
+ if (i !== columnIndex)
139
+ return column;
140
+ return {
141
+ ...column,
142
+ fragments: [...column.fragments, { id: nextMatchColumnsFragmentId(prev), text: '' }],
143
+ };
144
+ }));
145
+ };
146
+ const removeFragment = (columnIndex, fragmentIndex) => {
147
+ setColumns((prev) => prev.map((column, i) => {
148
+ if (i !== columnIndex || column.fragments.length <= 1)
149
+ return column;
150
+ return {
151
+ ...column,
152
+ fragments: column.fragments.filter((_, fi) => fi !== fragmentIndex),
153
+ };
154
+ }));
155
+ };
156
+ const addChoiceColumn = () => {
157
+ setColumns((prev) => [
158
+ ...prev,
159
+ {
160
+ id: `col-${prev.length + 1}`,
161
+ label: String(prev.length + 1),
162
+ role: 'choice',
163
+ fragments: [{ id: nextMatchColumnsFragmentId(prev), text: '' }],
164
+ },
165
+ ]);
166
+ };
167
+ const removeColumn = (index) => {
168
+ setColumns((prev) => {
169
+ const target = prev[index];
170
+ if (!target || target.role === 'stem' || prev.filter((column) => column.role === 'choice').length <= 1) {
171
+ return prev;
172
+ }
173
+ return prev.filter((_, i) => i !== index);
174
+ });
175
+ };
176
+ const updateItem = (index, patch) => {
177
+ setItems((prev) => {
178
+ const next = [...prev];
179
+ const current = next[index];
180
+ if (!current)
181
+ return prev;
182
+ const merged = { ...current, ...patch };
183
+ if (patch.stemFragmentId) {
184
+ merged.fragmentIds = [patch.stemFragmentId, ...current.fragmentIds.slice(1)];
185
+ }
186
+ next[index] = {
187
+ ...merged,
188
+ sentence: assembleMatchColumnsSentence(merged.fragmentIds, columns),
189
+ };
190
+ return next;
191
+ });
192
+ };
193
+ const setItemChoice = (itemIndex, choiceIndex, fragmentId) => {
194
+ setItems((prev) => prev.map((item, index) => {
195
+ if (index !== itemIndex)
196
+ return item;
197
+ const fragmentIds = item.fragmentIds.map((id, fi) => (fi === choiceIndex + 1 ? fragmentId : id));
198
+ return {
199
+ ...item,
200
+ fragmentIds,
201
+ sentence: assembleMatchColumnsSentence(fragmentIds, columns),
202
+ };
203
+ }));
204
+ };
205
+ const addItem = () => {
206
+ setItems((prev) => [...prev, createEmptyMatchColumnsItem(prev.length + 1, columns)]);
207
+ };
208
+ const removeItem = (index) => {
209
+ setItems((prev) => {
210
+ if (prev.length <= 1)
211
+ return prev;
212
+ return prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, questionNumber: i + 1 }));
213
+ });
214
+ };
215
+ const currentPayload = buildPayload();
216
+ const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
217
+ const stemColumn = getMatchColumnsStemColumn(columns);
218
+ const choiceColumns = getMatchColumnsChoiceColumns(columns);
219
+ 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 Gh\u00E9p c\u1ED9t th\u00E0nh c\u00E2u" }), _jsx("p", { className: "text-sm text-gray-500", children: "C\u1ED9t stem c\u1ED1 \u0111\u1ECBnh theo d\u00F2ng; h\u1ECDc sinh ch\u1ECDn 1 fragment m\u1ED7i c\u1ED9t choice." })] }), _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 border border-red-200 bg-red-50 p-4 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(MatchColumnsToMakeSentencesClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Th\u00F4ng tin chung" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "match-columns-title", children: "Ti\u00EAu \u0111\u1EC1" }), _jsx(Input, { id: "match-columns-title", className: "mt-1", value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "match-columns-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "match-columns-instruction", className: "mt-1", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: MATCH_COLUMNS_DEFAULT_INSTRUCTION })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { className: "text-base font-semibold", children: "C\u1ED9t fragment" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addChoiceColumn, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u1ED9t choice"] })] }), _jsx(CardContent, { className: "grid gap-4 md:grid-cols-3", children: columns.map((column, columnIndex) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Input, { value: column.label || '', onChange: (e) => updateColumn(columnIndex, { label: e.target.value }), placeholder: "Nh\u00E3n c\u1ED9t" }), _jsx("span", { className: "text-xs text-slate-500", children: column.role }), column.role === 'choice' && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeColumn(columnIndex), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), column.fragments.map((fragment, fragmentIndex) => (_jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { className: "w-16", value: fragment.id, onChange: (e) => updateFragment(columnIndex, fragmentIndex, { id: e.target.value }) }), _jsx(Input, { value: fragment.text, onChange: (e) => updateFragment(columnIndex, fragmentIndex, { text: e.target.value }), placeholder: "Text" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeFragment(columnIndex, fragmentIndex), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, `${column.id}-${fragmentIndex}`))), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => addFragment(columnIndex), children: "Th\u00EAm fragment" })] }, column.id))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["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"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 bg-gray-50/50 p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: `match-columns-example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `match-columns-example-${index}`, className: "flex items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
220
+ points: parsePointsValue(event.target.value, true),
221
+ }) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(index), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }), _jsxs("div", { className: "grid gap-3 md:grid-cols-3", children: [_jsxs("select", { className: "rounded-md border px-3 py-2 text-sm", value: item.stemFragmentId, onChange: (e) => updateItem(index, { stemFragmentId: e.target.value }), children: [_jsx("option", { value: "", children: "Ch\u1ECDn stem" }), (stemColumn?.fragments || []).map((fragment) => (_jsxs("option", { value: fragment.id, children: [fragment.id, ". ", fragment.text || '(trống)'] }, fragment.id)))] }), choiceColumns.map((column, choiceIndex) => (_jsxs("select", { className: "rounded-md border px-3 py-2 text-sm", value: item.fragmentIds[choiceIndex + 1] || '', onChange: (e) => setItemChoice(index, choiceIndex, e.target.value), children: [_jsxs("option", { value: "", children: ["Ch\u1ECDn ", column.label || column.id] }), column.fragments.map((fragment) => (_jsxs("option", { value: fragment.id, children: [fragment.id, ". ", fragment.text || '(trống)'] }, fragment.id)))] }, `${index}-${column.id}`)))] }), _jsx("p", { className: "text-sm text-slate-600", children: item.sentence || 'Câu sẽ tự ghép từ fragment đã chọn.' })] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "match-columns-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 }) })] })] }))] }));
222
+ }
@@ -0,0 +1,5 @@
1
+ export * from './MatchColumnsToMakeSentencesCreator';
2
+ export * from './MatchColumnsToMakeSentencesClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-match-columns-to-make-sentences-data';
@@ -0,0 +1,5 @@
1
+ export * from './MatchColumnsToMakeSentencesCreator';
2
+ export * from './MatchColumnsToMakeSentencesClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-match-columns-to-make-sentences-data';
@@ -0,0 +1,8 @@
1
+ import type { MatchColumnsColumn, MatchColumnsGroupData, MatchColumnsGroupItemData } from '../../_shared/types/match-columns-to-make-sentences.type';
2
+ export declare function assembleMatchColumnsSentence(fragmentIds: string[], columns: MatchColumnsColumn[]): string;
3
+ export declare function getMatchColumnsStemColumn(columns: MatchColumnsColumn[]): MatchColumnsColumn | undefined;
4
+ export declare function getMatchColumnsChoiceColumns(columns: MatchColumnsColumn[]): MatchColumnsColumn[];
5
+ export declare function nextMatchColumnsFragmentId(columns: MatchColumnsColumn[]): string;
6
+ export declare function createEmptyMatchColumnsColumns(): MatchColumnsColumn[];
7
+ export declare function createEmptyMatchColumnsItem(questionNumber: number, columns: MatchColumnsColumn[]): MatchColumnsGroupItemData;
8
+ export declare function mapQuestionToMatchColumnsGroupData(question: Record<string, unknown> | null | undefined): MatchColumnsGroupData;
@@ -0,0 +1,199 @@
1
+ import { MATCH_COLUMNS_DEFAULT_INSTRUCTION } from '../../_shared/types/match-columns-to-make-sentences.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 assembleMatchColumnsSentence(fragmentIds, columns) {
12
+ const texts = new Map();
13
+ columns.forEach((column) => {
14
+ column.fragments.forEach((fragment) => {
15
+ texts.set(fragment.id, fragment.text);
16
+ });
17
+ });
18
+ return fragmentIds
19
+ .map((id) => texts.get(id) || '')
20
+ .filter(Boolean)
21
+ .join(' ')
22
+ .replace(/\s+/g, ' ')
23
+ .trim();
24
+ }
25
+ export function getMatchColumnsStemColumn(columns) {
26
+ return columns.find((column) => column.role === 'stem');
27
+ }
28
+ export function getMatchColumnsChoiceColumns(columns) {
29
+ return columns.filter((column) => column.role === 'choice');
30
+ }
31
+ export function nextMatchColumnsFragmentId(columns) {
32
+ const used = new Set();
33
+ columns.forEach((column) => {
34
+ column.fragments.forEach((fragment) => used.add(fragment.id));
35
+ });
36
+ let n = 1;
37
+ while (used.has(String(n)))
38
+ n += 1;
39
+ return String(n);
40
+ }
41
+ export function createEmptyMatchColumnsColumns() {
42
+ return [
43
+ {
44
+ id: 'col-1',
45
+ label: '1',
46
+ role: 'stem',
47
+ fragments: [
48
+ { id: '1', text: '' },
49
+ { id: '2', text: '' },
50
+ ],
51
+ },
52
+ {
53
+ id: 'col-2',
54
+ label: '2',
55
+ role: 'choice',
56
+ fragments: [
57
+ { id: 'a', text: '' },
58
+ { id: 'b', text: '' },
59
+ ],
60
+ },
61
+ {
62
+ id: 'col-3',
63
+ label: '3',
64
+ role: 'choice',
65
+ fragments: [
66
+ { id: 'c', text: '' },
67
+ { id: 'd', text: '' },
68
+ ],
69
+ },
70
+ ];
71
+ }
72
+ export function createEmptyMatchColumnsItem(questionNumber, columns) {
73
+ const stem = getMatchColumnsStemColumn(columns);
74
+ const choices = getMatchColumnsChoiceColumns(columns);
75
+ const stemFragmentId = stem?.fragments[0]?.id || '';
76
+ const fragmentIds = [
77
+ stemFragmentId,
78
+ ...choices.map((column) => column.fragments[0]?.id || ''),
79
+ ];
80
+ return {
81
+ stemFragmentId,
82
+ fragmentIds,
83
+ sentence: assembleMatchColumnsSentence(fragmentIds, columns),
84
+ isExample: false,
85
+ points: 1,
86
+ questionNumber,
87
+ };
88
+ }
89
+ function parseItemAnswer(raw) {
90
+ const record = asRecord(raw);
91
+ const fragmentIds = Array.isArray(record.fragmentIds)
92
+ ? record.fragmentIds.filter((id) => typeof id === 'string')
93
+ : [];
94
+ return {
95
+ fragmentIds,
96
+ sentence: asString(record.sentence),
97
+ };
98
+ }
99
+ function mapColumn(raw, index) {
100
+ const record = asRecord(raw);
101
+ const fragments = Array.isArray(record.fragments)
102
+ ? record.fragments.map((fragment, fragmentIndex) => {
103
+ const item = asRecord(fragment);
104
+ return {
105
+ id: asString(item.id, `${index + 1}-${fragmentIndex + 1}`),
106
+ text: asString(item.text),
107
+ };
108
+ })
109
+ : [];
110
+ return {
111
+ id: asString(record.id, `col-${index + 1}`),
112
+ ...(asString(record.label) ? { label: asString(record.label) } : {}),
113
+ role: record.role === 'stem' ? 'stem' : 'choice',
114
+ fragments,
115
+ };
116
+ }
117
+ function mapApiItem(item, index, columns, fallbackAnswer) {
118
+ const record = asRecord(item);
119
+ const content = asRecord(record.content);
120
+ const correctAnswer = asRecord(record.correctAnswer);
121
+ const parsed = parseItemAnswer(correctAnswer.answer ?? fallbackAnswer);
122
+ const isExample = record.isExample === true;
123
+ const stemFragmentId = asString(content.stemFragmentId) || parsed.fragmentIds[0] || '';
124
+ const fragmentIds = parsed.fragmentIds.length > 0
125
+ ? parsed.fragmentIds
126
+ : [stemFragmentId];
127
+ const sentence = parsed.sentence.trim() || assembleMatchColumnsSentence(fragmentIds, columns);
128
+ return {
129
+ stemFragmentId,
130
+ fragmentIds,
131
+ sentence,
132
+ isExample,
133
+ points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
134
+ questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
135
+ };
136
+ }
137
+ export function mapQuestionToMatchColumnsGroupData(question) {
138
+ const source = asRecord(question);
139
+ const content = asRecord(source.content);
140
+ const answer = asRecord(source.answer);
141
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
142
+ const fallbackAnswers = Array.isArray(correctAnswer.answer)
143
+ ? correctAnswer.answer
144
+ : Array.isArray(answer.answer)
145
+ ? answer.answer
146
+ : [];
147
+ const explanation = asString(source.explanation) || asString(answer.explanation);
148
+ const meta = asRecord(content.meta ?? answer.meta);
149
+ if (Array.isArray(content.columns) || Array.isArray(content.items)) {
150
+ const columns = Array.isArray(content.columns)
151
+ ? content.columns.map((column, index) => mapColumn(column, index))
152
+ : [];
153
+ const items = Array.isArray(content.items)
154
+ ? content.items.map((item, index) => mapApiItem(item, index, columns, fallbackAnswers[index]))
155
+ : [];
156
+ return {
157
+ title: asString(meta.title) || asString(answer.title),
158
+ instruction: asString(meta.instruction) ||
159
+ asString(answer.instruction) ||
160
+ MATCH_COLUMNS_DEFAULT_INSTRUCTION,
161
+ columns,
162
+ items,
163
+ explanation,
164
+ points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
165
+ };
166
+ }
167
+ if (Array.isArray(answer.columns) || Array.isArray(answer.items)) {
168
+ const columns = Array.isArray(answer.columns)
169
+ ? answer.columns.map((column, index) => mapColumn(column, index))
170
+ : [];
171
+ const items = Array.isArray(answer.items)
172
+ ? answer.items.map((item, index) => ({
173
+ ...createEmptyMatchColumnsItem(index + 1, columns),
174
+ ...item,
175
+ questionNumber: item.questionNumber || index + 1,
176
+ }))
177
+ : [];
178
+ return {
179
+ title: asString(answer.title),
180
+ instruction: asString(answer.instruction) || MATCH_COLUMNS_DEFAULT_INSTRUCTION,
181
+ columns,
182
+ items,
183
+ explanation,
184
+ points: typeof answer.points === 'number'
185
+ ? answer.points
186
+ : items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
187
+ };
188
+ }
189
+ const columns = createEmptyMatchColumnsColumns();
190
+ const items = [createEmptyMatchColumnsItem(1, columns)];
191
+ return {
192
+ title: '',
193
+ instruction: MATCH_COLUMNS_DEFAULT_INSTRUCTION,
194
+ columns,
195
+ items,
196
+ explanation,
197
+ points: 1,
198
+ };
199
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export declare const matchColumnsToMakeSentencesRegistration: QuestionTypeRegistration;
@@ -0,0 +1,36 @@
1
+ import { MatchColumnsToMakeSentencesCreator } from './MatchColumnsToMakeSentencesCreator';
2
+ import { mapQuestionToMatchColumnsGroupData } from './map-match-columns-to-make-sentences-data';
3
+ export const matchColumnsToMakeSentencesRegistration = {
4
+ component: MatchColumnsToMakeSentencesCreator,
5
+ transformInitialData: (initialData) => mapQuestionToMatchColumnsGroupData(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 transformMatchColumnsToMakeSentences: TransformHandler;
@@ -0,0 +1,66 @@
1
+ import { MATCH_COLUMNS_DEFAULT_INSTRUCTION, MATCH_COLUMNS_TO_MAKE_SENTENCE_ITEM_TYPE, } from '../../_shared/types/match-columns-to-make-sentences.type';
2
+ function sumGradablePoints(items) {
3
+ return items.reduce((total, item) => {
4
+ if (item.isExample) {
5
+ return total;
6
+ }
7
+ return total + (typeof item.points === 'number' ? item.points : 1);
8
+ }, 0);
9
+ }
10
+ function assembleSentence(fragmentIds, columns) {
11
+ const texts = new Map();
12
+ columns.forEach((column) => {
13
+ column.fragments.forEach((fragment) => {
14
+ texts.set(fragment.id, fragment.text);
15
+ });
16
+ });
17
+ return fragmentIds
18
+ .map((id) => texts.get(id) || '')
19
+ .filter(Boolean)
20
+ .join(' ')
21
+ .replace(/\s+/g, ' ')
22
+ .trim();
23
+ }
24
+ export const transformMatchColumnsToMakeSentences = (question) => {
25
+ const answerData = question.answer;
26
+ const columns = Array.isArray(answerData?.columns) ? answerData.columns : [];
27
+ const items = Array.isArray(answerData?.items) ? answerData.items : [];
28
+ if (items.length === 0) {
29
+ return {
30
+ apiContent: { columns, items: [] },
31
+ apiCorrectAnswer: { answer: [] },
32
+ };
33
+ }
34
+ const apiItems = items.map((item, index) => {
35
+ const fragmentIds = Array.isArray(item.fragmentIds) ? item.fragmentIds : [];
36
+ const sentence = item.sentence?.trim() || assembleSentence(fragmentIds, columns);
37
+ return {
38
+ questionType: MATCH_COLUMNS_TO_MAKE_SENTENCE_ITEM_TYPE,
39
+ ...(item.isExample ? { isExample: true } : {}),
40
+ content: {
41
+ stemFragmentId: item.stemFragmentId || fragmentIds[0] || '',
42
+ },
43
+ correctAnswer: {
44
+ answer: { fragmentIds, sentence },
45
+ },
46
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
47
+ questionNumber: item.questionNumber || index + 1,
48
+ };
49
+ });
50
+ const title = answerData?.title?.trim();
51
+ const instruction = answerData?.instruction || MATCH_COLUMNS_DEFAULT_INSTRUCTION;
52
+ return {
53
+ apiContent: {
54
+ meta: {
55
+ ...(title ? { title } : {}),
56
+ instruction,
57
+ },
58
+ columns,
59
+ items: apiItems,
60
+ },
61
+ apiCorrectAnswer: {
62
+ answer: apiItems.map((item) => item.correctAnswer.answer),
63
+ },
64
+ totalPoints: sumGradablePoints(items),
65
+ };
66
+ };
@@ -0,0 +1,2 @@
1
+ import type { MatchWordToPictureGroupClientProps } from '../../_shared/types/match-word-to-picture-group.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,2 @@
1
+ import type { MatchWordToPictureGroupCreatorProps } from '../../_shared/types/match-word-to-picture-group.type';
2
+ export declare function MatchWordToPictureGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: MatchWordToPictureGroupCreatorProps): import("react").JSX.Element;