@tinyweb_dev/oe-exam-sdk 0.2.6 → 0.2.8

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 (30) hide show
  1. package/dist/components/exams/take/components/QuestionRenderer.js +1 -1
  2. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +2 -2
  3. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +2 -2
  4. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +58 -14
  5. package/dist/components/exams/take/types.d.ts +12 -5
  6. package/dist/components/exams/take/utils/answer-transformers.js +1 -1
  7. package/dist/components/exams/take/utils/question-transformers.d.ts +1 -0
  8. package/dist/components/exams/take/utils/question-transformers.js +29 -53
  9. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -0
  10. package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +16 -1
  11. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +13 -3
  12. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +7 -3
  13. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +13 -0
  14. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -0
  15. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.d.ts +1 -1
  16. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +37 -60
  17. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +122 -149
  18. package/dist/components/questions/types/fill-in-blank/answer-utils.d.ts +34 -0
  19. package/dist/components/questions/types/fill-in-blank/answer-utils.js +58 -0
  20. package/dist/components/questions/types/fill-in-blank/register.js +19 -6
  21. package/dist/components/questions/types/fill-in-blank/transform.js +23 -26
  22. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +1 -1
  23. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +2 -3
  24. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +17 -8
  25. package/dist/components/results/renderers/review-question-body-dedicated.js +3 -3
  26. package/dist/shared/lib/utils/fill-in-blank.d.ts +46 -0
  27. package/dist/shared/lib/utils/fill-in-blank.js +96 -0
  28. package/dist/shared/lib/utils/question-reverse-transform.js +68 -3
  29. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +2 -0
  30. package/package.json +1 -1
@@ -26,6 +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
30
  return {
30
31
  questionType: CHILD_QUESTION_TYPE,
31
32
  ...(item.isExample ? { isExample: true } : {}),
@@ -34,6 +35,7 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
34
35
  options,
35
36
  optionType: item.optionType || 'text',
36
37
  ...(imageUrl !== undefined ? { imageUrl } : {}),
38
+ ...(audioUrl ? { audioUrl } : {}),
37
39
  },
38
40
  correctAnswer: { answer: correctAnswer },
39
41
  points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
@@ -46,6 +48,9 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
46
48
  if (answerData?.passage && answerData.passage.trim()) {
47
49
  meta.passage = answerData.passage.trim();
48
50
  }
51
+ if (answerData?.audioUrl && answerData.audioUrl.trim()) {
52
+ meta.audioUrl = answerData.audioUrl.trim();
53
+ }
49
54
  return {
50
55
  apiContent: {
51
56
  meta,
@@ -1,2 +1,2 @@
1
1
  import { FillInBlankClientProps } from '../../_shared/types/fill-in-blank.type';
2
- export declare function FillInBlankClient({ questionData, onSubmit, isReviewMode, userAnswer, autoFillCorrectAnswers, explanation: explanationProp, }: FillInBlankClientProps): import("react").JSX.Element;
2
+ export declare function FillInBlankClient({ questionData, questionConfig, onSubmit, isReviewMode, userAnswer, autoFillCorrectAnswers, explanation: explanationProp, }: FillInBlankClientProps): import("react").JSX.Element;
@@ -2,22 +2,11 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useMemo } from 'react';
4
4
  import { Input } from '../../../../components/ui/input';
5
- import { Check, X, PenLine, AlertTriangle, ClipboardList, Lightbulb, BookOpen, Type, Hash, } from 'lucide-react';
5
+ import { Check, X, PenLine, AlertTriangle, ClipboardList, Lightbulb, BookOpen, Hash, Volume2, } from 'lucide-react';
6
+ import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAnswer, isBlankInputCorrect, normalizeBlankAnswers, normalizeFillInBlankAnswers, } from './answer-utils';
7
+ import { parseFillBlankSegments } from '../../../../shared/lib/utils/fill-in-blank';
6
8
  import { cn } from '../../../../shared/lib/utils';
7
9
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
8
- // Helper: Extract placeholder indices from question text
9
- function extractPlaceholderIndices(question) {
10
- const regex = /\{(\d+)\}/g;
11
- const indices = [];
12
- let match;
13
- while ((match = regex.exec(question)) !== null) {
14
- const index = parseInt(match[1], 10);
15
- if (!indices.includes(index)) {
16
- indices.push(index);
17
- }
18
- }
19
- return indices.sort((a, b) => a - b);
20
- }
21
10
  // Sub-component: Resolve presigned URL for group image
22
11
  function GroupImage({ src, alt, className }) {
23
12
  const { previewUrl, isLoading } = usePresignedFileUrl(src);
@@ -32,7 +21,7 @@ function GroupImage({ src, alt, className }) {
32
21
  target.src = '/images/placeholder-image.svg';
33
22
  } }));
34
23
  }
35
- export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false, userAnswer = [], autoFillCorrectAnswers = false, explanation: explanationProp, }) {
24
+ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isReviewMode = false, userAnswer = [], autoFillCorrectAnswers = false, explanation: explanationProp, }) {
36
25
  // Detect if this is new format (question + answers) or legacy format (blanks array)
37
26
  const isLegacyFormat = !questionData.question && questionData.blanks && questionData.blanks.length > 0;
38
27
  const explanation = explanationProp || questionData.explanation;
@@ -47,37 +36,48 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
47
36
  const { previewUrl: imagePreviewUrl, isLoading: isImageLoading } = usePresignedFileUrl(
48
37
  // Only use question-level imageUrl when NOT grouped (group image is handled separately)
49
38
  (!isGroup && questionData.imageUrl) ? questionData.imageUrl : '');
39
+ // Per-question audio — show when configured or when audioUrl already saved
40
+ const cleanAudioUrl = questionData.audioUrl?.trim() ? questionData.audioUrl : '';
41
+ const showAudio = Boolean(questionConfig?.includes('audio') || questionConfig?.includes('audioUrl') || cleanAudioUrl);
42
+ const { previewUrl: audioPreviewUrl } = usePresignedFileUrl(cleanAudioUrl || undefined);
50
43
  // For new format: extract placeholders from question
51
44
  const placeholderIndices = useMemo(() => {
52
45
  if (isLegacyFormat)
53
46
  return [];
54
47
  return extractPlaceholderIndices(questionData.question || '');
55
48
  }, [questionData.question, isLegacyFormat]);
56
- // Initialize answers based on format
49
+ // Canonical correct answers matrix (string[][]), supports legacy string[]
50
+ const correctAnswersMatrix = useMemo(() => {
51
+ if (isLegacyFormat && questionData.blanks) {
52
+ return normalizeFillInBlankAnswers(questionData.blanks.map((b) => b.correctAnswers ?? b.correctAnswer ?? ''));
53
+ }
54
+ return normalizeFillInBlankAnswers(questionData.answers || []);
55
+ }, [isLegacyFormat, questionData.answers, questionData.blanks]);
56
+ // Student answers are always flat string[] (one input per blank)
57
57
  const getInitialAnswers = () => {
58
58
  if (autoFillCorrectAnswers) {
59
59
  if (isLegacyFormat && questionData.blanks) {
60
- return questionData.blanks.map(b => b.correctAnswer);
60
+ return questionData.blanks.map((b) => getPrimaryBlankAnswer(b.correctAnswers ?? normalizeBlankAnswers(b.correctAnswer)));
61
61
  }
62
- return questionData.answers || [];
62
+ return correctAnswersMatrix.map((alts) => getPrimaryBlankAnswer(alts));
63
63
  }
64
64
  return userAnswer || [];
65
65
  };
66
- const [answers, setAnswers] = useState(getInitialAnswers());
66
+ const [answers, setAnswers] = useState(getInitialAnswers);
67
67
  // Sync userAnswer when it changes
68
68
  useEffect(() => {
69
69
  if (autoFillCorrectAnswers) {
70
70
  if (isLegacyFormat && questionData.blanks) {
71
- setAnswers(questionData.blanks.map(b => b.correctAnswer));
71
+ setAnswers(questionData.blanks.map((b) => getPrimaryBlankAnswer(b.correctAnswers ?? normalizeBlankAnswers(b.correctAnswer))));
72
72
  }
73
73
  else {
74
- setAnswers(questionData.answers || []);
74
+ setAnswers(correctAnswersMatrix.map((alts) => getPrimaryBlankAnswer(alts)));
75
75
  }
76
76
  }
77
77
  else if (userAnswer) {
78
78
  setAnswers(userAnswer);
79
79
  }
80
- }, [userAnswer, autoFillCorrectAnswers, questionData, isLegacyFormat]);
80
+ }, [userAnswer, autoFillCorrectAnswers, questionData, isLegacyFormat, correctAnswersMatrix]);
81
81
  // Handle answer change for specific placeholder index
82
82
  const handleAnswerChange = (index, value) => {
83
83
  if (isReviewMode)
@@ -90,26 +90,13 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
90
90
  setAnswers(newAnswers);
91
91
  onSubmit?.(newAnswers);
92
92
  };
93
- // Check if answer at index is correct
93
+ // Check if answer at index is correct against any equivalent
94
94
  const isAnswerCorrect = (index, userInput) => {
95
- let correctAnswer;
96
95
  if (isLegacyFormat && questionData.blanks?.[index]) {
97
- correctAnswer = questionData.blanks[index].correctAnswer;
98
- }
99
- else if (questionData.answers?.[index]) {
100
- correctAnswer = questionData.answers[index];
101
- }
102
- else {
103
- return false;
104
- }
105
- const answer = userInput.trim();
106
- const correct = correctAnswer.trim();
107
- if (questionData.caseSensitive) {
108
- return answer === correct;
109
- }
110
- else {
111
- return answer.toLowerCase() === correct.toLowerCase();
96
+ const blank = questionData.blanks[index];
97
+ return isBlankInputCorrect(userInput, blank.correctAnswers ?? normalizeBlankAnswers(blank.correctAnswer));
112
98
  }
99
+ return isBlankInputCorrect(userInput, correctAnswersMatrix[index]);
113
100
  };
114
101
  // Calculate score
115
102
  const calculateScore = () => {
@@ -145,12 +132,10 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
145
132
  };
146
133
  /** Render inline blanks within question text (new format) */
147
134
  const renderQuestionWithBlanks = () => {
148
- const question = questionData.question || '';
149
- const parts = question.split(/(\{\d+\})/g);
150
- return (_jsx("div", { className: "flex flex-wrap items-baseline gap-x-1.5 gap-y-2 leading-loose text-[15px]", children: parts.map((part, partIndex) => {
151
- const match = part.match(/\{(\d+)\}/);
152
- if (match) {
153
- const placeholderIndex = parseInt(match[1], 10);
135
+ const segments = parseFillBlankSegments(questionData.question || '');
136
+ return (_jsx("div", { className: "flex flex-wrap items-baseline gap-x-1.5 gap-y-2 leading-loose text-[15px]", children: segments.map((segment, partIndex) => {
137
+ if (segment.type === 'blank') {
138
+ const placeholderIndex = segment.index;
154
139
  const userInput = answers[placeholderIndex] || '';
155
140
  const isCorrect = isReviewMode && isAnswerCorrect(placeholderIndex, userInput);
156
141
  const isWrong = isReviewMode && userInput && !isCorrect;
@@ -162,9 +147,7 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
162
147
  : 'border-gray-200 bg-gray-50 text-gray-500'
163
148
  : 'border-orange-200 bg-white hover:border-orange-300 focus:border-orange-400 focus:ring-orange-200/50'), placeholder: "..." }), isReviewMode && (isCorrect || isWrong) && (_jsx("span", { className: cn('absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full shadow-sm ring-2 ring-white', isCorrect ? 'bg-emerald-500' : 'bg-red-500'), children: isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5 text-white" })) : (_jsx(X, { className: "h-2.5 w-2.5 text-white" })) }))] }, partIndex));
164
149
  }
165
- else {
166
- return (_jsx("span", { className: "text-slate-800 font-medium", children: part }, partIndex));
167
- }
150
+ return (_jsx("span", { className: "text-slate-800 font-medium", children: segment.value }, partIndex));
168
151
  }) }));
169
152
  };
170
153
  /** Legacy format: render blanks array */
@@ -195,12 +178,6 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
195
178
  }
196
179
  return (_jsxs("div", { className: "rounded-xl border border-violet-200 bg-gradient-to-r from-violet-50/80 to-purple-50/50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-lg bg-violet-100 ring-1 ring-violet-200/60", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5 text-violet-600" }) }), _jsx("span", { className: "text-sm font-semibold text-violet-800", children: "G\u1EE3i \u00FD \u0111\u00E1p \u00E1n" })] }), _jsx("div", { className: "flex flex-wrap gap-2", children: questionData.hints.map((hint, index) => (_jsx("span", { className: "inline-flex items-center rounded-full border border-violet-200 bg-white px-3 py-1 text-sm font-medium text-violet-700 shadow-sm transition-all hover:border-violet-300 hover:bg-violet-50 hover:shadow", children: hint }, index))) })] }));
197
180
  };
198
- /** Case-sensitivity warning */
199
- const renderCaseSensitiveNote = () => {
200
- if (isReviewMode || !questionData.caseSensitive)
201
- return null;
202
- return (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50/50 px-4 py-3", children: [_jsx("div", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm", children: _jsx(Type, { className: "h-3.5 w-3.5" }) }), _jsxs("div", { children: [_jsx("span", { className: "text-sm font-semibold text-amber-800", children: "Ph\u00E2n bi\u1EC7t hoa/th\u01B0\u1EDDng" }), _jsx("span", { className: "text-sm text-amber-700 ml-1", children: "\u2013 Vui l\u00F2ng nh\u1EADp \u0111\u00FAng ch\u1EEF hoa/th\u01B0\u1EDDng" })] })] }));
203
- };
204
181
  /** Explanation card (review mode only) */
205
182
  const renderExplanation = () => {
206
183
  const explText = explanation || questionData.explanation;
@@ -231,9 +208,9 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
231
208
  ? 'bg-emerald-100 text-emerald-700 ring-1 ring-emerald-200/60'
232
209
  : score.correct >= score.total / 2
233
210
  ? 'bg-yellow-100 text-yellow-700 ring-1 ring-yellow-200/60'
234
- : 'bg-red-100 text-red-700 ring-1 ring-red-200/60'), children: [score.correct === score.total && _jsx(Check, { className: "h-3 w-3" }), score.correct, "/", score.total, " \u0111\u00FAng"] }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [_jsx("div", { className: "rounded-xl bg-gradient-to-r from-orange-50/80 to-amber-50/50 p-5 border border-orange-100/80", children: renderQuestionWithBlanks() }), renderQuestionImage(), renderHints(), isReviewMode && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-slate-50 to-gray-50 px-4 py-2.5", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-slate-500 text-white", children: _jsx(ClipboardList, { className: "h-3 w-3" }) }), _jsx("h4", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wider", children: "Chi ti\u1EBFt \u0111\u00E1p \u00E1n" })] }), _jsx("div", { className: "p-4 space-y-2.5", children: placeholderIndices.map((idx) => {
211
+ : 'bg-red-100 text-red-700 ring-1 ring-red-200/60'), children: [score.correct === score.total && _jsx(Check, { className: "h-3 w-3" }), score.correct, "/", score.total, " \u0111\u00FAng"] }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx("div", { className: "rounded-xl bg-gradient-to-r from-orange-50/80 to-amber-50/50 p-5 border border-orange-100/80", children: renderQuestionWithBlanks() }), renderQuestionImage(), renderHints(), isReviewMode && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-slate-50 to-gray-50 px-4 py-2.5", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-slate-500 text-white", children: _jsx(ClipboardList, { className: "h-3 w-3" }) }), _jsx("h4", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wider", children: "Chi ti\u1EBFt \u0111\u00E1p \u00E1n" })] }), _jsx("div", { className: "p-4 space-y-2.5", children: placeholderIndices.map((idx) => {
235
212
  const userInput = answers[idx] || '';
236
- const correctAnswer = questionData.answers?.[idx] || '';
213
+ const correctAnswer = formatBlankAnswersDisplay(correctAnswersMatrix[idx]) || '';
237
214
  const isCorrect = isAnswerCorrect(idx, userInput);
238
215
  const hasInput = userInput.trim().length > 0;
239
216
  return (_jsxs("div", { className: cn('rounded-lg border p-3 transition-colors', isCorrect
@@ -241,7 +218,7 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
241
218
  : hasInput
242
219
  ? 'border-red-200 bg-red-50/50'
243
220
  : 'border-gray-100 bg-gray-50/50'), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-orange-100 text-xs font-bold text-orange-600 ring-1 ring-orange-200/60", children: idx + 1 }), _jsxs("span", { className: "text-sm text-gray-500", children: ["Ch\u1ED7 tr\u1ED1ng ", idx + 1] })] }), _jsx("div", { className: cn('flex items-center gap-1 text-xs font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3.5 w-3.5" }), " \u0110\u00FAng"] })) : hasInput ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-3.5 w-3.5" }), " Sai"] })) : ('Bỏ trống') })] }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white px-3 py-2 border border-gray-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E3 nh\u1EADp" }), _jsx("span", { className: cn('font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 px-3 py-2 border border-emerald-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E1p \u00E1n" }), _jsx("span", { className: "font-semibold text-emerald-600", children: correctAnswer })] })] })] }, idx));
244
- }) })] })), !isReviewMode && !hasAnyAnswer && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx(AlertTriangle, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi v\u00E0o c\u00E1c ch\u1ED7 tr\u1ED1ng" })] }))] })] }), renderCaseSensitiveNote(), renderExplanation()] }));
221
+ }) })] })), !isReviewMode && !hasAnyAnswer && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx(AlertTriangle, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi v\u00E0o c\u00E1c ch\u1ED7 tr\u1ED1ng" })] }))] })] }), renderExplanation()] }));
245
222
  }
246
223
  // =====================================================
247
224
  // LEGACY FORMAT RENDER
@@ -252,14 +229,14 @@ export function FillInBlankClient({ questionData, onSubmit, isReviewMode = false
252
229
  const hasAnswered = userInput.trim() !== '';
253
230
  return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:shadow-md", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-5 py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 to-amber-600 text-white shadow-sm", children: _jsx(PenLine, { className: "h-4 w-4" }) }), _jsxs("span", { className: "text-sm font-semibold text-gray-700", children: ["C\u00E2u ", index + 1, ": \u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng"] })] }), isReviewMode && hasAnswered && (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-bold', isCorrect
254
231
  ? 'bg-emerald-100 text-emerald-700 ring-1 ring-emerald-200/60'
255
- : 'bg-red-100 text-red-700 ring-1 ring-red-200/60'), children: [isCorrect ? _jsx(Check, { className: "h-3.5 w-3.5" }) : _jsx(X, { className: "h-3.5 w-3.5" }), isCorrect ? 'Đúng' : 'Sai'] }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [_jsx("div", { className: "rounded-xl bg-gradient-to-r from-orange-50/80 to-amber-50/50 p-5 border border-orange-100/80", children: renderLegacyBlank(blank.text, blank.id, index) }), index === 0 && renderQuestionImage(), index === 0 && renderHints(), isReviewMode && (_jsx("div", { className: cn('rounded-xl border p-3 transition-colors', isCorrect
232
+ : 'bg-red-100 text-red-700 ring-1 ring-red-200/60'), children: [isCorrect ? _jsx(Check, { className: "h-3.5 w-3.5" }) : _jsx(X, { className: "h-3.5 w-3.5" }), isCorrect ? 'Đúng' : 'Sai'] }))] }), _jsxs("div", { className: "p-5 space-y-4", children: [index === 0 && showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx("div", { className: "rounded-xl bg-gradient-to-r from-orange-50/80 to-amber-50/50 p-5 border border-orange-100/80", children: renderLegacyBlank(blank.text, blank.id, index) }), index === 0 && renderQuestionImage(), index === 0 && renderHints(), isReviewMode && (_jsx("div", { className: cn('rounded-xl border p-3 transition-colors', isCorrect
256
233
  ? 'border-emerald-200 bg-emerald-50/50'
257
234
  : hasAnswered
258
235
  ? 'border-red-200 bg-red-50/50'
259
- : 'border-gray-100 bg-gray-50/50'), children: _jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white p-3 border border-gray-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E3 nh\u1EADp:" }), _jsx("span", { className: cn('text-sm font-semibold', isCorrect ? 'text-emerald-600' : hasAnswered ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 p-3 border border-emerald-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-emerald-600", children: blank.correctAnswer })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-md p-3 border', isCorrect
236
+ : 'border-gray-100 bg-gray-50/50'), children: _jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white p-3 border border-gray-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E3 nh\u1EADp:" }), _jsx("span", { className: cn('text-sm font-semibold', isCorrect ? 'text-emerald-600' : hasAnswered ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 p-3 border border-emerald-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-emerald-600", children: formatBlankAnswersDisplay(blank.correctAnswers ?? normalizeBlankAnswers(blank.correctAnswer)) || '—' })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-md p-3 border', isCorrect
260
237
  ? 'bg-emerald-50 border-emerald-200'
261
238
  : 'bg-red-50 border-red-200'), children: [_jsx("span", { className: "text-sm text-gray-500", children: "K\u1EBFt qu\u1EA3:" }), _jsx("div", { className: cn('flex items-center gap-1.5 text-sm font-bold', isCorrect ? 'text-emerald-600' : 'text-red-600'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4" }), " Ch\u00EDnh x\u00E1c"] })) : (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-4 w-4" }), " Sai"] })) })] })] }) })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx(AlertTriangle, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi" })] }))] })] }, blank.id));
262
- }), renderCaseSensitiveNote(), isReviewMode && score && (questionData.blanks?.length || 0) > 1 && (_jsx("div", { className: cn('overflow-hidden rounded-xl border shadow-sm', score.correct === score.total
239
+ }), isReviewMode && score && (questionData.blanks?.length || 0) > 1 && (_jsx("div", { className: cn('overflow-hidden rounded-xl border shadow-sm', score.correct === score.total
263
240
  ? 'border-emerald-200 bg-gradient-to-r from-emerald-50 to-green-50'
264
241
  : score.correct >= score.total / 2
265
242
  ? 'border-yellow-200 bg-gradient-to-r from-yellow-50 to-amber-50'