@tinyweb_dev/oe-exam-sdk 1.0.7 → 1.0.9
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.
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -2
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +5 -1
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +7 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +4 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +24 -22
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +45 -58
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.d.ts +6 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.js +145 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +6 -15
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -4
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
- package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +79 -3
- package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/index.js +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +74 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
- package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
- package/dist/components/questions/viewer/QuestionViewer.js +22 -9
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -79
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
- package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +12 -4
- package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
- package/dist/shared/types/questions/answer-the-question-group.js +5 -0
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -1
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
- package/package.json +1 -1
|
@@ -3,18 +3,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { Label } from '../../../../components/ui/label';
|
|
6
|
-
import { Input } from '../../../../components/ui/input';
|
|
7
6
|
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
|
-
import { Switch } from '../../../../components/ui/switch';
|
|
9
|
-
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
10
7
|
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
11
8
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
12
|
-
import { BookOpen, HelpCircle, ImageIcon, Plus, Trash2,
|
|
9
|
+
import { BookOpen, HelpCircle, ImageIcon, Plus, Trash2, Volume2 } from 'lucide-react';
|
|
13
10
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
14
11
|
import { ChooseTheCorrectAnswerGroupClient } from './ChooseTheCorrectAnswerGroupClient';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
12
|
+
import { ChooseAnswerGroupAudioUploadItem, ChooseAnswerGroupImageUploadItem, } from './ChooseAnswerGroupImageUpload';
|
|
13
|
+
import { ChooseTheCorrectAnswerGroupItemEditor } from './ChooseTheCorrectAnswerGroupItemEditor';
|
|
14
|
+
import { fromChooseAnswerGroupImageUrls, isChooseAnswerGroupOptionFilled, toChooseAnswerGroupImageUrls, toEditableChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
18
15
|
function createEmptyItem(questionNumber) {
|
|
19
16
|
return {
|
|
20
17
|
question: '',
|
|
@@ -22,7 +19,6 @@ function createEmptyItem(questionNumber) {
|
|
|
22
19
|
options: [
|
|
23
20
|
{ id: 'opt-1', text: '' },
|
|
24
21
|
{ id: 'opt-2', text: '' },
|
|
25
|
-
{ id: 'opt-3', text: '' },
|
|
26
22
|
],
|
|
27
23
|
correctAnswer: 'opt-1',
|
|
28
24
|
isExample: false,
|
|
@@ -36,6 +32,7 @@ function normalizeItems(rawItems) {
|
|
|
36
32
|
}
|
|
37
33
|
return rawItems.map((item, index) => {
|
|
38
34
|
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(item.imageUrl));
|
|
35
|
+
const audioUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(item.audioUrl));
|
|
39
36
|
return {
|
|
40
37
|
question: item.question || '',
|
|
41
38
|
optionType: item.optionType === 'image' ? 'image' : 'text',
|
|
@@ -51,7 +48,7 @@ function normalizeItems(rawItems) {
|
|
|
51
48
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
52
49
|
questionNumber: item.questionNumber || index + 1,
|
|
53
50
|
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
54
|
-
...(
|
|
51
|
+
...(audioUrl !== undefined ? { audioUrl } : {}),
|
|
55
52
|
};
|
|
56
53
|
});
|
|
57
54
|
}
|
|
@@ -61,34 +58,35 @@ function sumPoints(items) {
|
|
|
61
58
|
export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
62
59
|
const [instruction, setInstruction] = useState(initialData?.instruction || 'Choose the correct answer.');
|
|
63
60
|
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
64
|
-
const [
|
|
65
|
-
const [
|
|
61
|
+
const [imageUrls, setImageUrls] = useState(() => (toEditableChooseAnswerGroupImageUrls(initialData?.imageUrl)));
|
|
62
|
+
const [audioUrls, setAudioUrls] = useState(() => (toEditableChooseAnswerGroupImageUrls(initialData?.audioUrl)));
|
|
66
63
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
67
64
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
68
65
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
69
66
|
const [errors, setErrors] = useState([]);
|
|
70
|
-
const [isGroupAudioVisible, setIsGroupAudioVisible] = useState(() => Boolean(initialData?.audioUrl));
|
|
71
|
-
const [isGroupImageVisible, setIsGroupImageVisible] = useState(() => toChooseAnswerGroupImageUrls(initialData?.imageUrl).length > 0);
|
|
72
67
|
const [isPassageVisible, setIsPassageVisible] = useState(() => {
|
|
73
68
|
const html = initialData?.passage;
|
|
74
69
|
return Boolean(html && html !== '<p></p>' && html !== '<p><br></p>' && html.trim());
|
|
75
70
|
});
|
|
76
|
-
const [visibleItemAudio, setVisibleItemAudio] = useState({});
|
|
77
71
|
const onChangeRef = useRef(onChange);
|
|
78
72
|
const previousPayloadRef = useRef('');
|
|
79
73
|
useEffect(() => {
|
|
80
74
|
onChangeRef.current = onChange;
|
|
81
75
|
}, [onChange]);
|
|
82
76
|
const isContentEmpty = (html) => !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
83
|
-
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation, nextAudioUrls = audioUrls, nextImageUrls = imageUrls) => {
|
|
78
|
+
const nextAudioUrl = fromChooseAnswerGroupImageUrls(nextAudioUrls);
|
|
79
|
+
const nextImageUrl = fromChooseAnswerGroupImageUrls(nextImageUrls);
|
|
80
|
+
return {
|
|
81
|
+
instruction: nextInstruction,
|
|
82
|
+
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
83
|
+
...(nextAudioUrl !== undefined ? { audioUrl: nextAudioUrl } : {}),
|
|
84
|
+
...(nextImageUrl !== undefined ? { imageUrl: nextImageUrl } : {}),
|
|
85
|
+
items: nextItems,
|
|
86
|
+
explanation: nextExplanation,
|
|
87
|
+
points: sumPoints(nextItems),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
92
90
|
const debouncedOnChange = useDebouncedCallback((payload) => {
|
|
93
91
|
onChangeRef.current?.(payload);
|
|
94
92
|
}, 300);
|
|
@@ -101,7 +99,7 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
101
99
|
previousPayloadRef.current = serialized;
|
|
102
100
|
onUnsavedChangesChange?.(true);
|
|
103
101
|
debouncedOnChange(payload);
|
|
104
|
-
}, [instruction, passage,
|
|
102
|
+
}, [instruction, passage, audioUrls, imageUrls, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
105
103
|
const validate = () => {
|
|
106
104
|
const nextErrors = [];
|
|
107
105
|
if (!instruction.trim()) {
|
|
@@ -111,9 +109,6 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
111
109
|
nextErrors.push('Cần ít nhất một câu hỏi.');
|
|
112
110
|
}
|
|
113
111
|
items.forEach((item, index) => {
|
|
114
|
-
if (!item.question.trim()) {
|
|
115
|
-
nextErrors.push(`Câu ${index + 1}: nội dung câu hỏi là bắt buộc.`);
|
|
116
|
-
}
|
|
117
112
|
const filledOptions = item.options.filter((option) => (isChooseAnswerGroupOptionFilled(option, item.optionType)));
|
|
118
113
|
if (filledOptions.length < 2) {
|
|
119
114
|
nextErrors.push(`Câu ${index + 1}: cần ít nhất 2 đáp án.`);
|
|
@@ -140,15 +135,6 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
140
135
|
const updateItem = (index, patch) => {
|
|
141
136
|
setItems((current) => current.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)));
|
|
142
137
|
};
|
|
143
|
-
const updateOption = (itemIndex, optionIndex, patch) => {
|
|
144
|
-
setItems((current) => current.map((item, index) => {
|
|
145
|
-
if (index !== itemIndex) {
|
|
146
|
-
return item;
|
|
147
|
-
}
|
|
148
|
-
const options = item.options.map((option, currentOptionIndex) => (currentOptionIndex === optionIndex ? { ...option, ...patch } : option));
|
|
149
|
-
return { ...item, options };
|
|
150
|
-
}));
|
|
151
|
-
};
|
|
152
138
|
const addItem = () => {
|
|
153
139
|
setItems((current) => [...current, createEmptyItem(current.length + 1)]);
|
|
154
140
|
};
|
|
@@ -170,11 +156,29 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
170
156
|
value: sumPoints(items),
|
|
171
157
|
onValueChange: () => undefined,
|
|
172
158
|
allowZero: true,
|
|
173
|
-
}, preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
}, preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer.", rows: 2, className: "min-h-[56px] border-gray-200 pb-11" }), _jsxs("div", { className: "absolute bottom-1.5 right-1.5 z-10 flex items-center gap-1 rounded-md border border-indigo-100 bg-white/95 p-0.5 shadow-sm", children: [_jsx("button", { type: "button", title: "Th\u00EAm h\u00ECnh \u1EA3nh", onClick: () => {
|
|
160
|
+
const hasEmpty = imageUrls.some((url) => !url.trim());
|
|
161
|
+
if (imageUrls.length === 0 || !hasEmpty) {
|
|
162
|
+
setImageUrls([...imageUrls, '']);
|
|
163
|
+
}
|
|
164
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${imageUrls.length > 0
|
|
176
165
|
? 'bg-indigo-600 text-white shadow-sm'
|
|
177
|
-
: 'bg-indigo-50 text-indigo-600 hover:bg-indigo-100'}`, children: _jsx(ImageIcon, { className: "h-4 w-4" }) })
|
|
166
|
+
: 'bg-indigo-50 text-indigo-600 hover:bg-indigo-100'}`, children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", title: "Th\u00EAm audio", onClick: () => {
|
|
167
|
+
const hasEmpty = audioUrls.some((url) => !url.trim());
|
|
168
|
+
if (audioUrls.length === 0 || !hasEmpty) {
|
|
169
|
+
setAudioUrls([...audioUrls, '']);
|
|
170
|
+
}
|
|
171
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${audioUrls.length > 0
|
|
172
|
+
? 'bg-violet-600 text-white shadow-sm'
|
|
173
|
+
: 'bg-violet-50 text-violet-600 hover:bg-violet-100'}`, children: _jsx(Volume2, { className: "h-4 w-4" }) })] })] })] }), imageUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [imageUrls.map((url, imageIndex) => (_jsx(ChooseAnswerGroupImageUploadItem, { index: imageIndex, itemIndex: -1, imageUrl: url, onChange: (nextUrl) => {
|
|
174
|
+
setImageUrls((current) => current.map((item, idx) => (idx === imageIndex ? nextUrl : item)));
|
|
175
|
+
}, onRemove: () => {
|
|
176
|
+
setImageUrls((current) => current.filter((_, idx) => idx !== imageIndex));
|
|
177
|
+
} }, `group-image-${imageIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setImageUrls((current) => (current.length ? [...current, ''] : [''])), className: "h-8 gap-1.5 border-dashed border-indigo-300 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm \u1EA3nh"] })] })), audioUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [audioUrls.map((url, audioIndex) => (_jsx(ChooseAnswerGroupAudioUploadItem, { index: audioIndex, itemIndex: -1, audioUrl: url, onChange: (nextUrl) => {
|
|
178
|
+
setAudioUrls((current) => current.map((item, idx) => (idx === audioIndex ? nextUrl : item)));
|
|
179
|
+
}, onRemove: () => {
|
|
180
|
+
setAudioUrls((current) => current.filter((_, idx) => idx !== audioIndex));
|
|
181
|
+
} }, `group-audio-${audioIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((current) => (current.length ? [...current, ''] : [''])), className: "h-8 gap-1.5 border-dashed border-violet-300 text-violet-600 hover:bg-violet-50 hover:text-violet-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] })), !isPassageVisible ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsPassageVisible(true), className: "h-8 gap-1.5 border-gray-200 px-2.5 text-xs text-gray-600 hover:bg-gray-50", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5" }), "Th\u00EAm n\u1ED9i dung b\u00E0i \u0111\u1ECDc"] })) : (_jsxs("div", { className: "relative space-y-1.5 pr-10", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsPassageVisible(false), className: "absolute right-0 top-0 z-10 h-7 px-2 text-xs text-gray-500 hover:text-gray-700", children: "\u1EA8n" }), _jsxs(Label, { className: "text-sm font-semibold text-gray-800", children: ["N\u1ED9i dung b\u00E0i \u0111\u1ECDc ", _jsx("span", { className: "text-xs font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, variant: "compact", minHeightClassName: "min-h-[72px]" })] }))] }), items.map((item, itemIndex) => (_jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: `Câu ${itemIndex + 1}`, points: !item.isExample ? {
|
|
178
182
|
value: item.points,
|
|
179
183
|
allowZero: true,
|
|
180
184
|
onValueChange: (value) => updateItem(itemIndex, { points: value }),
|
|
@@ -184,22 +188,5 @@ export function ChooseTheCorrectAnswerGroupCreator({ initialData, onChange, exte
|
|
|
184
188
|
isExample: checked,
|
|
185
189
|
points: checked ? 0 : (item.points || 1),
|
|
186
190
|
}),
|
|
187
|
-
}, extraActions: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, className: "h-8 w-8 text-gray-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) }), children:
|
|
188
|
-
? 'bg-violet-600 text-white shadow-sm'
|
|
189
|
-
: 'bg-violet-50 text-violet-600 hover:bg-violet-100'}`, children: _jsx(Volume2, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", title: "Th\u00EAm h\u00ECnh \u1EA3nh", onClick: () => {
|
|
190
|
-
const nextUrls = [...toEditableChooseAnswerGroupImageUrls(item.imageUrl), ''];
|
|
191
|
-
updateItem(itemIndex, { imageUrl: nextUrls });
|
|
192
|
-
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${toEditableChooseAnswerGroupImageUrls(item.imageUrl).length > 0
|
|
193
|
-
? 'bg-indigo-600 text-white shadow-sm'
|
|
194
|
-
: 'bg-indigo-50 text-indigo-600 hover:bg-indigo-100'}`, children: _jsx(ImageIcon, { className: "h-4 w-4" }) })] })] }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "A place for someone to stay or live is called", className: "min-h-[56px] border-gray-200" })] }), (item.audioUrl || visibleItemAudio[itemIndex]) && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-gray-200 bg-white p-1.5", children: [_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-indigo-50", children: _jsx(Volume2, { className: "h-3.5 w-3.5 text-indigo-500" }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(FileUpload, { id: `choose-answer-item-audio-${itemIndex}`, label: "", accept: "audio/*", value: item.audioUrl || '', onChange: (url) => updateItem(itemIndex, { audioUrl: url }), maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) })] })), toEditableChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "space-y-2", children: toEditableChooseAnswerGroupImageUrls(item.imageUrl).map((imageUrl, imageIndex) => (_jsx(ChooseAnswerGroupImageUploadItem, { index: imageIndex, itemIndex: itemIndex, imageUrl: imageUrl, onChange: (url) => {
|
|
195
|
-
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.imageUrl);
|
|
196
|
-
nextUrls[imageIndex] = url;
|
|
197
|
-
updateItem(itemIndex, { imageUrl: nextUrls });
|
|
198
|
-
}, onRemove: () => {
|
|
199
|
-
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.imageUrl)
|
|
200
|
-
.filter((_, idx) => idx !== imageIndex);
|
|
201
|
-
updateItem(itemIndex, { imageUrl: fromChooseAnswerGroupImageUrls(nextUrls) });
|
|
202
|
-
} }, `${itemIndex}-${imageIndex}`))) })), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "\u0110\u00E1p \u00E1n" }), _jsxs("div", { className: "flex h-8 items-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-2.5", children: [_jsx(Type, { className: `h-3.5 w-3.5 ${item.optionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${item.optionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}`, children: "Text" }), _jsx(Switch, { checked: item.optionType === 'image', onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
203
|
-
optionType: checked ? 'image' : 'text',
|
|
204
|
-
}) }), _jsx(ImageIcon, { className: `h-3.5 w-3.5 ${item.optionType === 'image' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${item.optionType === 'image' ? 'text-indigo-600' : 'text-gray-400'}`, children: "\u1EA2nh" })] })] }), item.options.map((option, optionIndex) => (_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("input", { type: "radio", name: `correct-${itemIndex}`, checked: item.correctAnswer === option.id, onChange: () => updateItem(itemIndex, { correctAnswer: option.id }), className: "mt-2" }), _jsx("span", { className: "mt-2 w-6 text-sm font-semibold text-gray-600", children: OPTION_LABELS[optionIndex] || optionIndex + 1 }), item.optionType === 'image' ? (_jsx(ChooseAnswerGroupOptionImageField, { itemIndex: itemIndex, optionIndex: optionIndex, imageUrl: getChooseAnswerGroupOptionImageSrc(option), onChange: (url) => updateOption(itemIndex, optionIndex, { imageUrl: url, text: '' }) })) : (_jsx(Input, { value: option.text, onChange: (event) => updateOption(itemIndex, optionIndex, { text: event.target.value }), placeholder: `Đáp án ${OPTION_LABELS[optionIndex]}`, className: "h-8 border-gray-200" }))] }, option.id)))] })] }, `item-${itemIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: addItem, className: "h-8 gap-1.5 border-dashed border-green-200 px-2.5 text-xs text-green-600 hover:bg-green-50 hover:text-green-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u00E2u h\u1ECFi"] }), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) }), allErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: allErrors.map((error) => (_jsx("p", { children: error }, error))) }))] }));
|
|
191
|
+
}, extraActions: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, className: "h-8 w-8 text-gray-400 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) }), children: _jsx(ChooseTheCorrectAnswerGroupItemEditor, { item: item, itemIndex: itemIndex, onChange: (patch) => updateItem(itemIndex, patch) }) }, `item-${itemIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: addItem, className: "h-8 gap-1.5 border-dashed border-green-200 px-2.5 text-xs text-green-600 hover:bg-green-50 hover:text-green-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u00E2u h\u1ECFi"] }), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) }), allErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: allErrors.map((error) => (_jsx("p", { children: error }, error))) }))] }));
|
|
205
192
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChooseTheCorrectAnswerGroupItemData } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
2
|
+
export declare function ChooseTheCorrectAnswerGroupItemEditor({ item, itemIndex, onChange, }: {
|
|
3
|
+
item: ChooseTheCorrectAnswerGroupItemData;
|
|
4
|
+
itemIndex: number;
|
|
5
|
+
onChange: (patch: Partial<ChooseTheCorrectAnswerGroupItemData>) => void;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { Label } from '../../../../components/ui/label';
|
|
6
|
+
import { Input } from '../../../../components/ui/input';
|
|
7
|
+
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
|
+
import { Switch } from '../../../../components/ui/switch';
|
|
9
|
+
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
10
|
+
import { hasRichTextFormatting, htmlToPlainEditorText } from '../../../../shared/lib/rich-text';
|
|
11
|
+
import { CheckCircle2, ImageIcon, Pilcrow, Plus, Trash2, Type, Volume2 } from 'lucide-react';
|
|
12
|
+
import { ChooseAnswerGroupAudioUploadItem, ChooseAnswerGroupImageUploadItem, ChooseAnswerGroupOptionImageField, } from './ChooseAnswerGroupImageUpload';
|
|
13
|
+
import { fromChooseAnswerGroupImageUrls, getChooseAnswerGroupOptionImageSrc, toEditableChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
14
|
+
function optionLetter(index) {
|
|
15
|
+
return String.fromCharCode(65 + index);
|
|
16
|
+
}
|
|
17
|
+
function nextOptionId(options) {
|
|
18
|
+
const used = new Set(options.map((option) => option.id));
|
|
19
|
+
let next = 1;
|
|
20
|
+
while (used.has(`opt-${next}`)) {
|
|
21
|
+
next += 1;
|
|
22
|
+
}
|
|
23
|
+
return `opt-${next}`;
|
|
24
|
+
}
|
|
25
|
+
function shouldStartInTipTapMode(item) {
|
|
26
|
+
if (hasRichTextFormatting(item.question)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return item.options.some((option) => hasRichTextFormatting(option.text));
|
|
30
|
+
}
|
|
31
|
+
function emptyOptions(count) {
|
|
32
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
33
|
+
id: `opt-${index + 1}`,
|
|
34
|
+
text: '',
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
function ChooseAnswerTextInput({ value, onChange, isTipTapMode, placeholder, compact = false, className, }) {
|
|
38
|
+
if (isTipTapMode) {
|
|
39
|
+
return (_jsx(RichTextEditor, { value: value, onChange: onChange, variant: "compact", minHeightClassName: compact ? `min-h-[56px] ${className ?? ''}` : `min-h-[72px] ${className ?? ''}` }));
|
|
40
|
+
}
|
|
41
|
+
if (compact) {
|
|
42
|
+
return (_jsx(Input, { value: htmlToPlainEditorText(value), onChange: (event) => onChange(event.target.value), placeholder: placeholder, className: `h-10 border-gray-200 bg-white ${className ?? ''}` }));
|
|
43
|
+
}
|
|
44
|
+
return (_jsx(Textarea, { value: htmlToPlainEditorText(value), onChange: (event) => onChange(event.target.value), placeholder: placeholder, rows: 2, className: `min-h-[56px] border-gray-200 bg-white ${className ?? ''}` }));
|
|
45
|
+
}
|
|
46
|
+
export function ChooseTheCorrectAnswerGroupItemEditor({ item, itemIndex, onChange, }) {
|
|
47
|
+
const [isTipTapMode, setIsTipTapMode] = useState(() => shouldStartInTipTapMode(item));
|
|
48
|
+
const textOptionsCacheRef = useRef(null);
|
|
49
|
+
const imageOptionsCacheRef = useRef(null);
|
|
50
|
+
const imageUrls = toEditableChooseAnswerGroupImageUrls(item.imageUrl);
|
|
51
|
+
const audioUrls = toEditableChooseAnswerGroupImageUrls(item.audioUrl);
|
|
52
|
+
const hasCorrectAnswer = item.options.some((option) => option.id === item.correctAnswer);
|
|
53
|
+
const updateOption = (optionIndex, patch) => {
|
|
54
|
+
onChange({
|
|
55
|
+
options: item.options.map((option, index) => (index === optionIndex ? { ...option, ...patch } : option)),
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const switchOptionType = (nextType) => {
|
|
59
|
+
if (nextType === item.optionType) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (item.optionType === 'text') {
|
|
63
|
+
textOptionsCacheRef.current = { options: item.options, correctAnswer: item.correctAnswer };
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
imageOptionsCacheRef.current = { options: item.options, correctAnswer: item.correctAnswer };
|
|
67
|
+
}
|
|
68
|
+
const targetCache = nextType === 'text' ? textOptionsCacheRef.current : imageOptionsCacheRef.current;
|
|
69
|
+
if (targetCache) {
|
|
70
|
+
onChange({
|
|
71
|
+
optionType: nextType,
|
|
72
|
+
options: targetCache.options,
|
|
73
|
+
correctAnswer: targetCache.correctAnswer,
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const nextOptions = emptyOptions(item.options.length);
|
|
78
|
+
onChange({
|
|
79
|
+
optionType: nextType,
|
|
80
|
+
options: nextOptions,
|
|
81
|
+
correctAnswer: nextOptions[0]?.id || 'opt-1',
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
return (_jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs(Label, { className: "text-sm font-semibold text-gray-800", children: ["C\u00E2u h\u1ECFi ", _jsx("span", { className: "text-xs font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsxs("button", { type: "button", onClick: () => setIsTipTapMode((enabled) => !enabled), title: isTipTapMode ? 'Chuyển về ô nhập thường' : 'Bật định dạng chữ (in đậm, danh sách, bảng)', className: `inline-flex h-8 items-center gap-1.5 rounded-md border px-2.5 text-xs font-medium transition-colors select-none ${isTipTapMode
|
|
85
|
+
? 'border-indigo-300 bg-indigo-50 text-indigo-700'
|
|
86
|
+
: 'border-gray-200 bg-white text-gray-400 hover:text-gray-600'}`, children: [_jsx(Pilcrow, { className: "h-3.5 w-3.5" }), "\u0110\u1ECBnh d\u1EA1ng"] })] }), _jsxs("div", { className: "relative", children: [_jsx(ChooseAnswerTextInput, { value: item.question, onChange: (question) => onChange({ question }), isTipTapMode: isTipTapMode, placeholder: "Nh\u1EADp c\u00E2u h\u1ECFi...", className: "pb-11" }), _jsxs("div", { className: "absolute bottom-1.5 right-1.5 z-10 flex items-center gap-1 rounded-md border border-indigo-100 bg-white/95 p-0.5 shadow-sm", children: [_jsx("button", { type: "button", title: "Th\u00EAm h\u00ECnh \u1EA3nh", onClick: () => {
|
|
87
|
+
const current = toEditableChooseAnswerGroupImageUrls(item.imageUrl);
|
|
88
|
+
const hasEmpty = current.some((url) => !url.trim());
|
|
89
|
+
if (current.length === 0 || !hasEmpty) {
|
|
90
|
+
onChange({ imageUrl: [...current, ''] });
|
|
91
|
+
}
|
|
92
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${imageUrls.length > 0
|
|
93
|
+
? 'bg-indigo-600 text-white shadow-sm'
|
|
94
|
+
: 'bg-indigo-50 text-indigo-600 hover:bg-indigo-100'}`, children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", title: "Th\u00EAm audio", onClick: () => {
|
|
95
|
+
const current = toEditableChooseAnswerGroupImageUrls(item.audioUrl);
|
|
96
|
+
const hasEmpty = current.some((url) => !url.trim());
|
|
97
|
+
if (current.length === 0 || !hasEmpty) {
|
|
98
|
+
onChange({ audioUrl: [...current, ''] });
|
|
99
|
+
}
|
|
100
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${audioUrls.length > 0
|
|
101
|
+
? 'bg-violet-600 text-white shadow-sm'
|
|
102
|
+
: 'bg-violet-50 text-violet-600 hover:bg-violet-100'}`, children: _jsx(Volume2, { className: "h-4 w-4" }) })] })] })] }), imageUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [imageUrls.map((imageUrl, imageIndex) => (_jsx(ChooseAnswerGroupImageUploadItem, { index: imageIndex, itemIndex: itemIndex, imageUrl: imageUrl, onChange: (url) => {
|
|
103
|
+
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.imageUrl);
|
|
104
|
+
nextUrls[imageIndex] = url;
|
|
105
|
+
onChange({ imageUrl: nextUrls });
|
|
106
|
+
}, onRemove: () => {
|
|
107
|
+
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.imageUrl)
|
|
108
|
+
.filter((_, idx) => idx !== imageIndex);
|
|
109
|
+
onChange({ imageUrl: fromChooseAnswerGroupImageUrls(nextUrls) });
|
|
110
|
+
} }, `${itemIndex}-image-${imageIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
111
|
+
const current = toEditableChooseAnswerGroupImageUrls(item.imageUrl);
|
|
112
|
+
onChange({ imageUrl: current.length ? [...current, ''] : [''] });
|
|
113
|
+
}, className: "h-8 gap-1.5 border-dashed border-indigo-300 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm \u1EA3nh"] })] })), audioUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [audioUrls.map((audioUrl, audioIndex) => (_jsx(ChooseAnswerGroupAudioUploadItem, { index: audioIndex, itemIndex: itemIndex, audioUrl: audioUrl, onChange: (url) => {
|
|
114
|
+
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.audioUrl);
|
|
115
|
+
nextUrls[audioIndex] = url;
|
|
116
|
+
onChange({ audioUrl: nextUrls });
|
|
117
|
+
}, onRemove: () => {
|
|
118
|
+
const nextUrls = toEditableChooseAnswerGroupImageUrls(item.audioUrl)
|
|
119
|
+
.filter((_, idx) => idx !== audioIndex);
|
|
120
|
+
onChange({ audioUrl: fromChooseAnswerGroupImageUrls(nextUrls) });
|
|
121
|
+
} }, `${itemIndex}-audio-${audioIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
122
|
+
const current = toEditableChooseAnswerGroupImageUrls(item.audioUrl);
|
|
123
|
+
onChange({ audioUrl: current.length ? [...current, ''] : [''] });
|
|
124
|
+
}, className: "h-8 gap-1.5 border-dashed border-violet-300 text-violet-600 hover:bg-violet-50 hover:text-violet-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] })), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { className: "text-sm font-semibold text-gray-800", children: ["\u0110\u00E1p \u00E1n ", _jsxs("span", { className: "text-xs font-normal text-gray-500", children: ["(", item.options.length, " \u0111\u00E1p \u00E1n)"] })] }), _jsxs("div", { className: "flex h-8 items-center gap-1.5 rounded-md border border-gray-200 bg-gray-50 px-2.5", children: [_jsx(Type, { className: `h-3.5 w-3.5 ${item.optionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${item.optionType === 'text' ? 'text-indigo-600' : 'text-gray-400'}`, children: "Text" }), _jsx(Switch, { checked: item.optionType === 'image', className: item.optionType === 'image' ? '!bg-indigo-600' : undefined, onCheckedChange: (checked) => switchOptionType(checked ? 'image' : 'text') }), _jsx(ImageIcon, { className: `h-3.5 w-3.5 ${item.optionType === 'image' ? 'text-indigo-600' : 'text-gray-400'}` }), _jsx("span", { className: `text-xs font-medium ${item.optionType === 'image' ? 'text-indigo-600' : 'text-gray-400'}`, children: "\u1EA2nh" })] })] }), item.options.map((option, optionIndex) => {
|
|
125
|
+
const isCorrect = option.id === item.correctAnswer;
|
|
126
|
+
const letter = optionLetter(optionIndex);
|
|
127
|
+
return (_jsx("div", { className: `group relative rounded-md px-3 py-2.5 transition-all duration-200 ${isCorrect
|
|
128
|
+
? 'border-2 border-green-400 bg-gradient-to-r from-green-50 to-emerald-50 shadow-sm shadow-green-100'
|
|
129
|
+
: 'border border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm'}`, children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: `flex h-8 w-8 flex-shrink-0 cursor-pointer items-center justify-center rounded-full text-xs font-bold leading-none transition-all ${isCorrect
|
|
130
|
+
? 'bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-sm'
|
|
131
|
+
: 'bg-gray-100 text-gray-800 hover:bg-indigo-100 hover:text-indigo-600'}`, onClick: () => onChange({ correctAnswer: option.id }), children: letter }), _jsx("div", { className: "flex-1 space-y-2", children: item.optionType === 'image' ? (_jsx(ChooseAnswerGroupOptionImageField, { itemIndex: itemIndex, optionIndex: optionIndex, imageUrl: getChooseAnswerGroupOptionImageSrc(option), onChange: (url) => updateOption(optionIndex, { imageUrl: url, text: '' }) })) : (_jsx(ChooseAnswerTextInput, { value: option.text, onChange: (text) => updateOption(optionIndex, { text }), isTipTapMode: isTipTapMode, compact: true, placeholder: `Nhập đáp án ${letter}...` })) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => onChange({ correctAnswer: option.id }), className: `h-7 w-7 ${isCorrect ? 'text-green-600 hover:text-green-700' : 'text-gray-400 hover:text-green-600'}`, title: "Ch\u1ECDn l\u00E0m \u0111\u00E1p \u00E1n \u0111\u00FAng", children: _jsx(CheckCircle2, { className: "h-3.5 w-3.5" }) }), item.options.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => {
|
|
132
|
+
const nextOptions = item.options.filter((_, index) => index !== optionIndex);
|
|
133
|
+
onChange({
|
|
134
|
+
options: nextOptions,
|
|
135
|
+
correctAnswer: isCorrect
|
|
136
|
+
? (nextOptions[0]?.id || 'opt-1')
|
|
137
|
+
: item.correctAnswer,
|
|
138
|
+
});
|
|
139
|
+
}, className: "h-7 w-7 text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }) }, option.id));
|
|
140
|
+
}), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
141
|
+
onChange({
|
|
142
|
+
options: [...item.options, { id: nextOptionId(item.options), text: '' }],
|
|
143
|
+
});
|
|
144
|
+
}, className: "h-8 w-full gap-1.5 border-dashed border-green-200 px-2.5 text-xs text-green-600 hover:bg-green-50 hover:text-green-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm \u0111\u00E1p \u00E1n"] }), !hasCorrectAnswer && (_jsx("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 p-2 text-xs text-amber-800", children: _jsx("span", { children: "Ch\u01B0a ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng. Click ch\u1EEF c\u00E1i ho\u1EB7c \u2713 \u0111\u1EC3 ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng." }) }))] })] }));
|
|
145
|
+
}
|
|
@@ -18,13 +18,6 @@ function asOptionId(value) {
|
|
|
18
18
|
}
|
|
19
19
|
return '';
|
|
20
20
|
}
|
|
21
|
-
function asOptionalUrl(value) {
|
|
22
|
-
if (typeof value !== 'string') {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
const trimmed = value.trim();
|
|
26
|
-
return trimmed !== '' ? trimmed : undefined;
|
|
27
|
-
}
|
|
28
21
|
export function createEmptyChooseAnswerGroupItem(questionNumber) {
|
|
29
22
|
return {
|
|
30
23
|
question: '',
|
|
@@ -32,7 +25,6 @@ export function createEmptyChooseAnswerGroupItem(questionNumber) {
|
|
|
32
25
|
options: [
|
|
33
26
|
{ id: 'opt-1', text: '' },
|
|
34
27
|
{ id: 'opt-2', text: '' },
|
|
35
|
-
{ id: 'opt-3', text: '' },
|
|
36
28
|
],
|
|
37
29
|
correctAnswer: 'opt-1',
|
|
38
30
|
isExample: false,
|
|
@@ -64,11 +56,10 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
|
|
|
64
56
|
: [
|
|
65
57
|
{ id: 'opt-1', text: '' },
|
|
66
58
|
{ id: 'opt-2', text: '' },
|
|
67
|
-
{ id: 'opt-3', text: '' },
|
|
68
59
|
];
|
|
69
60
|
const isExample = record.isExample === true;
|
|
70
61
|
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(content.imageUrl));
|
|
71
|
-
const audioUrl =
|
|
62
|
+
const audioUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(content.audioUrl));
|
|
72
63
|
return {
|
|
73
64
|
question: asString(content.question) || asString(record.question),
|
|
74
65
|
optionType: asString(content.optionType) === 'image' ? 'image' : 'text',
|
|
@@ -78,7 +69,7 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
|
|
|
78
69
|
points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
|
|
79
70
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
80
71
|
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
81
|
-
...(audioUrl ? { audioUrl } : {}),
|
|
72
|
+
...(audioUrl !== undefined ? { audioUrl } : {}),
|
|
82
73
|
};
|
|
83
74
|
}
|
|
84
75
|
export function mapQuestionToChooseAnswerGroupData(question) {
|
|
@@ -92,12 +83,12 @@ export function mapQuestionToChooseAnswerGroupData(question) {
|
|
|
92
83
|
: [];
|
|
93
84
|
if (Array.isArray(content.items)) {
|
|
94
85
|
const items = content.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index]));
|
|
95
|
-
const audioUrl =
|
|
86
|
+
const audioUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(asRecord(content.meta).audioUrl));
|
|
96
87
|
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(asRecord(content.meta).imageUrl));
|
|
97
88
|
return {
|
|
98
89
|
instruction: asString(asRecord(content.meta).instruction) || 'Choose the correct answer.',
|
|
99
90
|
passage: asString(asRecord(content.meta).passage),
|
|
100
|
-
...(audioUrl ? { audioUrl } : {}),
|
|
91
|
+
...(audioUrl !== undefined ? { audioUrl } : {}),
|
|
101
92
|
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
102
93
|
items,
|
|
103
94
|
explanation,
|
|
@@ -105,12 +96,12 @@ export function mapQuestionToChooseAnswerGroupData(question) {
|
|
|
105
96
|
};
|
|
106
97
|
}
|
|
107
98
|
if (Array.isArray(answer.items)) {
|
|
108
|
-
const audioUrl =
|
|
99
|
+
const audioUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answer.audioUrl));
|
|
109
100
|
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answer.imageUrl));
|
|
110
101
|
return {
|
|
111
102
|
instruction: asString(answer.instruction, 'Choose the correct answer.'),
|
|
112
103
|
passage: asString(answer.passage),
|
|
113
|
-
...(audioUrl ? { audioUrl } : {}),
|
|
104
|
+
...(audioUrl !== undefined ? { audioUrl } : {}),
|
|
114
105
|
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
115
106
|
items: answer.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index])),
|
|
116
107
|
explanation,
|
|
@@ -26,7 +26,7 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
|
|
|
26
26
|
}));
|
|
27
27
|
const correctAnswer = item.correctAnswer || options[0]?.id || 'opt-1';
|
|
28
28
|
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(item.imageUrl));
|
|
29
|
-
const audioUrl =
|
|
29
|
+
const audioUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(item.audioUrl));
|
|
30
30
|
return {
|
|
31
31
|
questionType: CHILD_QUESTION_TYPE,
|
|
32
32
|
...(item.isExample ? { isExample: true } : {}),
|
|
@@ -35,7 +35,7 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
|
|
|
35
35
|
options,
|
|
36
36
|
optionType: item.optionType || 'text',
|
|
37
37
|
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
38
|
-
...(audioUrl ? { audioUrl } : {}),
|
|
38
|
+
...(audioUrl !== undefined ? { audioUrl } : {}),
|
|
39
39
|
},
|
|
40
40
|
correctAnswer: { answer: correctAnswer },
|
|
41
41
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
@@ -48,8 +48,9 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
|
|
|
48
48
|
if (answerData?.passage && answerData.passage.trim()) {
|
|
49
49
|
meta.passage = answerData.passage.trim();
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const groupAudioUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answerData?.audioUrl));
|
|
52
|
+
if (groupAudioUrl !== undefined) {
|
|
53
|
+
meta.audioUrl = groupAudioUrl;
|
|
53
54
|
}
|
|
54
55
|
const groupImageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answerData?.imageUrl));
|
|
55
56
|
if (groupImageUrl !== undefined) {
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ChooseThenAnswerGroupClientProps } from '../../_shared/types/choose-then-answer-group.type';
|
|
2
|
-
export declare function ChooseThenAnswerGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: ChooseThenAnswerGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function ChooseThenAnswerGroupClient({ questionData, isReviewMode, userAnswers, itemResults, onAnswerChange, }: ChooseThenAnswerGroupClientProps): import("react").JSX.Element;
|