@tinyweb_dev/oe-exam-sdk 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +18 -20
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
- package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
- package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
- package/dist/components/questions/viewer/QuestionViewer.js +22 -9
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
- package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +8 -0
- package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
- package/dist/shared/types/questions/answer-the-question-group.js +5 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
- package/package.json +1 -1
|
@@ -28,6 +28,16 @@ export function createEmptyOption(existingIds) {
|
|
|
28
28
|
text: '',
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
export function normalizeExpectedAnswers(value) {
|
|
32
|
+
if (!Array.isArray(value)) {
|
|
33
|
+
return [''];
|
|
34
|
+
}
|
|
35
|
+
const filled = value
|
|
36
|
+
.filter((item) => typeof item === 'string')
|
|
37
|
+
.map((item) => item.trim())
|
|
38
|
+
.filter((item) => item !== '');
|
|
39
|
+
return filled.length > 0 ? filled : [''];
|
|
40
|
+
}
|
|
31
41
|
export function createEmptyItem(questionNumber) {
|
|
32
42
|
const first = createEmptyOption([]);
|
|
33
43
|
const second = createEmptyOption([first.id]);
|
|
@@ -35,6 +45,7 @@ export function createEmptyItem(questionNumber) {
|
|
|
35
45
|
question: '',
|
|
36
46
|
options: [first, second],
|
|
37
47
|
optionId: first.id,
|
|
48
|
+
expectedAnswers: [''],
|
|
38
49
|
isExample: false,
|
|
39
50
|
points: 1,
|
|
40
51
|
questionNumber,
|
|
@@ -42,8 +53,11 @@ export function createEmptyItem(questionNumber) {
|
|
|
42
53
|
}
|
|
43
54
|
export function parseItemAnswer(raw) {
|
|
44
55
|
if (!isRecord(raw))
|
|
45
|
-
return { optionId: '' };
|
|
46
|
-
return {
|
|
56
|
+
return { optionId: '', expectedAnswers: [''] };
|
|
57
|
+
return {
|
|
58
|
+
optionId: asString(raw.optionId),
|
|
59
|
+
expectedAnswers: normalizeExpectedAnswers(raw.expectedAnswers),
|
|
60
|
+
};
|
|
47
61
|
}
|
|
48
62
|
export function parseStudentAnswer(raw) {
|
|
49
63
|
if (!isRecord(raw))
|
|
@@ -78,6 +92,7 @@ export function mapApiItem(item, index, fallbackAnswer) {
|
|
|
78
92
|
question: asString(content.question) || asString(record.question),
|
|
79
93
|
options,
|
|
80
94
|
optionId: parsed.optionId || options[0]?.id || '',
|
|
95
|
+
expectedAnswers: parsed.expectedAnswers,
|
|
81
96
|
isExample,
|
|
82
97
|
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
83
98
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
@@ -125,3 +140,25 @@ export function mapQuestionToChooseThenAnswerGroupData(question) {
|
|
|
125
140
|
points: 1,
|
|
126
141
|
};
|
|
127
142
|
}
|
|
143
|
+
export function toCtaItemResults(details, items, userAnswers) {
|
|
144
|
+
if (!details?.length) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const byId = new Map(details.map((detail) => [detail.itemId, detail]));
|
|
148
|
+
return items.map((item, index) => {
|
|
149
|
+
const detail = byId.get(`item-${index}`);
|
|
150
|
+
const mappingCorrect = Boolean(item.optionId) && userAnswers[index]?.optionId === item.optionId;
|
|
151
|
+
const half = (typeof item.points === 'number' ? item.points : 1) * 0.5;
|
|
152
|
+
let aiCorrect;
|
|
153
|
+
if (detail?.partialScore != null) {
|
|
154
|
+
const mappingHalf = mappingCorrect ? half : 0;
|
|
155
|
+
aiCorrect = detail.partialScore - mappingHalf > 1e-9;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
mappingCorrect,
|
|
159
|
+
aiCorrect,
|
|
160
|
+
partialScore: detail?.partialScore,
|
|
161
|
+
feedback: detail?.feedback,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION, CHOOSE_THEN_ANSWER_ITEM_TYPE, } from '../../_shared/types/choose-then-answer-group.type';
|
|
2
2
|
import { sumGradablePoints } from './map-choose-then-answer-group-data';
|
|
3
|
+
function filledExpectedAnswers(values) {
|
|
4
|
+
return (values || []).map((value) => value.trim()).filter((value) => value !== '');
|
|
5
|
+
}
|
|
3
6
|
export const transformChooseThenAnswerGroup = (question) => {
|
|
4
7
|
const answerData = question.answer;
|
|
5
8
|
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
@@ -7,25 +10,35 @@ export const transformChooseThenAnswerGroup = (question) => {
|
|
|
7
10
|
return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
|
|
8
11
|
}
|
|
9
12
|
const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
|
|
10
|
-
const apiItems = items.map((item, index) =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
const apiItems = items.map((item, index) => {
|
|
14
|
+
const expectedAnswers = filledExpectedAnswers(item.expectedAnswers);
|
|
15
|
+
return {
|
|
16
|
+
questionType: CHOOSE_THEN_ANSWER_ITEM_TYPE,
|
|
17
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
18
|
+
content: {
|
|
19
|
+
question: item.question || '',
|
|
20
|
+
options: (item.options || []).map((option) => ({
|
|
21
|
+
id: option.id || '',
|
|
22
|
+
text: option.text || '',
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
correctAnswer: {
|
|
26
|
+
answer: {
|
|
27
|
+
optionId: item.optionId || '',
|
|
28
|
+
...(expectedAnswers.length > 0 ? { expectedAnswers } : {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
32
|
+
questionNumber: item.questionNumber || index + 1,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
24
35
|
return {
|
|
25
36
|
apiContent: {
|
|
26
37
|
meta: {
|
|
27
38
|
instruction: answerData?.instruction || CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION,
|
|
28
39
|
...(title ? { title } : {}),
|
|
40
|
+
gradingType: 'AI',
|
|
41
|
+
isAiGraded: true,
|
|
29
42
|
},
|
|
30
43
|
items: apiItems,
|
|
31
44
|
},
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CrossOutWordGroupClientProps } from '../../_shared/types/cross-out-word-group.type';
|
|
2
|
-
export declare function CrossOutWordGroupClient({ questionData, isReviewMode, userAnswers, }: CrossOutWordGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function CrossOutWordGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: CrossOutWordGroupClientProps): import("react").JSX.Element;
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { CircleHelp, Lightbulb, Star } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
+
function tokenize(sentence) {
|
|
6
|
+
return sentence.trim().split(/\s+/).filter(Boolean);
|
|
7
|
+
}
|
|
8
|
+
function normalizeWord(word) {
|
|
9
|
+
return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
|
|
10
|
+
}
|
|
5
11
|
function formatAnswer(answer) {
|
|
6
12
|
if (!answer)
|
|
7
13
|
return '—';
|
|
@@ -15,13 +21,58 @@ function formatAnswer(answer) {
|
|
|
15
21
|
return 'câu đúng (không gạch)';
|
|
16
22
|
return `gạch "${answer.crossOut}"${posInfo}`;
|
|
17
23
|
}
|
|
18
|
-
export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
|
|
24
|
+
export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
19
25
|
const items = questionData.items || [];
|
|
26
|
+
const emit = (itemIndex, nextItem) => {
|
|
27
|
+
const item = items[itemIndex];
|
|
28
|
+
if (isReviewMode || item?.isExample || !onAnswerChange) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const next = items.map((_, index) => userAnswers[index] ?? null);
|
|
32
|
+
next[itemIndex] = nextItem;
|
|
33
|
+
onAnswerChange(next);
|
|
34
|
+
};
|
|
20
35
|
return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => {
|
|
21
|
-
const userValue = userAnswers[itemIndex];
|
|
22
36
|
const isReplace = item.type === 'CROSS_OUT_AND_REPLACE';
|
|
37
|
+
const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
|
|
38
|
+
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
39
|
+
const tokens = tokenize(item.question || '');
|
|
40
|
+
const selectedIndex = userValue?.wordIndex;
|
|
41
|
+
const selectedWord = userValue?.crossOut || '';
|
|
42
|
+
const replaceWith = userValue?.replaceWith || '';
|
|
43
|
+
const correct = item.correctAnswer;
|
|
23
44
|
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] })), _jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
|
|
24
45
|
? 'bg-violet-100 text-violet-700'
|
|
25
|
-
: 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [
|
|
46
|
+
: 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", 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, {
|
|
47
|
+
type: item.type,
|
|
48
|
+
wordIndex: selectedIndex ?? null,
|
|
49
|
+
crossOut: selectedWord,
|
|
50
|
+
replaceWith: event.target.value,
|
|
51
|
+
}), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 min-w-[10rem] rounded-lg border border-slate-200 px-2.5 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 disabled:bg-slate-50" })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-sm font-medium leading-relaxed text-slate-800", children: tokens.map((token, tokenIndex) => {
|
|
52
|
+
const plain = normalizeWord(token);
|
|
53
|
+
const isSelected = typeof selectedIndex === 'number'
|
|
54
|
+
? selectedIndex === tokenIndex
|
|
55
|
+
: Boolean(selectedWord) && normalizeWord(selectedWord) === plain && plain !== '';
|
|
56
|
+
const isCorrectTarget = isReviewMode && !item.isExample && correct?.wordIndex === tokenIndex;
|
|
57
|
+
return (_jsx("button", { type: "button", disabled: !canSelect || plain === '', onClick: () => {
|
|
58
|
+
const already = typeof selectedIndex === 'number'
|
|
59
|
+
? selectedIndex === tokenIndex
|
|
60
|
+
: selectedWord === token;
|
|
61
|
+
emit(itemIndex, {
|
|
62
|
+
type: item.type,
|
|
63
|
+
wordIndex: already ? null : tokenIndex,
|
|
64
|
+
crossOut: already
|
|
65
|
+
? ''
|
|
66
|
+
: token.replace(/[^\p{L}\p{N}'-]/gu, '') || token,
|
|
67
|
+
...(isReplace ? { replaceWith } : {}),
|
|
68
|
+
});
|
|
69
|
+
}, className: cn('rounded-lg px-2 py-1 transition-colors', isSelected
|
|
70
|
+
? 'border border-blue-200 bg-blue-100 font-bold text-blue-700 line-through decoration-blue-600'
|
|
71
|
+
: isCorrectTarget
|
|
72
|
+
? 'border-2 border-dashed border-emerald-500 bg-emerald-50 font-bold text-emerald-800'
|
|
73
|
+
: canSelect
|
|
74
|
+
? 'cursor-pointer hover:bg-blue-50 hover:text-blue-700'
|
|
75
|
+
: 'cursor-default'), children: token }, `${item.questionNumber}-token-${tokenIndex}`));
|
|
76
|
+
}) }), isReviewMode ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("span", { className: "font-medium", children: "\u0110\u00E1p \u00E1n: " }), formatAnswer(item.correctAnswer)] })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
26
77
|
}), 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] }))] }));
|
|
27
78
|
}
|
|
@@ -7,5 +7,5 @@ interface CrosswordPuzzleClientProps {
|
|
|
7
7
|
userAnswers?: Record<string, string>;
|
|
8
8
|
onAnswerChange?: (key: string, value: string) => void;
|
|
9
9
|
}
|
|
10
|
-
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
10
|
+
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, onAnswerChange, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
11
11
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, }) {
|
|
4
|
+
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, onAnswerChange, }) {
|
|
5
5
|
const rows = content?.gridSize?.rows || 12;
|
|
6
6
|
const cols = content?.gridSize?.cols || 12;
|
|
7
7
|
const acrossClues = useMemo(() => content?.clues?.across || [], [content?.clues?.across]);
|
|
@@ -33,11 +33,11 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
33
33
|
return (_jsxs("div", { className: "flex flex-col gap-3 p-3 bg-white rounded-xl border border-gray-200 shadow-sm", children: [content?.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 text-center", children: content.title })), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("div", { className: "lg:col-span-5 flex flex-col gap-4", 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) => {
|
|
34
34
|
const answer = correctAnswer?.answers?.[`across-${c.number}`];
|
|
35
35
|
const userAns = userAnswers[`across-${c.number}`];
|
|
36
|
-
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: [_jsx("span", { className: "text-gray-800", children: c.clue }), 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}`));
|
|
36
|
+
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: [_jsx("span", { className: "text-gray-800", children: c.clue }), !isReviewMode && onAnswerChange ? (_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}`));
|
|
37
37
|
}) })] }), _jsxs("div", { className: "rounded-lg border border-purple-200 bg-purple-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-purple-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-purple-600 text-white text-xs font-semibold", children: "Down" }), _jsx("span", { children: "H\u00E0ng d\u1ECDc" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: downClues.map((c) => {
|
|
38
38
|
const answer = correctAnswer?.answers?.[`down-${c.number}`];
|
|
39
39
|
const userAns = userAnswers[`down-${c.number}`];
|
|
40
|
-
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: [_jsx("span", { className: "text-gray-800", children: c.clue }), 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}`));
|
|
40
|
+
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: [_jsx("span", { className: "text-gray-800", children: c.clue }), !isReviewMode && onAnswerChange ? (_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}`));
|
|
41
41
|
}) })] })] }), _jsx("div", { className: "lg:col-span-7 flex justify-center overflow-x-auto p-4 bg-gray-50 rounded-xl border border-gray-200", children: _jsx("div", { className: "inline-grid gap-1", style: {
|
|
42
42
|
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
43
43
|
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
@@ -66,8 +66,18 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
66
66
|
previewLetter = word[offset] || '';
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
else
|
|
70
|
-
|
|
69
|
+
else {
|
|
70
|
+
if (cellInfo.across) {
|
|
71
|
+
const word = userAnswers[`across-${cellInfo.across.number}`] || '';
|
|
72
|
+
previewLetter = word[c - cellInfo.across.startCol] || '';
|
|
73
|
+
}
|
|
74
|
+
if (!previewLetter && cellInfo.down) {
|
|
75
|
+
const word = userAnswers[`down-${cellInfo.down.number}`] || '';
|
|
76
|
+
previewLetter = word[r - cellInfo.down.startRow] || '';
|
|
77
|
+
}
|
|
78
|
+
if (!previewLetter) {
|
|
79
|
+
previewLetter = userAnswers[key] || '';
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
|
|
73
83
|
let textColor = isExample
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { FindWordsInMatrixContent, FindWordsInMatrixCorrectAnswer } from '../../../../shared/types/questions/find-words-in-matrix';
|
|
2
|
+
import type { FindWordsInMatrixContent, FindWordsInMatrixCorrectAnswer, MatrixCellCoord } from '../../../../shared/types/questions/find-words-in-matrix';
|
|
3
|
+
type WordSelection = {
|
|
4
|
+
wordId?: string;
|
|
5
|
+
word?: string;
|
|
6
|
+
categoryId?: string;
|
|
7
|
+
path: MatrixCellCoord[];
|
|
8
|
+
};
|
|
3
9
|
interface FindWordsInMatrixClientProps {
|
|
4
10
|
content: FindWordsInMatrixContent;
|
|
5
11
|
correctAnswer?: FindWordsInMatrixCorrectAnswer;
|
|
6
12
|
showSolution?: boolean;
|
|
13
|
+
userAnswer?: unknown;
|
|
14
|
+
onAnswerChange?: (payload: {
|
|
15
|
+
words: WordSelection[];
|
|
16
|
+
}) => void;
|
|
7
17
|
}
|
|
8
|
-
export declare function FindWordsInMatrixClient({ content, correctAnswer, showSolution, }: FindWordsInMatrixClientProps): React.JSX.Element;
|
|
18
|
+
export declare function FindWordsInMatrixClient({ content, correctAnswer, showSolution, userAnswer, onAnswerChange, }: FindWordsInMatrixClientProps): React.JSX.Element;
|
|
9
19
|
export {};
|
|
@@ -1,69 +1,168 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
3
4
|
import { Card } from '../../../../components/ui/card';
|
|
4
5
|
import { Badge } from '../../../../components/ui/badge';
|
|
5
|
-
// 10 Distinct pastel highlighter colors for word selections
|
|
6
6
|
const WORD_COLORS = [
|
|
7
|
-
{ bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white'
|
|
8
|
-
{ bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white'
|
|
9
|
-
{ bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white'
|
|
10
|
-
{ bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white'
|
|
11
|
-
{ bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white'
|
|
12
|
-
{ bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white'
|
|
13
|
-
{ bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white'
|
|
14
|
-
{ bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white'
|
|
15
|
-
{ bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white'
|
|
16
|
-
{ bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white'
|
|
7
|
+
{ bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white' },
|
|
8
|
+
{ bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white' },
|
|
9
|
+
{ bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white' },
|
|
10
|
+
{ bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white' },
|
|
11
|
+
{ bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white' },
|
|
12
|
+
{ bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white' },
|
|
13
|
+
{ bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white' },
|
|
14
|
+
{ bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white' },
|
|
15
|
+
{ bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white' },
|
|
16
|
+
{ bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white' },
|
|
17
17
|
];
|
|
18
|
-
|
|
18
|
+
function cleanWord(value) {
|
|
19
|
+
return (value || '').replace(/[\s\-_]/g, '').toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
function getStraightLinePath(start, end) {
|
|
22
|
+
const dr = end.row - start.row;
|
|
23
|
+
const dc = end.col - start.col;
|
|
24
|
+
const stepR = dr === 0 ? 0 : dr > 0 ? 1 : -1;
|
|
25
|
+
const stepC = dc === 0 ? 0 : dc > 0 ? 1 : -1;
|
|
26
|
+
if (dr === 0 && dc !== 0) {
|
|
27
|
+
return Array.from({ length: Math.abs(dc) + 1 }, (_, i) => ({
|
|
28
|
+
row: start.row,
|
|
29
|
+
col: start.col + i * stepC,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
if (dc === 0 && dr !== 0) {
|
|
33
|
+
return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
|
|
34
|
+
row: start.row + i * stepR,
|
|
35
|
+
col: start.col,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
if (Math.abs(dr) === Math.abs(dc) && dr !== 0) {
|
|
39
|
+
return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
|
|
40
|
+
row: start.row + i * stepR,
|
|
41
|
+
col: start.col + i * stepC,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
return dr === 0 && dc === 0 ? [start] : [];
|
|
45
|
+
}
|
|
46
|
+
function readSelections(value) {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (value && typeof value === 'object' && Array.isArray(value.words)) {
|
|
51
|
+
return value.words;
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
function pathLetters(grid, path) {
|
|
56
|
+
return path
|
|
57
|
+
.map((cell) => (grid[cell.row - 1]?.[cell.col - 1] || '').toUpperCase())
|
|
58
|
+
.join('');
|
|
59
|
+
}
|
|
60
|
+
export function FindWordsInMatrixClient({ content, correctAnswer, showSolution = false, userAnswer, onAnswerChange, }) {
|
|
19
61
|
const rows = content.gridSize?.rows || content.grid?.length || 14;
|
|
20
62
|
const cols = content.gridSize?.cols || content.grid?.[0]?.length || 14;
|
|
21
63
|
const grid = content.grid || [];
|
|
22
64
|
const wordBank = content.wordBank || [];
|
|
23
65
|
const isCategorize = content.type === 'CATEGORIZE' && Array.isArray(content.categories) && content.categories.length > 0;
|
|
24
66
|
const categories = content.categories || [];
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
67
|
+
const canSelect = !showSolution && Boolean(onAnswerChange);
|
|
68
|
+
const selections = readSelections(userAnswer);
|
|
69
|
+
const [activeWordId, setActiveWordId] = useState(null);
|
|
70
|
+
const [anchor, setAnchor] = useState(null);
|
|
71
|
+
const playableWords = wordBank.filter((word) => !word.isExample);
|
|
72
|
+
const activeWord = playableWords.find((word) => word.id === activeWordId) ||
|
|
73
|
+
playableWords.find((word) => !selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word))) ||
|
|
74
|
+
null;
|
|
75
|
+
const cellColorMap = useMemo(() => {
|
|
76
|
+
const map = {};
|
|
77
|
+
if (content.exampleSelection?.path) {
|
|
78
|
+
for (const cell of content.exampleSelection.path) {
|
|
79
|
+
map[`${cell.row}-${cell.col}`] = { colorIdx: 0, word: 'Example' };
|
|
80
|
+
}
|
|
31
81
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
const source = showSolution && correctAnswer?.words ? correctAnswer.words : selections;
|
|
83
|
+
source.forEach((item, index) => {
|
|
84
|
+
const colorIdx = (index + 1) % WORD_COLORS.length;
|
|
85
|
+
item.path?.forEach((cell) => {
|
|
86
|
+
map[`${cell.row}-${cell.col}`] = {
|
|
87
|
+
colorIdx,
|
|
88
|
+
word: item.word || item.wordId || '',
|
|
89
|
+
};
|
|
39
90
|
});
|
|
40
91
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
92
|
+
return map;
|
|
93
|
+
}, [content.exampleSelection, correctAnswer?.words, selections, showSolution]);
|
|
94
|
+
const emit = (next) => {
|
|
95
|
+
onAnswerChange?.({ words: next });
|
|
96
|
+
};
|
|
97
|
+
const handleWordClick = (wordId) => {
|
|
98
|
+
if (!canSelect)
|
|
99
|
+
return;
|
|
100
|
+
const already = selections.find((selection) => selection.wordId === wordId);
|
|
101
|
+
if (already) {
|
|
102
|
+
emit(selections.filter((selection) => selection.wordId !== wordId));
|
|
103
|
+
setActiveWordId(null);
|
|
104
|
+
setAnchor(null);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
setActiveWordId(wordId);
|
|
108
|
+
setAnchor(null);
|
|
109
|
+
};
|
|
110
|
+
const handleCellClick = (row, col) => {
|
|
111
|
+
if (!canSelect || !activeWord)
|
|
112
|
+
return;
|
|
113
|
+
const cell = { row, col };
|
|
114
|
+
if (!anchor) {
|
|
115
|
+
setAnchor(cell);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const path = getStraightLinePath(anchor, cell);
|
|
119
|
+
const letters = pathLetters(grid, path);
|
|
120
|
+
const target = cleanWord(activeWord.word);
|
|
121
|
+
const matched = letters === target || [...letters].reverse().join('') === target;
|
|
122
|
+
setAnchor(null);
|
|
123
|
+
if (!matched || path.length === 0) {
|
|
124
|
+
setAnchor(cell);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
emit([
|
|
128
|
+
...selections.filter((selection) => selection.wordId !== activeWord.id && cleanWord(selection.word) !== target),
|
|
129
|
+
{
|
|
130
|
+
wordId: activeWord.id,
|
|
131
|
+
word: activeWord.word,
|
|
132
|
+
categoryId: activeWord.categoryId,
|
|
133
|
+
path,
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
setActiveWordId(null);
|
|
137
|
+
};
|
|
138
|
+
const renderWordBadge = (word, index) => {
|
|
139
|
+
const isExample = Boolean(word.isExample);
|
|
140
|
+
const isFound = selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word));
|
|
141
|
+
const isActive = activeWord?.id === word.id;
|
|
142
|
+
return (_jsxs("button", { type: "button", disabled: !canSelect || isExample, onClick: () => handleWordClick(word.id), className: `rounded-full border px-3 py-1.5 text-sm font-medium ${isExample
|
|
143
|
+
? 'border-amber-300 bg-amber-50 text-amber-800 line-through'
|
|
144
|
+
: isFound
|
|
145
|
+
? 'border-emerald-400 bg-emerald-50 text-emerald-800'
|
|
146
|
+
: isActive
|
|
147
|
+
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
148
|
+
: 'border-gray-200 bg-white'}`, children: [word.word, isExample ? _jsx("span", { className: "ml-1 font-bold", children: "(Ex)" }) : null] }, word.id || index));
|
|
149
|
+
};
|
|
150
|
+
return (_jsxs("div", { className: "space-y-3", children: [content.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 dark:text-gray-100", children: content.title })), canSelect ? (_jsx("p", { className: "text-xs text-slate-500", children: "Ch\u1ECDn t\u1EEB trong word bank, r\u1ED3i b\u1EA5m \u00F4 \u0111\u1EA7u v\u00E0 \u00F4 cu\u1ED1i tr\u00EAn l\u01B0\u1EDBi." })) : null, isCategorize ? (_jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3", children: categories.map((cat) => {
|
|
151
|
+
const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
|
|
152
|
+
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));
|
|
153
|
+
}) })) : (_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: "flex justify-center overflow-x-auto pb-2", children: _jsx("div", { className: "inline-grid gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm", style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
58
154
|
const r = rIdx + 1;
|
|
59
155
|
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
60
156
|
const c = cIdx + 1;
|
|
61
157
|
const char = grid[rIdx]?.[cIdx] || '';
|
|
62
158
|
const highlight = cellColorMap[`${r}-${c}`];
|
|
63
159
|
const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
160
|
+
const isAnchor = anchor?.row === r && anchor?.col === c;
|
|
161
|
+
return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex h-9 w-9 items-center justify-center rounded-md border text-sm font-bold sm:h-11 sm:w-11 sm:text-base ${colorConfig
|
|
162
|
+
? `${colorConfig.bg} shadow-sm`
|
|
163
|
+
: isAnchor
|
|
164
|
+
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
165
|
+
: 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
|
|
67
166
|
});
|
|
68
167
|
}) }) })] }));
|
|
69
168
|
}
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { TrueFalseCorrectGroupClientProps } from '../../_shared/types/true-false-correct-group.type';
|
|
2
|
-
export declare function TrueFalseCorrectGroupClient({ questionData, isReviewMode, userAnswers, }: TrueFalseCorrectGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function TrueFalseCorrectGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: TrueFalseCorrectGroupClientProps): import("react").JSX.Element;
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js
CHANGED
|
@@ -2,24 +2,46 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
-
|
|
5
|
+
function emptyAnswer() {
|
|
6
|
+
return { isTrue: null, correction: '' };
|
|
7
|
+
}
|
|
8
|
+
export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
6
9
|
const items = questionData.items || [];
|
|
10
|
+
const emit = (itemIndex, nextItem) => {
|
|
11
|
+
const item = items[itemIndex];
|
|
12
|
+
if (isReviewMode || item?.isExample || !onAnswerChange) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const next = items.map((_, index) => userAnswers[index] ?? emptyAnswer());
|
|
16
|
+
next[itemIndex] = nextItem;
|
|
17
|
+
onAnswerChange(next);
|
|
18
|
+
};
|
|
7
19
|
return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => {
|
|
8
|
-
const userValue = userAnswers[itemIndex];
|
|
9
20
|
const correctIsTrue = item.isTrue === true;
|
|
21
|
+
const userValue = item.isExample
|
|
22
|
+
? { isTrue: item.isTrue, correction: item.correction }
|
|
23
|
+
: userAnswers[itemIndex];
|
|
24
|
+
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
25
|
+
const showStudentCorrection = userValue?.isTrue === false || (isReviewMode && !correctIsTrue);
|
|
10
26
|
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "flex items-center gap-3", children: [
|
|
11
27
|
{ value: true, label: 'True' },
|
|
12
28
|
{ value: false, label: 'False' },
|
|
13
29
|
].map((opt) => {
|
|
14
30
|
const isCorrect = opt.value === correctIsTrue;
|
|
15
31
|
const isSelected = userValue?.isTrue === opt.value;
|
|
16
|
-
return (_jsxs("
|
|
32
|
+
return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => emit(itemIndex, {
|
|
33
|
+
isTrue: opt.value,
|
|
34
|
+
correction: opt.value ? '' : (userValue?.correction || ''),
|
|
35
|
+
}), className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', canSelect ? 'cursor-pointer hover:border-blue-300' : 'cursor-default', isReviewMode && isCorrect
|
|
17
36
|
? 'border-green-500 bg-green-50 text-green-700'
|
|
18
37
|
: isReviewMode && isSelected && !isCorrect
|
|
19
38
|
? 'border-red-500 bg-red-50 text-red-700'
|
|
20
39
|
: isSelected
|
|
21
40
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
|
22
41
|
: 'border-gray-200 bg-white text-gray-700'), children: [opt.value ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(X, { className: "h-4 w-4" }), _jsx("span", { children: opt.label })] }, String(opt.value)));
|
|
23
|
-
}) }),
|
|
42
|
+
}) }), showStudentCorrection && canSelect ? (_jsxs("div", { className: "rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx("label", { className: "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500", children: "Correct the false sentence" }), _jsx("textarea", { value: userValue?.correction || '', onChange: (event) => emit(itemIndex, {
|
|
43
|
+
isTrue: false,
|
|
44
|
+
correction: event.target.value,
|
|
45
|
+
}), placeholder: "Write the correct sentence...", rows: 2, className: "w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })] })) : null, isReviewMode && !correctIsTrue ? (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50/70 px-3 py-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-amber-800", children: "C\u00E2u \u0111\u00FAng" }), _jsx("p", { className: "mt-1 text-sm text-amber-900", children: item.correction || 'Chưa nhập câu sửa' }), userValue && userValue.isTrue === false ? (_jsxs("p", { className: "mt-2 text-xs text-slate-600", children: ["H\u1ECDc sinh: ", userValue.correction || '(trống)'] })) : null] })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
24
46
|
}), 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] }))] }));
|
|
25
47
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { TrueFalseGroupClientProps } from '../../_shared/types/true-false-group.type';
|
|
2
|
-
export declare function TrueFalseGroupClient({ questionData, isReviewMode, userAnswers, }: TrueFalseGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function TrueFalseGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: TrueFalseGroupClientProps): import("react").JSX.Element;
|