@tinyweb_dev/oe-exam-sdk 0.2.4 → 0.2.5
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/QuestionRenderer.js +11 -3
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.d.ts +10 -0
- package/dist/components/exams/take/components/question-renderers/ChooseBestAnswerImages.js +38 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +5 -11
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureRenderer.js +75 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseCorrectGroupRenderer.js +92 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +2 -1
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +2 -2
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +2 -0
- package/dist/components/exams/take/exam-taking.utils.d.ts +1 -0
- package/dist/components/exams/take/exam-taking.utils.js +9 -1
- package/dist/components/exams/take/types.d.ts +3 -2
- package/dist/components/exams/take/utils/answer-transformers.js +12 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +29 -1
- package/dist/components/exams/take/utils/question-transformers.js +98 -0
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +9 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +49 -1
- package/dist/components/questions/_shared/types/index.d.ts +2 -0
- package/dist/components/questions/_shared/types/index.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture.type.d.ts +62 -0
- package/dist/components/questions/_shared/types/match-word-to-picture.type.js +1 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +42 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.js +2 -0
- package/dist/components/questions/_shared/types/write-a-short-letter.type.d.ts +4 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +13 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +32 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +42 -6
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +40 -20
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.d.ts +1 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +5 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +31 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureCreator.js +267 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +65 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.d.ts +8 -0
- package/dist/components/questions/types/match-word-to-picture/WordBankEditor.js +15 -0
- package/dist/components/questions/types/match-word-to-picture/index.d.ts +6 -0
- package/dist/components/questions/types/match-word-to-picture/index.js +6 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js +53 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.d.ts +4 -0
- package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-group-data.js +75 -0
- package/dist/components/questions/types/match-word-to-picture/register.d.ts +2 -0
- package/dist/components/questions/types/match-word-to-picture/register.js +55 -0
- package/dist/components/questions/types/match-word-to-picture/transform.d.ts +11 -0
- package/dist/components/questions/types/match-word-to-picture/transform.js +43 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +25 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js +144 -0
- package/dist/components/questions/types/true-false-correct-group/index.d.ts +5 -0
- package/dist/components/questions/types/true-false-correct-group/index.js +5 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.d.ts +4 -0
- package/dist/components/questions/types/true-false-correct-group/map-true-false-correct-group-data.js +79 -0
- package/dist/components/questions/types/true-false-correct-group/register.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/register.js +36 -0
- package/dist/components/questions/types/true-false-correct-group/transform.d.ts +2 -0
- package/dist/components/questions/types/true-false-correct-group/transform.js +49 -0
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +11 -1
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +8 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +4 -0
- package/dist/components/questions/types/write-a-short-letter/transform.js +4 -0
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.d.ts +2 -1
- package/dist/components/questions/viewer/ChooseTheCorrectAnswerGroupViewer.js +2 -2
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +11 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.d.ts +9 -0
- package/dist/components/questions/viewer/MatchWordToPictureViewer.js +8 -0
- package/dist/components/questions/viewer/QuestionViewer.js +18 -3
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +9 -0
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -0
- package/dist/components/questions/viewer/index.d.ts +3 -0
- package/dist/components/questions/viewer/index.js +3 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
- package/dist/components/results/renderers/review-question-body-movers.js +39 -3
- package/dist/components/results/review-data.js +22 -0
- package/dist/components/themes/english-certification/EcWritingTask.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +4 -0
- package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +3 -1
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +30 -12
- package/dist/shared/lib/utils/question-reverse-transform.js +56 -0
- package/dist/shared/lib/utils/question-transform.js +4 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +4 -0
- package/dist/shared/types/questions/match-word-to-picture.d.ts +35 -0
- package/dist/shared/types/questions/match-word-to-picture.js +5 -0
- package/dist/shared/types/questions/true-false-correct-group.d.ts +39 -0
- package/dist/shared/types/questions/true-false-correct-group.js +1 -0
- package/package.json +7 -2
package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
+
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
+
function normalizeWord(value) {
|
|
7
|
+
return value.trim().toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function ClientImageItem({ url, alt }) {
|
|
10
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
11
|
+
const src = previewUrl ||
|
|
12
|
+
(typeof url === 'string' &&
|
|
13
|
+
(url.startsWith('http') || url.startsWith('blob:') || url.startsWith('data:'))
|
|
14
|
+
? url
|
|
15
|
+
: '');
|
|
16
|
+
if (!url) {
|
|
17
|
+
return (_jsx("div", { className: "flex h-28 w-28 items-center justify-center rounded-lg border border-dashed border-gray-300 bg-gray-50 text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 \u1EA3nh" }));
|
|
18
|
+
}
|
|
19
|
+
if (!src && !isLoading) {
|
|
20
|
+
return (_jsx("div", { className: "flex h-28 w-28 items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-xs text-gray-400", children: "Kh\u00F4ng t\u1EA3i \u0111\u01B0\u1EE3c \u1EA3nh" }));
|
|
21
|
+
}
|
|
22
|
+
return (_jsx("div", { className: "relative h-28 w-28 flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: isLoading ? (_jsx("div", { className: "flex h-full w-full items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-violet-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Picture', className: "h-full w-full object-contain" })) }));
|
|
23
|
+
}
|
|
24
|
+
export function MatchWordToPictureGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
25
|
+
const items = questionData.items || [];
|
|
26
|
+
const wordBank = questionData.wordBank || [];
|
|
27
|
+
const handleSelect = (itemIndex, value) => {
|
|
28
|
+
const item = items[itemIndex];
|
|
29
|
+
if (isReviewMode || item?.isExample || !onAnswerChange) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const next = items.map((entry, index) => entry.isExample ? entry.correctAnswer : (userAnswers[index] ?? ''));
|
|
33
|
+
next[itemIndex] = value;
|
|
34
|
+
onAnswerChange(next);
|
|
35
|
+
};
|
|
36
|
+
const usedWords = new Set();
|
|
37
|
+
items.forEach((item, index) => {
|
|
38
|
+
const value = item.isExample ? item.correctAnswer : (userAnswers[index] ?? '');
|
|
39
|
+
if (value.trim()) {
|
|
40
|
+
usedWords.add(normalizeWord(value));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
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 })), wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-violet-200 bg-gradient-to-r from-violet-50 to-indigo-50 p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-violet-700", children: "Word bank:" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
|
|
44
|
+
const isUsed = usedWords.has(normalizeWord(word));
|
|
45
|
+
return (_jsx("span", { className: cn('rounded-full border px-3 py-1 text-sm font-semibold', isUsed
|
|
46
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
47
|
+
: 'border-violet-200 bg-white text-slate-700'), children: word }, `${word}-${index}`));
|
|
48
|
+
}) })] })), items.map((item, itemIndex) => {
|
|
49
|
+
const selectedWord = item.isExample
|
|
50
|
+
? item.correctAnswer
|
|
51
|
+
: (userAnswers[itemIndex] ?? '');
|
|
52
|
+
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
53
|
+
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
54
|
+
Boolean(item.correctAnswer);
|
|
55
|
+
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-4 py-2.5", 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-violet-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: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center", 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-2", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("select", { value: selectedWord, disabled: !canSelect, onChange: (event) => handleSelect(itemIndex, event.target.value), className: cn('h-10 max-w-xs rounded-md border-2 bg-white px-3 text-sm font-medium', isReviewMode && isCorrect
|
|
56
|
+
? 'border-green-400 bg-green-50 text-green-700'
|
|
57
|
+
: isReviewMode && selectedWord && !isCorrect
|
|
58
|
+
? 'border-red-400 bg-red-50 text-red-700'
|
|
59
|
+
: 'border-violet-300 focus:border-violet-500 focus:ring-violet-200', !canSelect && 'cursor-default opacity-80'), children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, wordIndex) => {
|
|
60
|
+
const isUsedByOther = usedWords.has(normalizeWord(word)) &&
|
|
61
|
+
normalizeWord(word) !== normalizeWord(selectedWord);
|
|
62
|
+
return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${wordIndex}`));
|
|
63
|
+
})] }), isReviewMode && !item.isExample && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(Check, { className: "h-5 w-5 text-green-500" })) : (_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}`));
|
|
64
|
+
}), 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] }))] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface WordBankEditorProps {
|
|
2
|
+
wordBank: string[];
|
|
3
|
+
instruction: string;
|
|
4
|
+
onWordBankChange: (wordBank: string[]) => void;
|
|
5
|
+
onInstructionChange: (instruction: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function WordBankEditor({ wordBank, instruction, onWordBankChange, onInstructionChange, }: WordBankEditorProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from '../../../../components/ui/button';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
5
|
+
import { Input } from '../../../../components/ui/input';
|
|
6
|
+
import { Label } from '../../../../components/ui/label';
|
|
7
|
+
import { Plus, Trash2, Type } from 'lucide-react';
|
|
8
|
+
export function WordBankEditor({ wordBank, instruction, onWordBankChange, onInstructionChange, }) {
|
|
9
|
+
const updateWord = (index, value) => {
|
|
10
|
+
const next = [...wordBank];
|
|
11
|
+
next[index] = value;
|
|
12
|
+
onWordBankChange(next);
|
|
13
|
+
};
|
|
14
|
+
return (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-violet-100/50", children: [_jsx(CardHeader, { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-violet-600 shadow-sm", children: _jsx(Type, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "Word bank nh\u00F3m" }), _jsx("p", { className: "text-xs text-gray-500", children: "C\u00E1c t\u1EEB d\u00F9ng chung cho m\u1ECDi tranh trong nh\u00F3m" })] })] }) }), _jsxs(CardContent, { className: "space-y-4 px-4 pb-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-instruction", className: "text-sm font-medium text-gray-700", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-instruction", value: instruction, onChange: (event) => onInstructionChange(event.target.value), placeholder: "Look and complete." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { className: "text-sm font-medium text-gray-700", children: "Danh s\u00E1ch t\u1EEB" }), _jsx("div", { className: "space-y-2", children: wordBank.map((word, index) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: word, onChange: (event) => updateWord(index, event.target.value), placeholder: `Từ ${index + 1}` }), _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: () => onWordBankChange(wordBank.filter((_, wordIndex) => wordIndex !== index)), "aria-label": `Xóa từ ${index + 1}`, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, index))) }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onWordBankChange([...wordBank, '']), className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm t\u1EEB"] })] })] })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MatchWordToPictureCreator } from './MatchWordToPictureCreator';
|
|
2
|
+
export { MatchWordToPictureClient } from './MatchWordToPictureClient';
|
|
3
|
+
export { MatchWordToPictureGroupClient } from './MatchWordToPictureGroupClient';
|
|
4
|
+
export { matchWordToPictureRegistration } from './register';
|
|
5
|
+
export { mapQuestionToMatchWordToPictureData } from './map-match-word-to-picture-data';
|
|
6
|
+
export { mapQuestionToMatchWordToPictureGroupData } from './map-match-word-to-picture-group-data';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MatchWordToPictureCreator } from './MatchWordToPictureCreator';
|
|
2
|
+
export { MatchWordToPictureClient } from './MatchWordToPictureClient';
|
|
3
|
+
export { MatchWordToPictureGroupClient } from './MatchWordToPictureGroupClient';
|
|
4
|
+
export { matchWordToPictureRegistration } from './register';
|
|
5
|
+
export { mapQuestionToMatchWordToPictureData } from './map-match-word-to-picture-data';
|
|
6
|
+
export { mapQuestionToMatchWordToPictureGroupData } from './map-match-word-to-picture-group-data';
|
package/dist/components/questions/types/match-word-to-picture/map-match-word-to-picture-data.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture.type';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return isRecord(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
function asString(value, fallback = '') {
|
|
9
|
+
return typeof value === 'string' ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
function asStringList(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return value
|
|
16
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
export function mapQuestionToMatchWordToPictureData(question) {
|
|
20
|
+
const source = asRecord(question);
|
|
21
|
+
const content = asRecord(source.content);
|
|
22
|
+
const meta = asRecord(content.meta);
|
|
23
|
+
const answer = asRecord(source.answer ?? source.correctAnswer ?? source.correct_answer);
|
|
24
|
+
const questionGroup = asRecord(source.questionGroup);
|
|
25
|
+
const groupPayload = asRecord(questionGroup.payload);
|
|
26
|
+
const correctAnswer = asString(answer.answer) || asString(answer.correctAnswer);
|
|
27
|
+
const wordBank = asStringList(groupPayload.wordBank).length
|
|
28
|
+
? asStringList(groupPayload.wordBank)
|
|
29
|
+
: asStringList(meta.wordBank).length
|
|
30
|
+
? asStringList(meta.wordBank)
|
|
31
|
+
: asStringList(answer.wordBank).length
|
|
32
|
+
? asStringList(answer.wordBank)
|
|
33
|
+
: correctAnswer
|
|
34
|
+
? [correctAnswer]
|
|
35
|
+
: [];
|
|
36
|
+
return {
|
|
37
|
+
imageUrl: asString(content.imageUrl) || asString(answer.imageUrl),
|
|
38
|
+
correctAnswer,
|
|
39
|
+
points: typeof source.points === 'number' ? source.points : 1,
|
|
40
|
+
groupNumber: typeof questionGroup.groupNumber === 'number'
|
|
41
|
+
? questionGroup.groupNumber
|
|
42
|
+
: typeof answer.groupNumber === 'number'
|
|
43
|
+
? answer.groupNumber
|
|
44
|
+
: 1,
|
|
45
|
+
isExample: source.isExample === true || answer.isExample === true,
|
|
46
|
+
wordBank,
|
|
47
|
+
instruction: asString(groupPayload.instruction) ||
|
|
48
|
+
asString(meta.instruction) ||
|
|
49
|
+
asString(answer.instruction) ||
|
|
50
|
+
MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION,
|
|
51
|
+
explanation: asString(source.explanation) || asString(answer.explanation) || undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type MatchWordToPictureGroupData, type MatchWordToPictureGroupItemData } from '../../_shared/types/match-word-to-picture.type';
|
|
2
|
+
export declare function createEmptyMatchWordToPictureGroupItem(questionNumber: number): MatchWordToPictureGroupItemData;
|
|
3
|
+
export declare function mapMatchWordToPictureGroupItem(item: unknown, index: number, fallbackAnswer?: string): MatchWordToPictureGroupItemData;
|
|
4
|
+
export declare function mapQuestionToMatchWordToPictureGroupData(question: Record<string, unknown> | null | undefined): MatchWordToPictureGroupData;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture.type';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return isRecord(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
function asString(value, fallback = '') {
|
|
9
|
+
return typeof value === 'string' ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
function asStringList(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return value
|
|
16
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
export function createEmptyMatchWordToPictureGroupItem(questionNumber) {
|
|
20
|
+
return {
|
|
21
|
+
imageUrl: '',
|
|
22
|
+
correctAnswer: '',
|
|
23
|
+
isExample: false,
|
|
24
|
+
points: 1,
|
|
25
|
+
questionNumber,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function mapMatchWordToPictureGroupItem(item, index, fallbackAnswer) {
|
|
29
|
+
const record = asRecord(item);
|
|
30
|
+
const content = asRecord(record.content);
|
|
31
|
+
const correctAnswer = asRecord(record.correctAnswer);
|
|
32
|
+
const isExample = record.isExample === true;
|
|
33
|
+
return {
|
|
34
|
+
imageUrl: asString(content.imageUrl) || asString(record.imageUrl),
|
|
35
|
+
correctAnswer: asString(correctAnswer.answer) ||
|
|
36
|
+
asString(record.correctAnswer) ||
|
|
37
|
+
asString(fallbackAnswer),
|
|
38
|
+
isExample,
|
|
39
|
+
points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
|
|
40
|
+
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function mapQuestionToMatchWordToPictureGroupData(question) {
|
|
44
|
+
const source = asRecord(question);
|
|
45
|
+
const content = asRecord(source.content);
|
|
46
|
+
const meta = asRecord(content.meta);
|
|
47
|
+
const answer = asRecord(source.answer);
|
|
48
|
+
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
|
|
49
|
+
const questionGroup = asRecord(source.questionGroup);
|
|
50
|
+
const groupPayload = asRecord(questionGroup.payload);
|
|
51
|
+
const explanation = String(source.explanation || answer.explanation || '');
|
|
52
|
+
const fallbackAnswers = Array.isArray(correctAnswer.answer)
|
|
53
|
+
? correctAnswer.answer.map((value) => (typeof value === 'string' ? value : ''))
|
|
54
|
+
: [];
|
|
55
|
+
const items = Array.isArray(content.items)
|
|
56
|
+
? content.items.map((item, index) => mapMatchWordToPictureGroupItem(item, index, fallbackAnswers[index]))
|
|
57
|
+
: [createEmptyMatchWordToPictureGroupItem(1)];
|
|
58
|
+
const wordBank = asStringList(meta.wordBank).length
|
|
59
|
+
? asStringList(meta.wordBank)
|
|
60
|
+
: asStringList(groupPayload.wordBank).length
|
|
61
|
+
? asStringList(groupPayload.wordBank)
|
|
62
|
+
: asStringList(answer.wordBank).length
|
|
63
|
+
? asStringList(answer.wordBank)
|
|
64
|
+
: Array.from(new Set(items.map((item) => item.correctAnswer).filter(Boolean)));
|
|
65
|
+
return {
|
|
66
|
+
instruction: asString(meta.instruction) ||
|
|
67
|
+
asString(groupPayload.instruction) ||
|
|
68
|
+
asString(answer.instruction) ||
|
|
69
|
+
MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION,
|
|
70
|
+
wordBank,
|
|
71
|
+
items,
|
|
72
|
+
explanation,
|
|
73
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION } from '../../_shared/types/match-word-to-picture.type';
|
|
2
|
+
import { MatchWordToPictureCreator } from './MatchWordToPictureCreator';
|
|
3
|
+
export const matchWordToPictureRegistration = {
|
|
4
|
+
component: MatchWordToPictureCreator,
|
|
5
|
+
transformInitialData: (initialData) => {
|
|
6
|
+
const content = initialData?.content;
|
|
7
|
+
const answer = initialData?.answer;
|
|
8
|
+
const questionGroup = initialData?.questionGroup;
|
|
9
|
+
const groupPayload = questionGroup?.payload || {};
|
|
10
|
+
const groupNumber = questionGroup?.groupNumber || 1;
|
|
11
|
+
const wordBank = (Array.isArray(groupPayload.wordBank) && groupPayload.wordBank) ||
|
|
12
|
+
(Array.isArray(answer?.wordBank) && answer?.wordBank) ||
|
|
13
|
+
[];
|
|
14
|
+
return {
|
|
15
|
+
imageUrl: content?.imageUrl || answer?.imageUrl || '',
|
|
16
|
+
correctAnswer: answer?.answer || answer?.correctAnswer || '',
|
|
17
|
+
points: initialData?.points || 1,
|
|
18
|
+
groupNumber: answer?.groupNumber || groupNumber,
|
|
19
|
+
isExample: initialData?.isExample || answer?.isExample || false,
|
|
20
|
+
wordBank,
|
|
21
|
+
instruction: groupPayload.instruction ||
|
|
22
|
+
answer?.instruction ||
|
|
23
|
+
MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION,
|
|
24
|
+
explanation: initialData?.explanation || answer?.explanation || '',
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
getExtraProps: (ctx) => ({
|
|
28
|
+
groups: ctx.groups,
|
|
29
|
+
groupConfig: ctx.groupConfig,
|
|
30
|
+
questionConfig: ctx.questionConfig,
|
|
31
|
+
questionIndexInPart: ctx.questionIndexInPart,
|
|
32
|
+
partId: ctx.partId,
|
|
33
|
+
validationRef: ctx.validationRef,
|
|
34
|
+
}),
|
|
35
|
+
wrapOnSave: (data, ctx) => ({
|
|
36
|
+
type: ctx.questionType,
|
|
37
|
+
content: ctx.state.content,
|
|
38
|
+
points: data?.points ?? ctx.state.points ?? 1,
|
|
39
|
+
level: ctx.state.level,
|
|
40
|
+
difficulty: ctx.state.difficulty,
|
|
41
|
+
skill: ctx.state.skill,
|
|
42
|
+
answer: data,
|
|
43
|
+
explanation: data?.explanation || '',
|
|
44
|
+
}),
|
|
45
|
+
wrapOnChange: (data, ctx) => ({
|
|
46
|
+
type: ctx.questionType,
|
|
47
|
+
content: ctx.state.content,
|
|
48
|
+
points: data?.points ?? ctx.state.points ?? 1,
|
|
49
|
+
level: ctx.state.level,
|
|
50
|
+
difficulty: ctx.state.difficulty,
|
|
51
|
+
skill: ctx.state.skill,
|
|
52
|
+
answer: data,
|
|
53
|
+
explanation: data?.explanation || '',
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ExamQuestion } from '../../../../shared/types/entities/exam.types';
|
|
2
|
+
import type { TransformResult } from '../../../../shared/lib/utils/question-transform-types';
|
|
3
|
+
/**
|
|
4
|
+
* Transform MATCH_WORD_TO_PICTURE creator data → API format
|
|
5
|
+
*
|
|
6
|
+
* API format:
|
|
7
|
+
* content: { imageUrl }
|
|
8
|
+
* correctAnswer: { answer }
|
|
9
|
+
* questionGroup.payload: { wordBank, instruction }
|
|
10
|
+
*/
|
|
11
|
+
export declare function transformMatchWordToPicture(question: Partial<ExamQuestion>): TransformResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION } from '../../_shared/types/match-word-to-picture.type';
|
|
2
|
+
function normalizeWordBank(wordBank) {
|
|
3
|
+
if (!Array.isArray(wordBank)) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return wordBank
|
|
7
|
+
.filter((word) => typeof word === 'string')
|
|
8
|
+
.map((word) => word.trim())
|
|
9
|
+
.filter((word) => word.length > 0);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Transform MATCH_WORD_TO_PICTURE creator data → API format
|
|
13
|
+
*
|
|
14
|
+
* API format:
|
|
15
|
+
* content: { imageUrl }
|
|
16
|
+
* correctAnswer: { answer }
|
|
17
|
+
* questionGroup.payload: { wordBank, instruction }
|
|
18
|
+
*/
|
|
19
|
+
export function transformMatchWordToPicture(question) {
|
|
20
|
+
const answerData = question.answer;
|
|
21
|
+
if (!answerData) {
|
|
22
|
+
return { apiContent: {}, apiCorrectAnswer: {} };
|
|
23
|
+
}
|
|
24
|
+
const wordBank = normalizeWordBank(answerData.wordBank);
|
|
25
|
+
const instruction = typeof answerData.instruction === 'string' && answerData.instruction.trim()
|
|
26
|
+
? answerData.instruction.trim()
|
|
27
|
+
: MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
|
|
28
|
+
return {
|
|
29
|
+
apiContent: {
|
|
30
|
+
imageUrl: answerData.imageUrl || '',
|
|
31
|
+
},
|
|
32
|
+
apiCorrectAnswer: {
|
|
33
|
+
answer: answerData.correctAnswer || '',
|
|
34
|
+
},
|
|
35
|
+
totalPoints: answerData.points || 1,
|
|
36
|
+
imageData: {
|
|
37
|
+
wordBank,
|
|
38
|
+
instruction,
|
|
39
|
+
},
|
|
40
|
+
groupNumber: answerData.groupNumber || 1,
|
|
41
|
+
isExample: answerData.isExample || false,
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
+
export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
|
|
6
|
+
const items = questionData.items || [];
|
|
7
|
+
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
|
+
const correctIsTrue = item.isTrue === true;
|
|
10
|
+
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-4 py-2.5", 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-4", 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
|
+
{ value: true, label: 'True' },
|
|
12
|
+
{ value: false, label: 'False' },
|
|
13
|
+
].map((opt) => {
|
|
14
|
+
const isCorrect = opt.value === correctIsTrue;
|
|
15
|
+
const isSelected = userValue?.isTrue === opt.value;
|
|
16
|
+
return (_jsxs("div", { 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', isReviewMode && isCorrect
|
|
17
|
+
? 'border-green-500 bg-green-50 text-green-700'
|
|
18
|
+
: isReviewMode && isSelected && !isCorrect
|
|
19
|
+
? 'border-red-500 bg-red-50 text-red-700'
|
|
20
|
+
: isSelected
|
|
21
|
+
? 'border-blue-500 bg-blue-50 text-blue-700'
|
|
22
|
+
: '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
|
+
}) }), !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' }), isReviewMode && userValue && userValue.isTrue === false && (_jsxs("p", { className: "mt-2 text-xs text-slate-600", children: ["H\u1ECDc sinh: ", userValue.correction || '(trống)'] }))] }))] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
24
|
+
}), 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
|
+
}
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { TrueFalseCorrectGroupCreatorProps } from '../../_shared/types/true-false-correct-group.type';
|
|
2
|
+
export declare function TrueFalseCorrectGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: TrueFalseCorrectGroupCreatorProps): import("react").JSX.Element;
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupCreator.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { Label } from '../../../../components/ui/label';
|
|
6
|
+
import { Input } from '../../../../components/ui/input';
|
|
7
|
+
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
|
+
import { PointsInput } from '../../../../components/ui/points-input';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
10
|
+
import { Switch } from '../../../../components/ui/switch';
|
|
11
|
+
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
12
|
+
import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
|
|
13
|
+
import { TrueFalseCorrectGroupClient } from './TrueFalseCorrectGroupClient';
|
|
14
|
+
import { TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/true-false-correct-group.type';
|
|
15
|
+
function parsePointsValue(raw, allowZero = false) {
|
|
16
|
+
if (raw === '')
|
|
17
|
+
return allowZero ? 0 : 1;
|
|
18
|
+
const parsed = parseInt(raw.replace(/[^0-9]/g, ''), 10);
|
|
19
|
+
if (Number.isNaN(parsed))
|
|
20
|
+
return allowZero ? 0 : 1;
|
|
21
|
+
const min = allowZero ? 0 : 1;
|
|
22
|
+
return Math.max(min, parsed);
|
|
23
|
+
}
|
|
24
|
+
function createEmptyItem(questionNumber) {
|
|
25
|
+
return {
|
|
26
|
+
question: '',
|
|
27
|
+
isTrue: true,
|
|
28
|
+
correction: '',
|
|
29
|
+
isExample: false,
|
|
30
|
+
points: 1,
|
|
31
|
+
questionNumber,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function normalizeItems(rawItems) {
|
|
35
|
+
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
36
|
+
return [createEmptyItem(1)];
|
|
37
|
+
}
|
|
38
|
+
return rawItems.map((item, index) => {
|
|
39
|
+
const isTrue = item.isTrue === true;
|
|
40
|
+
return {
|
|
41
|
+
question: item.question || '',
|
|
42
|
+
isTrue,
|
|
43
|
+
correction: isTrue ? '' : (item.correction || ''),
|
|
44
|
+
isExample: item.isExample === true,
|
|
45
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
46
|
+
questionNumber: item.questionNumber || index + 1,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function sumPoints(items) {
|
|
51
|
+
return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
|
|
52
|
+
}
|
|
53
|
+
export function TrueFalseCorrectGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
54
|
+
const [instruction, setInstruction] = useState(initialData?.instruction || TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION);
|
|
55
|
+
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
56
|
+
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
57
|
+
const [isClientMode, setIsClientMode] = useState(false);
|
|
58
|
+
const [errors, setErrors] = useState([]);
|
|
59
|
+
const onChangeRef = useRef(onChange);
|
|
60
|
+
const previousPayloadRef = useRef('');
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
onChangeRef.current = onChange;
|
|
63
|
+
}, [onChange]);
|
|
64
|
+
const buildPayload = (nextInstruction = instruction, nextItems = items, nextExplanation = explanation) => ({
|
|
65
|
+
instruction: nextInstruction,
|
|
66
|
+
items: nextItems,
|
|
67
|
+
explanation: nextExplanation,
|
|
68
|
+
points: sumPoints(nextItems),
|
|
69
|
+
});
|
|
70
|
+
const debouncedOnChange = useDebouncedCallback((payload) => {
|
|
71
|
+
onChangeRef.current?.(payload);
|
|
72
|
+
}, 300);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const payload = buildPayload();
|
|
75
|
+
const serialized = JSON.stringify(payload);
|
|
76
|
+
if (serialized !== previousPayloadRef.current) {
|
|
77
|
+
previousPayloadRef.current = serialized;
|
|
78
|
+
debouncedOnChange(payload);
|
|
79
|
+
onUnsavedChangesChange?.(true);
|
|
80
|
+
}
|
|
81
|
+
}, [instruction, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
82
|
+
const validate = () => {
|
|
83
|
+
const nextErrors = [];
|
|
84
|
+
if (!instruction.trim()) {
|
|
85
|
+
nextErrors.push('Hướng dẫn (Instruction) không được để trống.');
|
|
86
|
+
}
|
|
87
|
+
if (items.length === 0) {
|
|
88
|
+
nextErrors.push('Cần ít nhất một câu hỏi trong nhóm.');
|
|
89
|
+
}
|
|
90
|
+
items.forEach((item, index) => {
|
|
91
|
+
if (!item.question.trim()) {
|
|
92
|
+
nextErrors.push(`Câu ${index + 1}: Nội dung câu hỏi không được để trống.`);
|
|
93
|
+
}
|
|
94
|
+
if (item.isTrue === false && !item.correction.trim()) {
|
|
95
|
+
nextErrors.push(`Câu ${index + 1}: Câu False cần nhập câu sửa đúng.`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
setErrors(nextErrors);
|
|
99
|
+
return nextErrors.length === 0;
|
|
100
|
+
};
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (validationRef) {
|
|
103
|
+
validationRef.current = {
|
|
104
|
+
triggerValidation: async () => validate(),
|
|
105
|
+
getFormData: () => buildPayload(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}, [validationRef, instruction, items, explanation]);
|
|
109
|
+
const updateItem = (index, patch) => {
|
|
110
|
+
setItems((prev) => {
|
|
111
|
+
const next = [...prev];
|
|
112
|
+
const current = next[index];
|
|
113
|
+
if (!current)
|
|
114
|
+
return prev;
|
|
115
|
+
next[index] = { ...current, ...patch };
|
|
116
|
+
return next;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const setItemTruth = (index, isTrue) => {
|
|
120
|
+
updateItem(index, {
|
|
121
|
+
isTrue,
|
|
122
|
+
correction: isTrue ? '' : items[index]?.correction || '',
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const addItem = () => {
|
|
126
|
+
setItems((prev) => [...prev, createEmptyItem(prev.length + 1)]);
|
|
127
|
+
};
|
|
128
|
+
const removeItem = (index) => {
|
|
129
|
+
setItems((prev) => {
|
|
130
|
+
if (prev.length <= 1)
|
|
131
|
+
return prev;
|
|
132
|
+
const filtered = prev.filter((_, i) => i !== index);
|
|
133
|
+
return filtered.map((item, i) => ({ ...item, questionNumber: i + 1 }));
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const currentPayload = buildPayload();
|
|
137
|
+
const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
|
|
138
|
+
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-medium text-gray-900", children: "T\u1EA1o c\u00E2u h\u1ECFi \u0110\u00FAng / Sai + S\u1EEDa c\u00E2u (Nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "Tick/Cross t\u1EEBng c\u00E2u. C\u00E2u False b\u1EAFt bu\u1ED9c vi\u1EBFt l\u1EA1i c\u00E2u \u0111\u00FAng." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode((prev) => !prev), className: "gap-2", children: [isClientMode ? _jsx(Pencil, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }), isClientMode ? 'Chế độ chỉnh sửa' : 'Xem trước học sinh'] })] }), displayErrors.length > 0 && (_jsx("div", { className: "rounded-lg bg-red-50 p-4 border border-red-200", children: _jsx("div", { className: "flex", children: _jsxs("div", { className: "ml-3", children: [_jsx("h3", { className: "text-sm font-medium text-red-800", children: "C\u1EA7n s\u1EEDa c\u00E1c l\u1ED7i sau:" }), _jsx("div", { className: "mt-2 text-sm text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-5", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })] }) }) })), isClientMode ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Xem tr\u01B0\u1EDBc hi\u1EC3n th\u1ECB h\u1ECDc sinh" }) }), _jsx(CardContent, { children: _jsx(TrueFalseCorrectGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "H\u01B0\u1EDBng d\u1EABn chung (Instruction)" }) }), _jsx(CardContent, { className: "space-y-4", children: _jsxs("div", { children: [_jsx(Label, { htmlFor: "instruction", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: TRUE_FALSE_CORRECT_GROUP_DEFAULT_INSTRUCTION, className: "mt-1" })] }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["Danh s\u00E1ch c\u00E2u h\u1ECFi (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u00E2u h\u1ECFi"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-4 rounded-lg border border-gray-200 bg-gray-50/50 p-4 transition-colors hover:border-gray-300", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700", children: item.questionNumber || index + 1 }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u h\u1ECFi ", index + 1] })] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, {
|
|
139
|
+
isExample: checked,
|
|
140
|
+
points: checked ? 0 : 1,
|
|
141
|
+
}) }), _jsxs(Label, { htmlFor: `example-${index}`, className: "flex items-center gap-1 text-xs cursor-pointer", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "C\u00E2u m\u1EABu (Example)"] })] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
|
|
142
|
+
points: parsePointsValue(event.target.value, true),
|
|
143
|
+
}) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(index), className: "text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `question-${index}`, children: "N\u1ED9i dung c\u00E2u h\u1ECFi" }), _jsx(Textarea, { id: `question-${index}`, value: item.question, onChange: (e) => updateItem(index, { question: e.target.value }), placeholder: "Nh\u1EADp n\u1ED9i dung c\u00E2u h\u1ECFi...", className: "mt-1 bg-white", rows: 2 })] }), _jsxs("div", { children: [_jsx(Label, { className: "mb-2 block text-xs font-medium text-gray-700", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Button, { type: "button", variant: item.isTrue === true ? 'default' : 'outline', size: "sm", onClick: () => setItemTruth(index, true), className: "w-24", children: "True" }), _jsx(Button, { type: "button", variant: item.isTrue === false ? 'default' : 'outline', size: "sm", onClick: () => setItemTruth(index, false), className: "w-24", children: "False" })] })] }), item.isTrue === false && (_jsxs("div", { children: [_jsx(Label, { htmlFor: `correction-${index}`, children: "C\u00E2u s\u1EEDa \u0111\u00FAng" }), _jsx(Textarea, { id: `correction-${index}`, value: item.correction, onChange: (e) => updateItem(index, { correction: e.target.value }), placeholder: "Vi\u1EBFt l\u1EA1i c\u00E2u \u0111\u00FAng...", className: "mt-1 bg-white", rows: 2 })] }))] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "explanation", value: explanation, onChange: (event) => setExplanation(event.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3 }) })] })] }))] }));
|
|
144
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TrueFalseCorrectGroupData, TrueFalseCorrectGroupItemData } from '../../_shared/types/true-false-correct-group.type';
|
|
2
|
+
export declare function createEmptyTrueFalseCorrectGroupItem(questionNumber: number): TrueFalseCorrectGroupItemData;
|
|
3
|
+
export declare function mapTrueFalseCorrectGroupItem(item: unknown, index: number, fallbackAnswer?: unknown): TrueFalseCorrectGroupItemData;
|
|
4
|
+
export declare function mapQuestionToTrueFalseCorrectGroupData(question: Record<string, unknown> | null | undefined): TrueFalseCorrectGroupData;
|