@tinyweb_dev/oe-exam-sdk 0.2.6 → 0.2.7
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/MoversListenAndWriteRenderer.d.ts +2 -2
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +58 -14
- package/dist/components/exams/take/types.d.ts +12 -5
- package/dist/components/exams/take/utils/answer-transformers.js +1 -1
- package/dist/components/exams/take/utils/question-transformers.js +19 -53
- package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +16 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.d.ts +1 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +37 -60
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +122 -149
- package/dist/components/questions/types/fill-in-blank/answer-utils.d.ts +34 -0
- package/dist/components/questions/types/fill-in-blank/answer-utils.js +58 -0
- package/dist/components/questions/types/fill-in-blank/register.js +19 -6
- package/dist/components/questions/types/fill-in-blank/transform.js +23 -26
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +2 -3
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +17 -8
- package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
- package/dist/shared/lib/utils/fill-in-blank.d.ts +46 -0
- package/dist/shared/lib/utils/fill-in-blank.js +96 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +64 -3
- package/package.json +1 -1
package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ interface MoversListenAndWriteRendererProps {
|
|
|
10
10
|
exampleText?: string;
|
|
11
11
|
exampleHighlight?: string;
|
|
12
12
|
questions: FillBlankQuestion[];
|
|
13
|
-
answers: Record<string,
|
|
14
|
-
onAnswerChange: (questionId: string, value: string) => void;
|
|
13
|
+
answers: Record<string, unknown>;
|
|
14
|
+
onAnswerChange: (questionId: string, value: string[]) => void;
|
|
15
15
|
isReviewMode?: boolean;
|
|
16
16
|
showHints?: boolean;
|
|
17
17
|
groupContent?: string;
|
package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js
CHANGED
|
@@ -5,22 +5,68 @@ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-y
|
|
|
5
5
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
6
6
|
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
7
7
|
import CambridgeYleExampleText from '../../../../../components/themes/cambridge-yle/CambridgeYleExampleText';
|
|
8
|
-
|
|
8
|
+
import { countBlanks, formatAcceptedAnswers, getPrimaryAnswer, isBlankCorrect, toBlankValues, } from '../../../../../shared/lib/utils/fill-in-blank';
|
|
9
|
+
/** Preserve author newlines in fill-blank stems by mapping `\n` → `<br />`. */
|
|
9
10
|
function renderTextWithLineBreaks(text) {
|
|
10
11
|
const lines = text.split('\n');
|
|
11
12
|
return lines.map((line, index) => (_jsxs(Fragment, { children: [index > 0 ? _jsx("br", {}) : null, line] }, index)));
|
|
12
13
|
}
|
|
14
|
+
function blankInputClassName({ isExampleQ, isReviewMode, isCorrect, isWrong, }) {
|
|
15
|
+
if (isExampleQ)
|
|
16
|
+
return 'border-amber-300 bg-amber-100/50 text-amber-800 disabled:opacity-100';
|
|
17
|
+
if (isReviewMode) {
|
|
18
|
+
if (isCorrect)
|
|
19
|
+
return 'border-emerald-400 bg-emerald-50 text-emerald-800';
|
|
20
|
+
if (isWrong)
|
|
21
|
+
return 'border-rose-400 bg-rose-50 text-rose-800';
|
|
22
|
+
return 'border-slate-200 bg-slate-50 text-slate-500';
|
|
23
|
+
}
|
|
24
|
+
return 'border-blue-200 bg-blue-50/30 text-blue-900 focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-100';
|
|
25
|
+
}
|
|
13
26
|
export function MoversListenAndWriteRenderer({ partNumber, partName, questionCount, instruction, imageUrl, audioUrl, title, exampleText, exampleHighlight, questions, answers, onAnswerChange, isReviewMode = false, showHints = false, groupContent, groupTitle, }) {
|
|
14
27
|
const hasGroupContent = !!(audioUrl || imageUrl || groupContent || exampleText);
|
|
15
28
|
return (_jsxs("div", { className: "flex flex-col h-full gap-4 sm:gap-6 overflow-y-auto custom-scrollbar relative", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, hasExample: !!exampleText, isReviewMode: isReviewMode })] }), hasGroupContent && (_jsxs("div", { className: "flex flex-col gap-2 sm:gap-3 p-3 sm:px-6 sm:pt-0 sticky top-0 z-20 bg-white/95 backdrop-blur pb-2 shadow-sm border-b border-gray-100", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), _jsxs("div", { className: "max-h-[35vh] overflow-y-auto custom-scrollbar pr-2 flex flex-col gap-3", children: [imageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Shared illustration", className: "max-h-[200px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsxs("div", { className: "rounded-lg bg-amber-50 p-3 text-sm leading-relaxed text-gray-700 shadow-inner", children: [groupTitle && _jsx("h3", { className: "mb-2 font-semibold text-gray-800", children: groupTitle }), groupContent] }))] }), exampleText && (_jsx(CambridgeYleExampleText, { text: exampleText, highlightedWord: exampleHighlight || '' }))] })), _jsxs("div", { className: "flex flex-col gap-4 p-3 sm:gap-6 sm:pb-6 sm:px-6", children: [title && (_jsx("h2", { className: "text-center text-3xl font-bold uppercase text-coral-500", children: title })), _jsx("div", { className: "flex flex-col gap-6 sm:gap-8", children: questions.map((question) => {
|
|
16
29
|
const isExampleQ = question.isExample || false;
|
|
17
30
|
const isDisabled = isReviewMode || isExampleQ;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const blankCount = countBlanks(question.segments);
|
|
32
|
+
// Examples are pre-filled with the first accepted answer; everyone
|
|
33
|
+
// else shows what the student typed.
|
|
34
|
+
const displayValues = isExampleQ
|
|
35
|
+
? question.acceptedAnswers.map(getPrimaryAnswer)
|
|
36
|
+
: toBlankValues(answers[question.id], blankCount);
|
|
37
|
+
const blankStatuses = displayValues.map((value, index) => {
|
|
38
|
+
const hasValue = !!value.trim();
|
|
39
|
+
const matches = isBlankCorrect(value, question.acceptedAnswers[index]);
|
|
40
|
+
return {
|
|
41
|
+
hasValue,
|
|
42
|
+
isCorrect: isReviewMode && !isExampleQ && hasValue && matches,
|
|
43
|
+
isWrong: isReviewMode && !isExampleQ && hasValue && !matches,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const hasAnswer = blankStatuses.some((status) => status.hasValue);
|
|
47
|
+
const isCorrect = isReviewMode &&
|
|
48
|
+
!isExampleQ &&
|
|
49
|
+
hasAnswer &&
|
|
50
|
+
blankStatuses.every((status) => status.isCorrect);
|
|
51
|
+
const isWrong = isReviewMode && !isExampleQ && hasAnswer && !isCorrect;
|
|
52
|
+
const setBlankValue = (blankIndex, value) => {
|
|
53
|
+
const next = toBlankValues(answers[question.id], blankCount);
|
|
54
|
+
next[blankIndex] = value;
|
|
55
|
+
onAnswerChange(question.id, next);
|
|
56
|
+
};
|
|
57
|
+
const renderBlankInput = (blankIndex) => {
|
|
58
|
+
const status = blankStatuses[blankIndex] || {
|
|
59
|
+
hasValue: false,
|
|
60
|
+
isCorrect: false,
|
|
61
|
+
isWrong: false,
|
|
62
|
+
};
|
|
63
|
+
return (_jsx("span", { className: "mx-1 inline-block w-[7.5rem] max-w-[40%] min-w-[5.5rem] align-baseline sm:w-[8.5rem]", children: _jsx("input", { type: "text", value: displayValues[blankIndex] || '', onChange: (e) => setBlankValue(blankIndex, e.target.value), disabled: isDisabled, placeholder: isExampleQ ? '' : '...', className: `w-full rounded-md border-2 px-2 py-1 text-center text-sm font-semibold outline-none transition-all sm:text-base ${blankInputClassName({
|
|
64
|
+
isExampleQ,
|
|
65
|
+
isReviewMode,
|
|
66
|
+
isCorrect: status.isCorrect,
|
|
67
|
+
isWrong: status.isWrong,
|
|
68
|
+
})} ${isDisabled ? 'disabled:cursor-not-allowed disabled:opacity-75' : ''}` }) }));
|
|
69
|
+
};
|
|
24
70
|
return (_jsxs("div", { id: `question-${question.id}`, className: `flex flex-col gap-4 rounded-2xl border p-4 sm:p-6 transition-all duration-200 hover:shadow-md ${isExampleQ
|
|
25
71
|
? 'border-amber-300 bg-amber-50/60 shadow-xs'
|
|
26
72
|
: 'border-slate-200/80 bg-white shadow-xs hover:border-blue-300'}`, children: [_jsxs("div", { className: "flex items-center justify-between border-b border-slate-100 pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `inline-flex items-center justify-center rounded-full px-3.5 py-1 text-sm font-bold text-white shadow-xs ${isExampleQ
|
|
@@ -31,12 +77,10 @@ export function MoversListenAndWriteRenderer({ partNumber, partName, questionCou
|
|
|
31
77
|
: isWrong
|
|
32
78
|
? 'bg-rose-500'
|
|
33
79
|
: 'bg-[#001590]'
|
|
34
|
-
: 'bg-[#001590]'}`, children: isExampleQ ? `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
: 'border-rose-400 bg-rose-50 text-rose-800'
|
|
40
|
-
: 'border-blue-200 bg-blue-50/30 text-blue-900 focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-100'} ${isDisabled ? 'disabled:cursor-not-allowed disabled:opacity-75' : ''}` }) }), question.textAfter && (_jsx("span", { className: "font-medium", children: renderTextWithLineBreaks(question.textAfter) })), question.verbHint && (_jsxs("span", { className: "ml-1 italic text-slate-500 font-normal", children: ["(", question.verbHint, ")"] }))] }), showHints && question.hints && question.hints.length > 0 && !isReviewMode && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2 border-t border-slate-100", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "G\u1EE3i \u00FD:" }), question.hints.map((hint, idx) => (_jsx("button", { type: "button", onClick: () => onAnswerChange(question.id, hint), className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700 shadow-2xs transition-all hover:bg-blue-600 hover:text-white hover:border-blue-600 active:scale-95", children: hint }, idx)))] })), isReviewMode && !isExampleQ && question.correctAnswer && (_jsx("div", { className: "mt-1 rounded-lg bg-emerald-50/80 p-2.5 text-sm border border-emerald-200/80", children: _jsxs("span", { className: "font-semibold text-emerald-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("span", { className: "font-bold underline", children: question.correctAnswer })] }) }))] }, question.id));
|
|
80
|
+
: 'bg-[#001590]'}`, children: isExampleQ ? `Và dụ` : `${question.questionNumber}.` }), isExampleQ && (_jsx("span", { className: "rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: "Example" }))] }), isReviewMode && !isExampleQ && (_jsx("div", { children: isCorrect ? (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700 border border-emerald-200", children: "\u00E2\u0153\u201C \u00C4\u0090\u00C3\u00BAng" })) : isWrong ? (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-rose-50 px-2.5 py-0.5 text-xs font-semibold text-rose-700 border border-rose-200", children: "\u00E2\u0153\u2022 Ch\u00C6\u00B0a \u00C4\u2018\u00C3\u00BAng" })) : (_jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-600", children: "Ch\u00C6\u00B0a tr\u00E1\u00BA\u00A3 l\u00E1\u00BB\u009Di" })) }))] }), question.audioUrl && (_jsx("div", { className: "w-full my-1", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), question.imageUrl && (_jsx("div", { className: "flex items-center justify-center rounded-xl bg-slate-50/80 p-3 border border-slate-100 max-w-md mx-auto w-full", children: _jsx(ResolvedImage, { src: question.imageUrl, alt: `Question ${question.questionNumber} illustration`, className: "max-h-[200px] sm:max-h-[220px] w-auto rounded-lg object-contain drop-shadow-xs" }) })), _jsxs("div", { className: "text-base sm:text-lg leading-relaxed text-slate-800 pt-1", children: [question.segments.map((segment, segmentIndex) => segment.type === 'text' ? (_jsx("span", { className: "font-medium", children: renderTextWithLineBreaks(segment.value) }, `text-${segmentIndex}`)) : (_jsx(Fragment, { children: renderBlankInput(segment.index) }, `blank-${segment.index}-${segmentIndex}`))), question.verbHint && (_jsxs("span", { className: "ml-1 italic text-slate-500 font-normal", children: ["(", question.verbHint, ")"] }))] }), showHints && question.hints && question.hints.length > 0 && !isReviewMode && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2 border-t border-slate-100", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "G\u00E1\u00BB\u00A3i \u00C3\u00BD:" }), question.hints.map((hint, idx) => (_jsx("button", { type: "button", onClick: () => {
|
|
81
|
+
const current = toBlankValues(answers[question.id], blankCount);
|
|
82
|
+
const emptyIndex = current.findIndex((value) => !value.trim());
|
|
83
|
+
setBlankValue(emptyIndex >= 0 ? emptyIndex : 0, hint);
|
|
84
|
+
}, className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700 shadow-2xs transition-all hover:bg-blue-600 hover:text-white hover:border-blue-600 active:scale-95", children: hint }, idx)))] })), isReviewMode && !isExampleQ && question.acceptedAnswers.some((alts) => alts.length > 0) && (_jsx("div", { className: "mt-1 rounded-lg bg-emerald-50/80 p-2.5 text-sm border border-emerald-200/80", children: _jsxs("span", { className: "font-semibold text-emerald-800", children: ["\u00C4\u0090\u00C3\u00A1p \u00C3\u00A1n \u00C4\u2018\u00C3\u00BAng:", ' ', _jsx("span", { className: "font-bold underline", children: question.acceptedAnswers.map(formatAcceptedAnswers).filter(Boolean).join(' | ') })] }) }))] }, question.id));
|
|
41
85
|
}) })] })] }));
|
|
42
86
|
}
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
* Types for Movers Exam UI
|
|
3
3
|
*/
|
|
4
4
|
import type { ReactNode } from 'react';
|
|
5
|
+
import type { FillBlankSegment } from '../../../shared/lib/utils/fill-in-blank';
|
|
6
|
+
export type { FillBlankSegment };
|
|
7
|
+
/**
|
|
8
|
+
* A fill-in-the-blank stem, already split into segments. Every blank in
|
|
9
|
+
* `segments` gets its own input, and the student answer is always one string
|
|
10
|
+
* per blank, index-aligned with `acceptedAnswers`.
|
|
11
|
+
*/
|
|
5
12
|
export interface FillBlankQuestion {
|
|
6
13
|
id: string;
|
|
7
14
|
questionNumber: number;
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
segments: FillBlankSegment[];
|
|
16
|
+
/** acceptedAnswers[i] = equivalent correct answers for blank i. */
|
|
17
|
+
acceptedAnswers: string[][];
|
|
10
18
|
verbHint?: string;
|
|
11
|
-
correctAnswer?: string;
|
|
12
19
|
imageUrl?: string;
|
|
13
20
|
audioUrl?: string;
|
|
14
21
|
hints?: string[];
|
|
@@ -25,8 +32,8 @@ export interface MoversListenAndWriteRendererProps {
|
|
|
25
32
|
exampleText?: string;
|
|
26
33
|
exampleHighlight?: string;
|
|
27
34
|
questions: FillBlankQuestion[];
|
|
28
|
-
answers: Record<string,
|
|
29
|
-
onAnswerChange: (questionId: string, value: string) => void;
|
|
35
|
+
answers: Record<string, unknown>;
|
|
36
|
+
onAnswerChange: (questionId: string, value: string[]) => void;
|
|
30
37
|
isReviewMode?: boolean;
|
|
31
38
|
showHints?: boolean;
|
|
32
39
|
}
|
|
@@ -216,7 +216,7 @@ function wrapAnswer(answer, questionType) {
|
|
|
216
216
|
// For array answers (word fill with word bank), wrap in { answers: array }
|
|
217
217
|
// WORD_FILL_PARAGRAPH stores answers as array of strings
|
|
218
218
|
if (Array.isArray(answer)) {
|
|
219
|
-
if (questionType === 'WORD_FILL_PARAGRAPH') {
|
|
219
|
+
if (questionType === 'WORD_FILL_PARAGRAPH' || questionType === 'FILL_IN_BLANK') {
|
|
220
220
|
return { answers: answer };
|
|
221
221
|
}
|
|
222
222
|
// Fallback for other question types with array answers
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Maps backend question types to frontend renderer props.
|
|
5
5
|
* Each transformer handles specific question type data structure.
|
|
6
6
|
*/
|
|
7
|
+
import { countBlanks, normalizeAcceptedAnswers, parseFillBlankStem, } from '../../../../shared/lib/utils/fill-in-blank';
|
|
7
8
|
/**
|
|
8
9
|
* Group questions by part_id AND questionGroup.id
|
|
9
10
|
* This ensures questions with different passages (different questionGroups)
|
|
@@ -154,55 +155,20 @@ export function transformFillInBlank(questions) {
|
|
|
154
155
|
const content = firstQuestion?.content;
|
|
155
156
|
const groupPayload = firstQuestion?.questionGroup?.payload;
|
|
156
157
|
const transformed = [];
|
|
157
|
-
/** Take UI shows plain textBefore/textAfter — strip `**` so markers never leak. */
|
|
158
|
-
const stripStemBoldMarkers = (s) => s.replace(/\*\*/g, '');
|
|
159
158
|
for (const q of questions) {
|
|
160
159
|
const qContent = q.content;
|
|
161
|
-
const isExample = q.is_example || false;
|
|
162
160
|
// Support both: content.question (new) and content.blanks[0].text (legacy)
|
|
163
161
|
const text = qContent.question || qContent.blanks?.[0]?.text || '';
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const parts = line.split(/\{\d+\}/);
|
|
175
|
-
const textBefore = stripStemBoldMarkers(parts[0]?.trim() || '');
|
|
176
|
-
const textAfter = stripStemBoldMarkers(parts[1]?.trim() || '');
|
|
177
|
-
transformed.push({
|
|
178
|
-
id: `${q.id}-blank-${blankIndex}`,
|
|
179
|
-
questionNumber: q.question_number,
|
|
180
|
-
textBefore,
|
|
181
|
-
textAfter,
|
|
182
|
-
imageUrl: qContent.imageUrl,
|
|
183
|
-
hints: qContent.hints,
|
|
184
|
-
isExample,
|
|
185
|
-
correctAnswer: isExample ? q.correct_answer?.answers?.[blankIndex] : undefined,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
// Single placeholder or no placeholder.
|
|
191
|
-
// Use [\s\S] so stems with newlines still match (`.` does not cross `\n`).
|
|
192
|
-
const match = text.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
|
|
193
|
-
const textBefore = stripStemBoldMarkers(match ? match[1].trim() : text);
|
|
194
|
-
const textAfter = stripStemBoldMarkers(match ? match[2].trim() : '');
|
|
195
|
-
transformed.push({
|
|
196
|
-
id: q.id,
|
|
197
|
-
questionNumber: q.question_number,
|
|
198
|
-
textBefore,
|
|
199
|
-
textAfter,
|
|
200
|
-
imageUrl: qContent.imageUrl,
|
|
201
|
-
hints: qContent.hints,
|
|
202
|
-
isExample,
|
|
203
|
-
correctAnswer: isExample ? q.correct_answer?.answers?.[0] : undefined,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
162
|
+
const segments = parseFillBlankStem(text);
|
|
163
|
+
transformed.push({
|
|
164
|
+
id: q.id,
|
|
165
|
+
questionNumber: q.question_number,
|
|
166
|
+
segments,
|
|
167
|
+
acceptedAnswers: normalizeAcceptedAnswers(q.correct_answer?.answers, countBlanks(segments)),
|
|
168
|
+
imageUrl: qContent.imageUrl,
|
|
169
|
+
hints: qContent.hints,
|
|
170
|
+
isExample: q.is_example || false,
|
|
171
|
+
});
|
|
206
172
|
}
|
|
207
173
|
return {
|
|
208
174
|
questions: transformed,
|
|
@@ -228,17 +194,17 @@ export function transformWriteCorrectVerbForm(questions) {
|
|
|
228
194
|
const content = firstQuestion?.content;
|
|
229
195
|
const transformed = questions.map((q) => {
|
|
230
196
|
const qContent = q.content;
|
|
231
|
-
const
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
197
|
+
const segments = parseFillBlankStem(qContent.sentence || '');
|
|
198
|
+
// Stored as { correctAnswer: "doesn't like" }; older rows used { answers: { 'blank-1': … } }.
|
|
199
|
+
const correct = q.correct_answer;
|
|
200
|
+
const rawAccepted = Array.isArray(correct?.answers)
|
|
201
|
+
? correct.answers
|
|
202
|
+
: (correct?.answers?.['blank-1'] ?? correct?.correctAnswer ?? correct?.answer);
|
|
237
203
|
return {
|
|
238
204
|
id: q.id,
|
|
239
205
|
questionNumber: q.question_number,
|
|
240
|
-
|
|
241
|
-
|
|
206
|
+
segments,
|
|
207
|
+
acceptedAnswers: normalizeAcceptedAnswers(rawAccepted, countBlanks(segments)),
|
|
242
208
|
verbHint: qContent.verbHint, // Pass verbHint per question
|
|
243
209
|
};
|
|
244
210
|
});
|
|
@@ -3,20 +3,32 @@ export type { ValidationRef };
|
|
|
3
3
|
export interface BlankItem {
|
|
4
4
|
id: string;
|
|
5
5
|
text: string;
|
|
6
|
+
/** @deprecated Prefer correctAnswers — kept for legacy load */
|
|
6
7
|
correctAnswer: string;
|
|
8
|
+
/** Equivalent correct answers for this blank */
|
|
9
|
+
correctAnswers?: string[];
|
|
7
10
|
}
|
|
8
11
|
export interface FillInBlankGroup {
|
|
9
12
|
number: number;
|
|
10
13
|
questionCount: number;
|
|
11
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Main data interface - supports multiple blanks in a single question
|
|
17
|
+
* Question format: "He {0} 10 years old and she {1} 12."
|
|
18
|
+
* Answers format (canonical): [["is", "'s"], ["are"]]
|
|
19
|
+
* - answers[i] = list of equivalent correct answers for placeholder {i}
|
|
20
|
+
* Legacy format still accepted when loading: ["is", "are"] → [["is"], ["are"]]
|
|
21
|
+
*/
|
|
12
22
|
export interface FillInBlankData {
|
|
13
23
|
question: string;
|
|
14
|
-
answers
|
|
24
|
+
/** Per-blank list of equivalent correct answers */
|
|
25
|
+
answers: string[][];
|
|
15
26
|
caseSensitive?: boolean;
|
|
16
27
|
explanation?: string;
|
|
17
28
|
points?: number;
|
|
18
29
|
isExample?: boolean;
|
|
19
30
|
imageUrl?: string;
|
|
31
|
+
audioUrl?: string;
|
|
20
32
|
showHints?: boolean;
|
|
21
33
|
hints?: string[];
|
|
22
34
|
isGroup?: boolean;
|
|
@@ -41,9 +53,12 @@ export interface FillInBlankCreatorProps {
|
|
|
41
53
|
groups?: FillInBlankGroup[];
|
|
42
54
|
questionIndexInPart?: number;
|
|
43
55
|
partId?: string;
|
|
56
|
+
/** Template questionConfig — controls which fields are shown (e.g. "audio") */
|
|
57
|
+
questionConfig?: string[];
|
|
44
58
|
}
|
|
45
59
|
export interface FillInBlankClientProps {
|
|
46
60
|
questionData: FillInBlankData;
|
|
61
|
+
questionConfig?: string[];
|
|
47
62
|
onSubmit?: (answers: string[]) => void;
|
|
48
63
|
isReviewMode?: boolean;
|
|
49
64
|
userAnswer?: string[];
|
|
@@ -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,
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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 =
|
|
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" })] }))] })] }),
|
|
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
|
-
}),
|
|
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'
|