@tinyweb_dev/oe-exam-sdk 1.0.12 → 1.0.14
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/MoversTrueFalseRenderer.js +3 -1
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +3 -1
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.d.ts +8 -0
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.js +16 -0
- package/dist/components/questions/_shared/components/compact/KidPassage.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPassage.js +21 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +3 -1
- package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.d.ts +7 -0
- package/dist/components/questions/_shared/components/compact/SpeakingReplyMic.js +86 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.d.ts +3 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.js +11 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +14 -5
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -2
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +2 -2
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +19 -8
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -2
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +36 -56
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +2 -2
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +53 -50
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +2 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +8 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +5 -3
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +18 -15
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.d.ts +4 -1
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +4 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +17 -82
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +2 -2
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +18 -23
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +2 -8
- package/dist/components/questions/types/word-fill-structured-form/map-wfsf-question-data.js +4 -11
- package/dist/components/questions/types/word-fill-structured-form/transform.js +4 -9
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +4 -2
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.d.ts +3 -1
- package/dist/components/questions/viewer/QuestionViewer.js +37 -19
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +1 -1
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/lib/i18n/messages/en/common.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/en/common.js +39 -0
- package/dist/shared/lib/i18n/messages/en.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/vi/common.d.ts +38 -0
- package/dist/shared/lib/i18n/messages/vi/common.js +39 -0
- package/dist/shared/lib/i18n/messages/vi.d.ts +38 -0
- package/dist/shared/lib/utils/media-urls.d.ts +2 -0
- package/dist/shared/lib/utils/media-urls.js +23 -0
- package/package.json +1 -1
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Check, Lightbulb, X } from 'lucide-react';
|
|
4
|
+
import { useT } from '../../../../shared/lib/i18n';
|
|
4
5
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
6
7
|
function emptyAnswer() {
|
|
7
8
|
return { optionId: '', text: '' };
|
|
8
9
|
}
|
|
@@ -10,33 +11,13 @@ function formatHalf(points) {
|
|
|
10
11
|
const half = points * 0.5;
|
|
11
12
|
return Number.isInteger(half) ? String(half) : half.toFixed(1);
|
|
12
13
|
}
|
|
13
|
-
function badgeClass(params) {
|
|
14
|
-
if (params.isExample) {
|
|
15
|
-
return 'bg-amber-500';
|
|
16
|
-
}
|
|
17
|
-
if (!params.isReviewMode || !params.hasAttempt) {
|
|
18
|
-
return 'bg-indigo-600';
|
|
19
|
-
}
|
|
20
|
-
if (params.partialScore != null) {
|
|
21
|
-
if (params.partialScore >= params.points - 1e-9) {
|
|
22
|
-
return 'bg-emerald-500';
|
|
23
|
-
}
|
|
24
|
-
if (params.partialScore > 0) {
|
|
25
|
-
return 'bg-amber-500';
|
|
26
|
-
}
|
|
27
|
-
return 'bg-red-500';
|
|
28
|
-
}
|
|
29
|
-
if (params.mappingCorrect || params.hasText) {
|
|
30
|
-
return 'bg-amber-500';
|
|
31
|
-
}
|
|
32
|
-
return 'bg-red-500';
|
|
33
|
-
}
|
|
34
14
|
export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], itemResults, onAnswerChange, }) {
|
|
15
|
+
const t = useT();
|
|
35
16
|
const canEdit = Boolean(onAnswerChange) && !isReviewMode;
|
|
36
17
|
const items = questionData.items || [];
|
|
37
18
|
const hasAttempt = Boolean(itemResults?.length) ||
|
|
38
19
|
userAnswers.some((answer) => Boolean(answer?.optionId || answer?.text?.trim()));
|
|
39
|
-
return (_jsxs("div", { className: "space-y-
|
|
20
|
+
return (_jsxs("div", { className: "space-y-3", children: [(questionData.title || questionData.instruction) && (_jsxs(KidInstruction, { children: [questionData.title ? (_jsx("p", { className: "mb-1 text-[11px] font-semibold text-indigo-500", children: questionData.title })) : null, questionData.instruction] })), items.map((item, itemIndex) => {
|
|
40
21
|
const stored = userAnswers[itemIndex] || emptyAnswer();
|
|
41
22
|
const current = item.isExample
|
|
42
23
|
? { optionId: item.optionId, text: stored.text }
|
|
@@ -62,38 +43,37 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
62
43
|
text: next.text ?? current.text,
|
|
63
44
|
});
|
|
64
45
|
};
|
|
65
|
-
return (_jsx(KidQuestionCard, { title:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
: 'bg-red-50 text-red-700'), children: ["Vi\u1EBFt ", result.aiCorrect ? halfLabel : '0'] })) : null] })) : null, canEdit || hasStudentText || (isReviewMode && hasAttempt) ? (_jsxs("label", { className: "block space-y-1", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "C\u00E2u tr\u1EA3 l\u1EDDi c\u1EE7a b\u1EA1n" }), isReviewMode && !hasStudentText ? (_jsx("p", { className: "rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-2 text-sm italic text-slate-400", children: "Ch\u01B0a vi\u1EBFt" })) : (_jsx("textarea", { value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: "Vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y...", rows: 2, className: "min-h-0 w-full resize-none rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed text-slate-800 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 disabled:bg-slate-50" }))] })) : null, isReviewMode && sampleAnswers.length > 0 ? (_jsxs("p", { className: "text-xs text-slate-500", children: [_jsx("span", { className: "font-semibold", children: "M\u1EABu:" }), " ", sampleAnswers.join(' · ')] })) : null] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
46
|
+
return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, t('question.viewer.questionN', { n: item.questionNumber || itemIndex + 1 })), points: reviewItemPoints(isReviewMode, points), isExample: item.isExample, children: _jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "min-w-0 space-y-2.5", children: [(parts[0] || parts.slice(1).join('{0}')) ? (_jsxs("p", { className: "text-sm font-semibold leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "mx-1 inline-block min-w-[3rem] border-b-2 border-dashed border-slate-300 align-baseline" }), _jsx("span", { children: parts.slice(1).join('{0}') })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-2.5", role: "group", "aria-label": t('question.viewer.questionN', {
|
|
47
|
+
n: item.questionNumber || itemIndex + 1,
|
|
48
|
+
}), children: item.options.map((option) => {
|
|
49
|
+
const selected = current.optionId === option.id;
|
|
50
|
+
const isKey = option.id === expectedId;
|
|
51
|
+
const pickedCorrect = showGradedReview && selected && isKey;
|
|
52
|
+
const pickedWrong = showGradedReview && selected && !isKey;
|
|
53
|
+
const keyOnly = showGradedReview && !selected && isKey;
|
|
54
|
+
const officialCorrect = showAnswerKey && isKey;
|
|
55
|
+
const optionId = `cta-${itemIndex}-opt-${option.id}`;
|
|
56
|
+
return (_jsxs("button", { id: optionId, name: `cta-${itemIndex}-option`, type: "button", disabled: !canEdit || item.isExample, onClick: () => setAnswer({ optionId: option.id }), className: cn('inline-flex min-w-[3.5rem] items-center justify-center gap-1 rounded-full border px-3 py-1.5 text-sm font-semibold transition-colors', !showReview &&
|
|
57
|
+
selected &&
|
|
58
|
+
'border-blue-500 bg-blue-500 text-white shadow-sm ring-2 ring-blue-100', !showReview &&
|
|
59
|
+
!selected &&
|
|
60
|
+
'border-gray-200 bg-white text-slate-800 hover:border-blue-300 hover:bg-blue-50', showAnswerKey &&
|
|
61
|
+
!isKey &&
|
|
62
|
+
'border-slate-200 bg-white text-slate-800', officialCorrect &&
|
|
63
|
+
'border-2 border-emerald-500 bg-white text-slate-800', pickedCorrect && 'border-emerald-600 bg-emerald-600 text-white', pickedWrong && 'border-red-500 bg-red-500 text-white', keyOnly && 'border-emerald-500 bg-white text-emerald-800', showGradedReview &&
|
|
64
|
+
!selected &&
|
|
65
|
+
!isKey &&
|
|
66
|
+
'border-slate-200 bg-slate-50 text-slate-400', canEdit &&
|
|
67
|
+
!item.isExample &&
|
|
68
|
+
'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-200', (!canEdit || item.isExample) && 'cursor-default'), children: [option.text || option.id, pickedCorrect ? _jsx(Check, { className: "h-3 w-3" }) : null, pickedWrong ? _jsx(X, { className: "h-3 w-3" }) : null] }, option.id));
|
|
69
|
+
}) }), showReview && hasAttempt ? (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', mappingCorrect
|
|
70
|
+
? 'bg-emerald-50 text-emerald-700'
|
|
71
|
+
: 'bg-red-50 text-red-700'), children: t('question.viewer.chooseScore', {
|
|
72
|
+
score: mappingCorrect ? halfLabel : '0',
|
|
73
|
+
}) }), result?.aiCorrect !== undefined ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', result.aiCorrect
|
|
74
|
+
? 'bg-emerald-50 text-emerald-700'
|
|
75
|
+
: 'bg-red-50 text-red-700'), children: t('question.viewer.writeScore', {
|
|
76
|
+
score: result.aiCorrect ? halfLabel : '0',
|
|
77
|
+
}) })) : null] })) : null, canEdit || hasStudentText || (isReviewMode && hasAttempt) ? (_jsxs("label", { className: "block space-y-1", htmlFor: `cta-${itemIndex}-text`, children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: t('question.viewer.yourAnswer') }), isReviewMode && !hasStudentText ? (_jsx("p", { className: "rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-2 text-sm italic text-slate-400", children: t('question.viewer.notWritten') })) : (_jsx("textarea", { id: `cta-${itemIndex}-text`, name: `cta-${itemIndex}-text`, value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: t('question.viewer.writePlaceholder'), rows: 2, className: "min-h-0 w-full resize-none rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed text-slate-800 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 disabled:bg-slate-50" }))] })) : null, isReviewMode && sampleAnswers.length > 0 ? (_jsxs("p", { className: "text-xs text-slate-500", children: [_jsxs("span", { className: "font-semibold", children: [t('question.viewer.sample'), ":"] }), ' ', sampleAnswers.join(' · ')] })) : null] }) }) }, `${item.questionNumber}-${itemIndex}`));
|
|
98
78
|
}), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
|
99
79
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
6
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
7
7
|
function normalizeWord(word) {
|
|
8
8
|
return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
|
|
9
9
|
}
|
|
@@ -48,7 +48,7 @@ export function CrossOutWordGroupClient({ questionData, isReviewMode = false, us
|
|
|
48
48
|
const selectedWord = userValue?.crossOut || '';
|
|
49
49
|
const replaceWith = userValue?.replaceWith || '';
|
|
50
50
|
const correct = item.correctAnswer;
|
|
51
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
51
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, headerExtra: isReviewMode ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
|
|
52
52
|
? 'bg-violet-100 text-violet-700'
|
|
53
53
|
: 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })) : null, children: [isReplace ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-semibold text-slate-600", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: !canSelect, onChange: (event) => emit(itemIndex, {
|
|
54
54
|
type: item.type,
|
|
@@ -32,7 +32,9 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
32
32
|
}
|
|
33
33
|
return { cellMap: cMap, clueNumberMap: numMap };
|
|
34
34
|
}, [acrossClues, downClues]);
|
|
35
|
-
return (_jsxs("div", { className: "flex flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [
|
|
35
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [!isReviewMode ? (_jsxs("span", { className: "font-bold", children: [displayNumber, "."] })) : null, instructionText
|
|
36
|
+
? `${!isReviewMode ? ' ' : ''}${instructionText}`
|
|
37
|
+
: ''] }), _jsxs("div", { className: "flex w-full min-w-0 flex-col gap-4", children: [_jsxs("div", { className: "grid min-w-0 grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-semibold", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: acrossClues.map((c) => {
|
|
36
38
|
const answer = correctAnswer?.answers?.[`across-${c.number}`];
|
|
37
39
|
const userAns = userAnswers[`across-${c.number}`];
|
|
38
40
|
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-blue-100", children: [_jsxs("span", { className: "font-bold text-blue-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`across-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-blue-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-blue-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `across-${c.number}`));
|
|
@@ -40,57 +42,58 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
40
42
|
const answer = correctAnswer?.answers?.[`down-${c.number}`];
|
|
41
43
|
const userAns = userAnswers[`down-${c.number}`];
|
|
42
44
|
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-purple-100", children: [_jsxs("span", { className: "font-bold text-purple-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`down-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-purple-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-purple-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `down-${c.number}`));
|
|
43
|
-
}) })] })] }), _jsx("div", { className: "
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const isMarked = content?.markedColumn && c === content.markedColumn;
|
|
56
|
-
const isExample = Boolean(cellInfo.across?.isExample || cellInfo.down?.isExample);
|
|
57
|
-
// If in review mode or cell is an example, display correct answer letter
|
|
58
|
-
let previewLetter = '';
|
|
59
|
-
if (isReviewMode || isExample) {
|
|
60
|
-
if (cellInfo.across && correctAnswer?.answers) {
|
|
61
|
-
const word = correctAnswer.answers[`across-${cellInfo.across.number}`] || '';
|
|
62
|
-
const offset = c - cellInfo.across.startCol;
|
|
63
|
-
previewLetter = word[offset] || '';
|
|
45
|
+
}) })] })] }), _jsx("div", { className: "w-full min-w-0 overflow-x-auto rounded-xl border border-gray-200 bg-gray-50", children: _jsx("div", { className: "flex w-max min-w-full justify-center p-4", children: _jsx("div", { className: "inline-grid shrink-0 gap-1 [--cw-cell:2rem] sm:[--cw-cell:2.25rem] md:[--cw-cell:2.5rem]", style: {
|
|
46
|
+
gridTemplateColumns: `repeat(${cols}, var(--cw-cell))`,
|
|
47
|
+
gridAutoRows: 'var(--cw-cell)',
|
|
48
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
49
|
+
const r = rIdx + 1;
|
|
50
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
51
|
+
const c = cIdx + 1;
|
|
52
|
+
const key = `${r}-${c}`;
|
|
53
|
+
const cellInfo = cellMap.get(key);
|
|
54
|
+
const clueNum = clueNumberMap.get(key);
|
|
55
|
+
if (!cellInfo) {
|
|
56
|
+
return (_jsx("div", { className: "size-[var(--cw-cell)] bg-transparent" }, key));
|
|
64
57
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
const isMarked = content?.markedColumn && c === content.markedColumn;
|
|
59
|
+
const isExample = Boolean(cellInfo.across?.isExample || cellInfo.down?.isExample);
|
|
60
|
+
// If in review mode or cell is an example, display correct answer letter
|
|
61
|
+
let previewLetter = '';
|
|
62
|
+
if (isReviewMode || isExample) {
|
|
63
|
+
if (cellInfo.across && correctAnswer?.answers) {
|
|
64
|
+
const word = correctAnswer.answers[`across-${cellInfo.across.number}`] || '';
|
|
65
|
+
const offset = c - cellInfo.across.startCol;
|
|
66
|
+
previewLetter = word[offset] || '';
|
|
67
|
+
}
|
|
68
|
+
else if (cellInfo.down && correctAnswer?.answers) {
|
|
69
|
+
const word = correctAnswer.answers[`down-${cellInfo.down.number}`] || '';
|
|
70
|
+
const offset = r - cellInfo.down.startRow;
|
|
71
|
+
previewLetter = word[offset] || '';
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
else {
|
|
75
|
+
if (cellInfo.across) {
|
|
76
|
+
const word = userAnswers[`across-${cellInfo.across.number}`] || '';
|
|
77
|
+
previewLetter = word[c - cellInfo.across.startCol] || '';
|
|
78
|
+
}
|
|
79
|
+
if (!previewLetter && cellInfo.down) {
|
|
80
|
+
const word = userAnswers[`down-${cellInfo.down.number}`] || '';
|
|
81
|
+
previewLetter = word[r - cellInfo.down.startRow] || '';
|
|
82
|
+
}
|
|
83
|
+
if (!previewLetter) {
|
|
84
|
+
previewLetter = userAnswers[key] || '';
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
|
|
88
|
+
let textColor = isExample
|
|
89
|
+
? 'text-orange-600 font-bold italic'
|
|
90
|
+
: isMarked
|
|
91
|
+
? 'text-gray-950 font-bold'
|
|
92
|
+
: 'text-gray-800 font-bold';
|
|
93
|
+
if (isReviewMode && previewLetter && !isExample) {
|
|
94
|
+
textColor = 'text-green-700 font-extrabold';
|
|
79
95
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
|
|
85
|
-
let textColor = isExample
|
|
86
|
-
? 'text-orange-600 font-bold italic'
|
|
87
|
-
: isMarked
|
|
88
|
-
? 'text-gray-950 font-bold'
|
|
89
|
-
: 'text-gray-800 font-bold';
|
|
90
|
-
if (isReviewMode && previewLetter && !isExample) {
|
|
91
|
-
textColor = 'text-green-700 font-extrabold';
|
|
92
|
-
}
|
|
93
|
-
return (_jsxs("div", { className: `relative flex items-center justify-center rounded border-2 font-bold shadow-sm h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 text-sm sm:text-base md:text-lg ${cellBg} ${textColor}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
|
|
94
|
-
});
|
|
95
|
-
}) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
|
|
96
|
+
return (_jsxs("div", { className: `relative flex size-[var(--cw-cell)] items-center justify-center rounded border-2 font-bold shadow-sm text-sm sm:text-base md:text-lg ${cellBg} ${textColor}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
|
|
97
|
+
});
|
|
98
|
+
}) }) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
|
|
96
99
|
}
|
|
@@ -9,7 +9,7 @@ import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAn
|
|
|
9
9
|
import { parseFillBlankSegments } from '../../../../shared/lib/utils/fill-in-blank';
|
|
10
10
|
import { cn } from '../../../../shared/lib/utils';
|
|
11
11
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
12
|
-
import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
12
|
+
import { KidPrompt, KidQuestionCard, reviewItemTitle } from '../../_shared/components/compact';
|
|
13
13
|
// Sub-component: Resolve presigned URL for group image
|
|
14
14
|
function GroupImage({ src, alt, className }) {
|
|
15
15
|
const { previewUrl, isLoading } = usePresignedFileUrl(src);
|
|
@@ -228,7 +228,7 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
|
|
|
228
228
|
const userInput = answers[index] || '';
|
|
229
229
|
const isCorrect = isReviewMode && isAnswerCorrect(index, userInput);
|
|
230
230
|
const hasAnswered = userInput.trim() !== '';
|
|
231
|
-
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${index + 1}: Điền vào chỗ trống
|
|
231
|
+
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(index + 1, isReviewMode, `Câu ${index + 1}: Điền vào chỗ trống`), review: isReviewMode && hasAnswered ? (isCorrect ? 'correct' : 'wrong') : null, 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(KidPrompt, { 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
232
|
? 'border-emerald-200 bg-emerald-50/50'
|
|
233
233
|
: hasAnswered
|
|
234
234
|
? 'border-red-200 bg-red-50/50'
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment } from 'react';
|
|
2
|
+
import { Fragment, useState } from 'react';
|
|
3
|
+
import { useT } from '../../../../shared/lib/i18n';
|
|
3
4
|
import { acceptedAnswerList } from './blank-utils';
|
|
4
5
|
function AnswerOrList({ answers }) {
|
|
6
|
+
const t = useT();
|
|
7
|
+
const [expanded, setExpanded] = useState(false);
|
|
5
8
|
if (answers.length === 0) {
|
|
6
9
|
return _jsx("span", { className: "font-semibold text-green-900", children: "(tr\u1ED1ng)" });
|
|
7
10
|
}
|
|
8
|
-
|
|
11
|
+
const visible = expanded ? answers : answers.slice(0, 1);
|
|
12
|
+
return (_jsxs(_Fragment, { children: [visible.map((answer, index) => (_jsxs(Fragment, { children: [index > 0 && (_jsx("span", { className: "mx-1 text-xs font-normal italic text-green-600", children: "ho\u1EB7c" })), _jsx("span", { className: "font-semibold text-green-900", children: answer })] }, `${answer}-${index}`))), answers.length > 1 ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "ml-1 text-xs font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
|
|
13
|
+
? t('question.viewer.collapsePassage')
|
|
14
|
+
: t('question.viewer.expandPassage') })) : null] }));
|
|
9
15
|
}
|
|
10
16
|
export function FillInBlankAnswerKey({ answersByBlank, blankIndices, }) {
|
|
11
17
|
const rows = blankIndices.map((blankIndex, order) => ({
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { BookOpen, Lightbulb, PenLine, Volume2 } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { COLLAPSE_CHAR_THRESHOLD, KidCollapsibleBlock, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
7
7
|
import { FillInBlankAnswerKey } from './FillInBlankAnswerKey';
|
|
8
8
|
import { blankSlotCount, extractPlaceholderIndices, isBlankMatch, padStudentAnswers, resolveBlankOptions, splitStem, splitStemTextDecorations, toFillInBlankImageUrls, } from './blank-utils';
|
|
9
9
|
const STEM_MARK_STYLE = {
|
|
@@ -66,7 +66,9 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
|
|
|
66
66
|
next[itemIndex] = { answers: current };
|
|
67
67
|
onAnswerChange(next);
|
|
68
68
|
};
|
|
69
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), questionData.passage && (
|
|
69
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), questionData.passage && (_jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
|
|
70
|
+
questionData.passage.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().length >
|
|
71
|
+
COLLAPSE_CHAR_THRESHOLD, children: _jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] }) })), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toFillInBlankImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Group image ${imageIndex + 1}` }, `group-image-${imageIndex}`))) })), groupHints.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: groupHints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3", children: items.map((item, itemIndex) => {
|
|
70
72
|
const blankCount = blankSlotCount(item.question || '{0}');
|
|
71
73
|
const placeholderIndices = extractPlaceholderIndices(item.question || '{0}');
|
|
72
74
|
const userValue = padStudentAnswers(userAnswers[itemIndex], blankCount);
|
|
@@ -74,7 +76,7 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
|
|
|
74
76
|
const showItemAudio = Boolean(item.audioUrl) && !questionData.audioUrl;
|
|
75
77
|
const hasAnyAnswer = userValue.some((value) => value.trim().length > 0);
|
|
76
78
|
const allBlanksCorrect = placeholderIndices.every((index) => isBlankMatch(userValue[index] || '', item.answers[index] || []));
|
|
77
|
-
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
79
|
+
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnyAnswer ? (allBlanksCorrect ? 'correct' : 'wrong') : null, children: [toFillInBlankImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-2", children: toFillInBlankImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Item ${item.questionNumber} image ${imageIndex + 1}` }, `${item.questionNumber}-${imageIndex}`))) })), showItemAudio && item.audioUrl && (_jsx(MediaPreview, { src: item.audioUrl, kind: "audio", alt: `Item ${item.questionNumber} audio` })), _jsx(KidPrompt, { children: _jsx("div", { className: "min-w-0 text-sm leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
|
|
78
80
|
if (part.type === 'text') {
|
|
79
81
|
return (_jsx("span", { children: _jsx(StemText, { value: part.value }) }, partIndex));
|
|
80
82
|
}
|
|
@@ -193,19 +193,22 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
|
|
|
193
193
|
: 'bấm ô cuối để hoàn thành từ.'] })) : (_jsx(_Fragment, { children: "Ch\u1ECDn m\u1ED9t t\u1EEB trong danh s\u00E1ch, r\u1ED3i b\u1EA5m t\u1EEBng ch\u1EEF tr\u00EAn l\u01B0\u1EDBi (ho\u1EB7c \u00F4 \u0111\u1EA7u r\u1ED3i \u00F4 cu\u1ED1i)." })) })) : null, isCategorize ? (_jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3", children: categories.map((cat) => {
|
|
194
194
|
const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
|
|
195
195
|
return (_jsxs(Card, { className: "border bg-gray-50/70 p-3.5", children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between border-b pb-1.5 text-sm font-bold", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((word, index) => renderWordBadge(word, index)) })] }, cat.id));
|
|
196
|
-
}) })) : (_jsxs(Card, { className: "border-2 border-dashed bg-gray-50 p-4", children: [_jsxs("div", { className: "mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500", children: ["Word Bank (", wordBank.length, " words)"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => renderWordBadge(word, index)) })] })), _jsx("div", { className: "
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
196
|
+
}) })) : (_jsxs(Card, { className: "border-2 border-dashed bg-gray-50 p-4", children: [_jsxs("div", { className: "mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500", children: ["Word Bank (", wordBank.length, " words)"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => renderWordBadge(word, index)) })] })), _jsx("div", { className: "w-full min-w-0 overflow-x-auto pb-2", children: _jsx("div", { className: "flex w-max min-w-full justify-center", children: _jsx("div", { className: "inline-grid shrink-0 gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm [--fw-cell:2.25rem] sm:[--fw-cell:2.75rem]", style: {
|
|
197
|
+
gridTemplateColumns: `repeat(${cols}, var(--fw-cell))`,
|
|
198
|
+
gridAutoRows: 'var(--fw-cell)',
|
|
199
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
200
|
+
const r = rIdx + 1;
|
|
201
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
202
|
+
const c = cIdx + 1;
|
|
203
|
+
const char = grid[rIdx]?.[cIdx] || '';
|
|
204
|
+
const highlight = cellColorMap[`${r}-${c}`];
|
|
205
|
+
const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
|
|
206
|
+
const isTapped = tappedPath.some((cell) => cell.row === r && cell.col === c);
|
|
207
|
+
return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex size-[var(--fw-cell)] items-center justify-center rounded-md border text-sm font-bold sm:text-base ${colorConfig
|
|
208
|
+
? `${colorConfig.bg} shadow-sm`
|
|
209
|
+
: isTapped
|
|
210
|
+
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
211
|
+
: 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
|
|
212
|
+
});
|
|
213
|
+
}) }) }) })] }));
|
|
211
214
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { assembleMatchColumnsSentence, getMatchColumnsChoiceColumns, getMatchColumnsStemColumn, } from './map-match-columns-to-make-sentences-data';
|
|
6
|
-
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, } from '../../_shared/components/compact';
|
|
6
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
7
7
|
function emptyAnswer(stemFragmentId) {
|
|
8
8
|
return { fragmentIds: stemFragmentId ? [stemFragmentId] : [], sentence: '' };
|
|
9
9
|
}
|
|
@@ -59,7 +59,7 @@ export function MatchColumnsToMakeSentencesClient({ questionData, isReviewMode =
|
|
|
59
59
|
|| '—';
|
|
60
60
|
const isCorrect = isReviewMode &&
|
|
61
61
|
current.sentence.trim().toLowerCase() === item.sentence.trim().toLowerCase();
|
|
62
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
62
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: [_jsx(KidPrompt, { children: stemText }), choiceColumns.map((column, choiceIndex) => {
|
|
63
63
|
const selectedId = current.fragmentIds[choiceIndex + 1] || '';
|
|
64
64
|
const correctId = item.fragmentIds[choiceIndex + 1] || '';
|
|
65
65
|
return (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wider text-gray-500", children: column.label || column.id }), column.fragments.map((fragment, fragmentIndex) => {
|
package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import { KidChoiceRow, KidInstruction, KidQuestionCard, optionLetter, } from '../../_shared/components/compact';
|
|
6
|
+
import { KidChoiceRow, KidInstruction, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
7
7
|
function normalizeWord(value) {
|
|
8
8
|
return value.trim().toLowerCase();
|
|
9
9
|
}
|
|
@@ -53,7 +53,7 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
53
53
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
54
54
|
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
55
55
|
Boolean(item.correctAnswer);
|
|
56
|
-
return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
56
|
+
return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [_jsx(ClientImageItem, { url: item.imageUrl, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}` }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), wordBank.map((word, wordIndex) => {
|
|
57
57
|
const isSelected = normalizeWord(word) === normalizeWord(selectedWord);
|
|
58
58
|
const isUsedByOther = usedWords.has(normalizeWord(word)) && !isSelected;
|
|
59
59
|
const isWordCorrect = normalizeWord(word) === normalizeWord(item.correctAnswer);
|
package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js
CHANGED
|
@@ -4,7 +4,7 @@ import { BookOpen, Check, ChevronDown, Lightbulb, Volume2, X } from 'lucide-reac
|
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { toMatchWordToPictureImageUrls, toMatchWordToPictureStorageKey, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
7
|
-
import { KidInstruction, KidPrompt, KidQuestionCard, } from '../../_shared/components/compact';
|
|
7
|
+
import { KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
8
8
|
function normalizeWord(value) {
|
|
9
9
|
return value.trim().toLowerCase();
|
|
10
10
|
}
|
|
@@ -76,7 +76,7 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
76
76
|
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
77
77
|
Boolean(item.correctAnswer);
|
|
78
78
|
const itemImages = toMatchWordToPictureImageUrls(item.imageUrl);
|
|
79
|
-
return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
79
|
+
return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [itemImages.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: itemImages.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(WordDropdown, { value: selectedWord, wordBank: wordBank, usedWords: usedWords, disabled: !canSelect, onChange: (value) => handleSelect(itemIndex, value) }), isReviewMode && (isCorrect
|
|
80
80
|
? _jsx(Check, { className: "h-5 w-5 text-emerald-600" })
|
|
81
81
|
: _jsx(X, { className: "h-5 w-5 text-red-500" }))] })), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
82
82
|
}), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
|
@@ -6,6 +6,9 @@ export interface SpeakingCueCardClientProps {
|
|
|
6
6
|
explanation?: string;
|
|
7
7
|
};
|
|
8
8
|
isReviewMode?: boolean;
|
|
9
|
+
audioKey?: string | null;
|
|
10
|
+
uploading?: boolean;
|
|
11
|
+
onRecorded?: (blob: Blob) => void;
|
|
9
12
|
}
|
|
10
13
|
export declare function mapQuestionToSpeakingCueCardData(question: Record<string, unknown> | null | undefined): SpeakingCueCardClientProps['questionData'];
|
|
11
|
-
export declare function SpeakingCueCardClient({ questionData, isReviewMode, }: SpeakingCueCardClientProps): import("react").JSX.Element;
|
|
14
|
+
export declare function SpeakingCueCardClient({ questionData, isReviewMode, audioKey, uploading, onRecorded, }: SpeakingCueCardClientProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { CheckCircle2, Info, CreditCard } from 'lucide-react';
|
|
4
4
|
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
5
|
-
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
5
|
+
import { KidQuestionCard, SpeakingReplyMic } from '../../_shared/components/compact';
|
|
6
6
|
function asRecord(value) {
|
|
7
7
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
8
8
|
? value
|
|
@@ -22,7 +22,7 @@ export function mapQuestionToSpeakingCueCardData(question) {
|
|
|
22
22
|
explanation: typeof source.explanation === 'string' ? source.explanation : '',
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
export function SpeakingCueCardClient({ questionData, isReviewMode = false, }) {
|
|
25
|
+
export function SpeakingCueCardClient({ questionData, isReviewMode = false, audioKey = null, uploading = false, onRecorded, }) {
|
|
26
26
|
const answers = questionData.expectedAnswers || [];
|
|
27
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)", icon: _jsx(CreditCard, { className: "h-3.5 w-3.5" }), children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }),
|
|
27
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)", icon: _jsx(CreditCard, { className: "h-3.5 w-3.5" }), children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }), isReviewMode && !onRecorded ? (audioKey ? (_jsx(SpeakingReplyMic, { flattenIndex: 0, audioKey: audioKey, disabled: true })) : (_jsx("p", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 b\u1EA3n ghi." }))) : (_jsx(SpeakingReplyMic, { flattenIndex: 0, audioKey: audioKey, uploading: uploading, onRecorded: onRecorded ? (_index, blob) => onRecorded(blob) : undefined })), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] }));
|
|
28
28
|
}
|