@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.
Files changed (65) hide show
  1. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
  2. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
  3. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -2
  4. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +5 -1
  5. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
  6. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
  7. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
  8. package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
  9. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
  10. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
  11. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
  12. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
  13. package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
  14. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +7 -0
  15. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +4 -1
  16. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +24 -22
  17. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +45 -58
  18. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.d.ts +6 -0
  19. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.js +145 -0
  20. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +6 -15
  21. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -4
  22. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
  23. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
  24. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
  25. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
  26. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
  27. package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
  28. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
  29. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
  30. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
  31. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
  32. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
  33. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
  34. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  35. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +79 -3
  36. package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +1 -0
  37. package/dist/components/questions/types/spontaneous-qa-group/index.js +1 -0
  38. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.d.ts +2 -0
  39. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +74 -0
  40. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
  41. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
  42. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
  43. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
  44. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
  45. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
  46. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
  47. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
  48. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
  49. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
  50. package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
  51. package/dist/components/questions/viewer/QuestionViewer.js +22 -9
  52. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -79
  53. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
  54. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
  55. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
  56. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
  57. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
  58. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
  59. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
  60. package/dist/shared/lib/utils/question-reverse-transform.js +12 -4
  61. package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
  62. package/dist/shared/types/questions/answer-the-question-group.js +5 -0
  63. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -1
  64. package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
  65. 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, Type, Volume2 } from 'lucide-react';
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 { ChooseAnswerGroupImageUploadItem, ChooseAnswerGroupOptionImageField, } from './ChooseAnswerGroupImageUpload';
16
- import { fromChooseAnswerGroupImageUrls, getChooseAnswerGroupOptionImageSrc, isChooseAnswerGroupOptionFilled, toChooseAnswerGroupImageUrls, toEditableChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
17
- const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
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
- ...(item.audioUrl ? { audioUrl: item.audioUrl } : {}),
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 [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || '');
65
- const [imageUrl, setImageUrl] = useState(toChooseAnswerGroupImageUrls(initialData?.imageUrl)[0] || '');
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, nextAudioUrl = audioUrl, nextImageUrl = imageUrl) => ({
84
- instruction: nextInstruction,
85
- ...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
86
- ...(nextAudioUrl.trim() ? { audioUrl: nextAudioUrl.trim() } : {}),
87
- ...(nextImageUrl.trim() ? { imageUrl: nextImageUrl.trim() } : {}),
88
- items: nextItems,
89
- explanation: nextExplanation,
90
- points: sumPoints(nextItems),
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, audioUrl, imageUrl, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
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: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { htmlFor: "group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", title: "Th\u00EAm audio chung", onClick: () => setIsGroupAudioVisible(true), className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${audioUrl || isGroupAudioVisible
174
- ? 'bg-violet-600 text-white shadow-sm'
175
- : '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 chung", onClick: () => setIsGroupImageVisible(true), className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${imageUrl || isGroupImageVisible
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" }) })] })] }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "Choose the correct answer.", className: "h-8 border-gray-200" })] }), isGroupAudioVisible && (_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-group-audio", label: "", accept: "audio/*", value: audioUrl, onChange: setAudioUrl, maxSize: 20, placeholder: "Upload file audio ho\u1EB7c paste URL", autoUpload: true, prefix: "questions", showPlayButton: true }) })] })), isGroupImageVisible && (_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(ImageIcon, { className: "h-3.5 w-3.5 text-indigo-500" }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(FileUpload, { id: "choose-answer-group-image", label: "", accept: "image/*", value: imageUrl, onChange: setImageUrl, maxSize: 5, placeholder: "Upload \u1EA3nh nh\u00F3m ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }) })] })), !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) => (_jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: `Câu ${itemIndex + 1}`, points: !item.isExample ? {
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: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", title: "Th\u00EAm audio", onClick: () => setVisibleItemAudio((prev) => ({ ...prev, [itemIndex]: true })), className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${item.audioUrl || visibleItemAudio[itemIndex]
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 = asOptionalUrl(content.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 = asOptionalUrl(asRecord(content.meta).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 = asOptionalUrl(answer.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 = typeof item.audioUrl === 'string' ? item.audioUrl.trim() : '';
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
- if (answerData?.audioUrl && answerData.audioUrl.trim()) {
52
- meta.audioUrl = answerData.audioUrl.trim();
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) {
@@ -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;