@tinyweb_dev/oe-exam-sdk 0.2.4 → 0.2.6
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/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +19 -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/MoversFillInBlankGroupRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
- 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/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -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 +4 -0
- package/dist/components/exams/take/components/question-renderers/index.js +4 -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 +28 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +73 -1
- package/dist/components/exams/take/utils/question-transformers.js +188 -0
- package/dist/components/questions/_shared/config/question-types.config.js +24 -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/fill-in-blank-group.type.d.ts +50 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
- package/dist/components/questions/_shared/types/index.d.ts +4 -0
- package/dist/components/questions/_shared/types/index.js +4 -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/matching-with-lines-group.type.d.ts +40 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.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 +6 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
- package/dist/components/questions/creator/question-type-registry.js +8 -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/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
- package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
- package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
- package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
- package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -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/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
- package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
- package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
- package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
- package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -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/FillInBlankGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
- 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/MatchingWithLinesGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
- package/dist/components/questions/viewer/QuestionViewer.js +24 -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 +5 -0
- package/dist/components/questions/viewer/index.js +5 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +4 -9
- package/dist/components/results/renderers/review-question-body-movers.js +74 -3
- package/dist/components/results/review-data.js +59 -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 +8 -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 +142 -0
- package/dist/shared/lib/utils/question-transform.js +8 -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/fill-in-blank-group.d.ts +45 -0
- package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
- package/dist/shared/types/questions/index.d.ts +4 -0
- package/dist/shared/types/questions/index.js +8 -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/matching-with-lines-group.d.ts +39 -0
- package/dist/shared/types/questions/matching-with-lines-group.js +1 -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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION, FILL_IN_BLANK_ITEM_TYPE, } from '../../_shared/types/fill-in-blank-group.type';
|
|
2
|
+
import { blankSlotCount, extractPlaceholderIndices, normalizeAnswersMatrix, } from './blank-utils';
|
|
3
|
+
function sumGradablePoints(items) {
|
|
4
|
+
return items.reduce((total, item) => {
|
|
5
|
+
if (item.isExample)
|
|
6
|
+
return total;
|
|
7
|
+
const points = typeof item.points === 'number' ? item.points : 1;
|
|
8
|
+
return total + points;
|
|
9
|
+
}, 0);
|
|
10
|
+
}
|
|
11
|
+
function toItemAnswer(item) {
|
|
12
|
+
const indices = extractPlaceholderIndices(item.question || '');
|
|
13
|
+
const requiredLength = blankSlotCount(item.question || '');
|
|
14
|
+
return {
|
|
15
|
+
answers: normalizeAnswersMatrix(item.answers, requiredLength).map((alts, index) => {
|
|
16
|
+
if (!indices.includes(index))
|
|
17
|
+
return [];
|
|
18
|
+
return alts.length > 0 ? alts : [''];
|
|
19
|
+
}),
|
|
20
|
+
caseSensitive: item.caseSensitive === true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export const transformFillInBlankGroup = (question) => {
|
|
24
|
+
const answerData = question.answer;
|
|
25
|
+
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
26
|
+
if (items.length === 0) {
|
|
27
|
+
return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
|
|
28
|
+
}
|
|
29
|
+
const apiItems = items.map((item, index) => {
|
|
30
|
+
const correctAnswer = toItemAnswer(item);
|
|
31
|
+
const hints = Array.isArray(item.hints)
|
|
32
|
+
? item.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
|
|
33
|
+
: [];
|
|
34
|
+
return {
|
|
35
|
+
questionType: FILL_IN_BLANK_ITEM_TYPE,
|
|
36
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
37
|
+
content: {
|
|
38
|
+
question: item.question || '',
|
|
39
|
+
...(item.imageUrl ? { imageUrl: item.imageUrl } : {}),
|
|
40
|
+
...(item.audioUrl ? { audioUrl: item.audioUrl } : {}),
|
|
41
|
+
...(item.showHints ? { showHints: true } : {}),
|
|
42
|
+
...(hints.length > 0 ? { hints } : {}),
|
|
43
|
+
},
|
|
44
|
+
correctAnswer,
|
|
45
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
46
|
+
questionNumber: item.questionNumber || index + 1,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const groupHints = Array.isArray(answerData?.hints)
|
|
50
|
+
? answerData.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
|
|
51
|
+
: [];
|
|
52
|
+
return {
|
|
53
|
+
apiContent: {
|
|
54
|
+
meta: {
|
|
55
|
+
instruction: answerData?.instruction || FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION,
|
|
56
|
+
...(answerData?.audioUrl ? { audioUrl: answerData.audioUrl } : {}),
|
|
57
|
+
...(answerData?.imageUrl ? { imageUrl: answerData.imageUrl } : {}),
|
|
58
|
+
...(answerData?.showHints ? { showHints: true } : {}),
|
|
59
|
+
...(groupHints.length > 0 ? { hints: groupHints } : {}),
|
|
60
|
+
},
|
|
61
|
+
items: apiItems,
|
|
62
|
+
},
|
|
63
|
+
apiCorrectAnswer: {
|
|
64
|
+
answer: apiItems.map((item) => item.correctAnswer),
|
|
65
|
+
},
|
|
66
|
+
totalPoints: sumGradablePoints(items),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { MatchWordToPictureClientProps } from '../../_shared/types/match-word-to-picture.type';
|
|
2
|
+
export declare function MatchWordToPictureClient({ questionData, onSubmit, isReviewMode, userAnswer, autoFillCorrectAnswers, explanation, }: MatchWordToPictureClientProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Card, CardContent } from '../../../../components/ui/card';
|
|
4
|
+
import { Label } from '../../../../components/ui/label';
|
|
5
|
+
import { BookOpen, CheckCircle2, XCircle } from 'lucide-react';
|
|
6
|
+
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
7
|
+
import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture.type';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
export function MatchWordToPictureClient({ questionData, onSubmit, isReviewMode = false, userAnswer, autoFillCorrectAnswers = false, explanation, }) {
|
|
10
|
+
const [answer, setAnswer] = useState(userAnswer || '');
|
|
11
|
+
const { previewUrl: questionImageUrl } = usePresignedFileUrl(questionData.imageUrl || '');
|
|
12
|
+
const wordBank = questionData.wordBank || [];
|
|
13
|
+
if (!questionData.imageUrl) {
|
|
14
|
+
return (_jsx(Card, { children: _jsx(CardContent, { className: "p-6", children: _jsx("p", { className: "text-gray-500", children: "Kh\u00F4ng c\u00F3 d\u1EEF li\u1EC7u c\u00E2u h\u1ECFi" }) }) }));
|
|
15
|
+
}
|
|
16
|
+
const isExample = questionData.isExample || false;
|
|
17
|
+
const displayedAnswer = autoFillCorrectAnswers || isExample
|
|
18
|
+
? questionData.correctAnswer
|
|
19
|
+
: answer;
|
|
20
|
+
const isCorrect = isReviewMode && userAnswer !== undefined
|
|
21
|
+
? userAnswer.trim().toLowerCase() === questionData.correctAnswer.trim().toLowerCase()
|
|
22
|
+
: undefined;
|
|
23
|
+
return (_jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg", children: [_jsx("div", { className: "h-1 bg-gradient-to-r from-violet-400 to-indigo-500" }), _jsxs(CardContent, { className: "space-y-6 p-6", children: [_jsx("p", { className: "text-sm font-semibold uppercase tracking-wide text-violet-700", children: questionData.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_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) => (_jsx("span", { className: "rounded-full border border-violet-200 bg-white px-3 py-1 text-sm font-semibold text-slate-700", children: word }, `${word}-${index}`))) })] })), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5", children: [isExample && (_jsxs("div", { className: "mb-2 inline-flex items-center gap-1.5 rounded-full border border-amber-300 bg-amber-100 px-3 py-1", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5 text-amber-600" }), _jsx("span", { className: "text-xs font-semibold uppercase tracking-wide text-amber-700", children: "Example" })] })), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "relative h-24 w-24 flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm", children: _jsx("img", { src: questionImageUrl || questionData.imageUrl, alt: questionData.correctAnswer || 'Picture', className: "h-full w-full object-contain" }) }), _jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Label, { className: "text-base 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: displayedAnswer, onChange: (event) => {
|
|
24
|
+
setAnswer(event.target.value);
|
|
25
|
+
onSubmit?.(event.target.value);
|
|
26
|
+
}, disabled: isReviewMode || isExample, className: `h-10 max-w-xs rounded-md border-2 bg-white px-3 text-sm font-medium ${isReviewMode
|
|
27
|
+
? isCorrect
|
|
28
|
+
? 'border-green-400 bg-green-50 text-green-700'
|
|
29
|
+
: 'border-red-400 bg-red-50 text-red-700'
|
|
30
|
+
: 'border-violet-300 focus:border-violet-500 focus:ring-violet-200'}`, children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, index) => (_jsx("option", { value: word, children: word }, `${word}-${index}`)))] }), isReviewMode && isCorrect !== undefined && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(CheckCircle2, { className: "h-5 w-5 text-green-500" })) : (_jsx(XCircle, { className: "h-5 w-5 text-red-500" })) }))] })] })] }), isReviewMode && isCorrect === false && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: questionData.correctAnswer })] })] }))] }), explanation && isReviewMode && (_jsxs("div", { className: "rounded-xl border border-blue-200 bg-blue-50 p-4", children: [_jsx("p", { className: "mb-1 text-sm font-semibold text-blue-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "text-sm text-blue-700", children: explanation })] }))] })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { Button } from '../../../../components/ui/button';
|
|
6
|
+
import { Label } from '../../../../components/ui/label';
|
|
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 { BookOpen, Eye, HelpCircle, ImageIcon, Pencil } from 'lucide-react';
|
|
11
|
+
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
12
|
+
import { Checkbox } from '../../../../components/ui/checkbox';
|
|
13
|
+
import { MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture.type';
|
|
14
|
+
import { MatchWordToPictureClient } from './MatchWordToPictureClient';
|
|
15
|
+
import { WordBankEditor } from './WordBankEditor';
|
|
16
|
+
import { useDebouncedCallback, usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
17
|
+
import { useExamQuestionStore } from '../../../../shared/lib/stores/examQuestionStore';
|
|
18
|
+
function getGroupNumberForIndex(questionIndex, groups) {
|
|
19
|
+
if (!groups || groups.length === 0) {
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
let currentIndex = 0;
|
|
23
|
+
for (const group of groups) {
|
|
24
|
+
if (questionIndex < currentIndex + group.questionCount) {
|
|
25
|
+
return group.number;
|
|
26
|
+
}
|
|
27
|
+
currentIndex += group.questionCount;
|
|
28
|
+
}
|
|
29
|
+
return groups[groups.length - 1]?.number || 1;
|
|
30
|
+
}
|
|
31
|
+
function normalizeWordBank(wordBank) {
|
|
32
|
+
if (!Array.isArray(wordBank)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return wordBank.filter((word) => typeof word === 'string');
|
|
36
|
+
}
|
|
37
|
+
function MatchWordToPictureContent({ initialData, onChange, onUnsavedChangesChange, externalErrors, groups, questionIndexInPart = 0, partId = '', validationRef, }) {
|
|
38
|
+
const setSharedPassageData = useExamQuestionStore((state) => state.setSharedPassageData);
|
|
39
|
+
const getSharedPassageData = useExamQuestionStore((state) => state.getSharedPassageData);
|
|
40
|
+
const initialGroupNum = getGroupNumberForIndex(questionIndexInPart, groups);
|
|
41
|
+
const [imageUrl, setImageUrl] = useState(initialData?.imageUrl || '');
|
|
42
|
+
const [correctAnswer, setCorrectAnswer] = useState(initialData?.correctAnswer || '');
|
|
43
|
+
const [points, setPoints] = useState(initialData?.points || 1);
|
|
44
|
+
const [isExample, setIsExample] = useState(initialData?.isExample || false);
|
|
45
|
+
const [groupNumber, setGroupNumber] = useState(initialData?.groupNumber || initialGroupNum);
|
|
46
|
+
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
47
|
+
const [wordBank, setWordBank] = useState(() => {
|
|
48
|
+
if (partId) {
|
|
49
|
+
const storedData = getSharedPassageData(partId, initialGroupNum);
|
|
50
|
+
if (Array.isArray(storedData?.wordBank)) {
|
|
51
|
+
return storedData.wordBank;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return initialData?.wordBank || [];
|
|
55
|
+
});
|
|
56
|
+
const [instruction, setInstruction] = useState(() => {
|
|
57
|
+
if (partId) {
|
|
58
|
+
const storedData = getSharedPassageData(partId, initialGroupNum);
|
|
59
|
+
if (storedData?.instruction) {
|
|
60
|
+
return storedData.instruction;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return initialData?.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
|
|
64
|
+
});
|
|
65
|
+
const [isClientMode, setIsClientMode] = useState(false);
|
|
66
|
+
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
67
|
+
const [imagePreviewUrl, setImagePreviewUrl] = useState('');
|
|
68
|
+
const [validationErrors, setValidationErrors] = useState({});
|
|
69
|
+
const savedStateRef = useRef('');
|
|
70
|
+
const previousQuestionIndexRef = useRef(questionIndexInPart);
|
|
71
|
+
const isInitializingRef = useRef(false);
|
|
72
|
+
const hasInitializedRef = useRef(false);
|
|
73
|
+
const onChangeRef = useRef(onChange);
|
|
74
|
+
const previousPayloadRef = useRef('');
|
|
75
|
+
const onUnsavedChangesChangeRef = useRef(onUnsavedChangesChange);
|
|
76
|
+
const currentGroupNumber = groupNumber || 1;
|
|
77
|
+
const { previewUrl: generatedPreviewUrl } = usePresignedFileUrl(imageUrl);
|
|
78
|
+
const displayPreviewUrl = imagePreviewUrl || generatedPreviewUrl || '';
|
|
79
|
+
const usableWords = wordBank.map((word) => word.trim()).filter((word) => word.length > 0);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
onUnsavedChangesChangeRef.current = onUnsavedChangesChange;
|
|
82
|
+
}, [onUnsavedChangesChange]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
onChangeRef.current = onChange;
|
|
85
|
+
}, [onChange]);
|
|
86
|
+
const debouncedOnChange = useDebouncedCallback((data) => {
|
|
87
|
+
onChangeRef.current?.(data);
|
|
88
|
+
}, 300);
|
|
89
|
+
const getCurrentStateString = () => JSON.stringify({ imageUrl, correctAnswer, points, isExample, wordBank, instruction });
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (isInitializingRef.current)
|
|
92
|
+
return;
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
savedStateRef.current = getCurrentStateString();
|
|
95
|
+
setHasUnsavedChanges(false);
|
|
96
|
+
}, 150);
|
|
97
|
+
return () => clearTimeout(timer);
|
|
98
|
+
}, [questionIndexInPart]);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (isInitializingRef.current || !savedStateRef.current)
|
|
101
|
+
return;
|
|
102
|
+
const hasChanges = getCurrentStateString() !== savedStateRef.current;
|
|
103
|
+
if (hasChanges !== hasUnsavedChanges) {
|
|
104
|
+
setHasUnsavedChanges(hasChanges);
|
|
105
|
+
onUnsavedChangesChangeRef.current?.(hasChanges);
|
|
106
|
+
}
|
|
107
|
+
}, [imageUrl, correctAnswer, points, isExample, wordBank, instruction, hasUnsavedChanges]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!validationRef)
|
|
110
|
+
return;
|
|
111
|
+
validationRef.current = {
|
|
112
|
+
triggerValidation: async () => {
|
|
113
|
+
const errors = {};
|
|
114
|
+
let isValid = true;
|
|
115
|
+
if (!imageUrl.trim()) {
|
|
116
|
+
errors.imageUrl = 'Vui lòng upload hình ảnh';
|
|
117
|
+
isValid = false;
|
|
118
|
+
}
|
|
119
|
+
if (usableWords.length === 0) {
|
|
120
|
+
errors.wordBank = 'Vui lòng thêm word bank';
|
|
121
|
+
isValid = false;
|
|
122
|
+
}
|
|
123
|
+
if (!correctAnswer.trim()) {
|
|
124
|
+
errors.correctAnswer = 'Vui lòng chọn đáp án';
|
|
125
|
+
isValid = false;
|
|
126
|
+
}
|
|
127
|
+
else if (usableWords.length > 0 &&
|
|
128
|
+
!usableWords.some((word) => word.toLowerCase() === correctAnswer.trim().toLowerCase())) {
|
|
129
|
+
errors.correctAnswer = 'Đáp án phải nằm trong word bank';
|
|
130
|
+
isValid = false;
|
|
131
|
+
}
|
|
132
|
+
setValidationErrors(errors);
|
|
133
|
+
return isValid;
|
|
134
|
+
},
|
|
135
|
+
getFormData: () => ({
|
|
136
|
+
imageUrl,
|
|
137
|
+
correctAnswer,
|
|
138
|
+
points,
|
|
139
|
+
groupNumber: currentGroupNumber,
|
|
140
|
+
isExample,
|
|
141
|
+
wordBank,
|
|
142
|
+
instruction,
|
|
143
|
+
explanation,
|
|
144
|
+
groups,
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}, [
|
|
148
|
+
validationRef,
|
|
149
|
+
imageUrl,
|
|
150
|
+
correctAnswer,
|
|
151
|
+
points,
|
|
152
|
+
currentGroupNumber,
|
|
153
|
+
isExample,
|
|
154
|
+
wordBank,
|
|
155
|
+
instruction,
|
|
156
|
+
explanation,
|
|
157
|
+
groups,
|
|
158
|
+
usableWords,
|
|
159
|
+
]);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const questionIndexChanged = questionIndexInPart !== previousQuestionIndexRef.current;
|
|
162
|
+
const shouldSyncForFirstInit = !hasInitializedRef.current && !!initialData?.imageUrl;
|
|
163
|
+
if (!shouldSyncForFirstInit && !questionIndexChanged)
|
|
164
|
+
return;
|
|
165
|
+
isInitializingRef.current = true;
|
|
166
|
+
previousQuestionIndexRef.current = questionIndexInPart;
|
|
167
|
+
hasInitializedRef.current = true;
|
|
168
|
+
const groupNum = getGroupNumberForIndex(questionIndexInPart, groups);
|
|
169
|
+
setImageUrl(initialData?.imageUrl || '');
|
|
170
|
+
setCorrectAnswer(initialData?.correctAnswer || '');
|
|
171
|
+
setPoints(initialData?.points || 1);
|
|
172
|
+
setIsExample(initialData?.isExample || false);
|
|
173
|
+
setGroupNumber(groupNum);
|
|
174
|
+
setExplanation(initialData?.explanation || '');
|
|
175
|
+
let sharedWordBank = [];
|
|
176
|
+
let sharedInstruction = MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
|
|
177
|
+
if (partId) {
|
|
178
|
+
const storedData = getSharedPassageData(partId, groupNum);
|
|
179
|
+
if (Array.isArray(storedData?.wordBank)) {
|
|
180
|
+
sharedWordBank = storedData.wordBank;
|
|
181
|
+
sharedInstruction = storedData.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
sharedWordBank = initialData?.wordBank || [];
|
|
185
|
+
sharedInstruction = initialData?.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
|
|
186
|
+
if (sharedWordBank.length > 0 || sharedInstruction) {
|
|
187
|
+
setSharedPassageData(partId, groupNum, {
|
|
188
|
+
wordBank: sharedWordBank,
|
|
189
|
+
instruction: sharedInstruction,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
sharedWordBank = initialData?.wordBank || [];
|
|
196
|
+
sharedInstruction = initialData?.instruction || MATCH_WORD_TO_PICTURE_DEFAULT_INSTRUCTION;
|
|
197
|
+
}
|
|
198
|
+
setWordBank(normalizeWordBank(sharedWordBank));
|
|
199
|
+
setInstruction(sharedInstruction);
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
isInitializingRef.current = false;
|
|
202
|
+
}, 100);
|
|
203
|
+
}, [initialData, groups, questionIndexInPart, partId, getSharedPassageData, setSharedPassageData]);
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (isInitializingRef.current)
|
|
206
|
+
return;
|
|
207
|
+
const payload = {
|
|
208
|
+
imageUrl,
|
|
209
|
+
correctAnswer,
|
|
210
|
+
points: points || 1,
|
|
211
|
+
groupNumber: currentGroupNumber,
|
|
212
|
+
isExample,
|
|
213
|
+
wordBank,
|
|
214
|
+
instruction,
|
|
215
|
+
explanation,
|
|
216
|
+
groups,
|
|
217
|
+
};
|
|
218
|
+
const payloadString = JSON.stringify(payload);
|
|
219
|
+
if (previousPayloadRef.current === payloadString)
|
|
220
|
+
return;
|
|
221
|
+
previousPayloadRef.current = payloadString;
|
|
222
|
+
if (partId) {
|
|
223
|
+
setSharedPassageData(partId, currentGroupNumber, { wordBank, instruction });
|
|
224
|
+
}
|
|
225
|
+
debouncedOnChange(payload);
|
|
226
|
+
}, [
|
|
227
|
+
imageUrl,
|
|
228
|
+
correctAnswer,
|
|
229
|
+
points,
|
|
230
|
+
currentGroupNumber,
|
|
231
|
+
isExample,
|
|
232
|
+
wordBank,
|
|
233
|
+
instruction,
|
|
234
|
+
explanation,
|
|
235
|
+
groups,
|
|
236
|
+
partId,
|
|
237
|
+
setSharedPassageData,
|
|
238
|
+
debouncedOnChange,
|
|
239
|
+
]);
|
|
240
|
+
if (isClientMode) {
|
|
241
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { children: _jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: "Match word to picture (Preview)" }), _jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Ch\u1EBF \u0111\u1ED9 xem tr\u01B0\u1EDBc - Hi\u1EC3n th\u1ECB \u0111\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Quay l\u1EA1i Edit Mode"] })] }) }) }), _jsx(MatchWordToPictureClient, { questionData: {
|
|
242
|
+
imageUrl: displayPreviewUrl || imageUrl,
|
|
243
|
+
correctAnswer,
|
|
244
|
+
points,
|
|
245
|
+
groupNumber: currentGroupNumber,
|
|
246
|
+
isExample,
|
|
247
|
+
wordBank: usableWords,
|
|
248
|
+
instruction,
|
|
249
|
+
}, isReviewMode: true, autoFillCorrectAnswers: true, explanation: explanation })] }));
|
|
250
|
+
}
|
|
251
|
+
return (_jsxs("div", { className: "space-y-4", children: [groups && groups.length > 0 && (_jsx(WordBankEditor, { wordBank: wordBank, instruction: instruction, onWordBankChange: setWordBank, onInstructionChange: setInstruction })), _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 justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { className: "text-base font-bold text-gray-900", children: "N\u1ED1i t\u1EEB v\u1EDBi tranh" }), _jsx("p", { className: "text-xs text-gray-500", children: "Upload tranh v\u00E0 ch\u1ECDn t\u1EEB trong word bank" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setIsClientMode(true), disabled: !imageUrl, className: "gap-2 border-violet-200 text-violet-600 hover:bg-violet-50", children: [_jsx(Eye, { className: "h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }) }), _jsxs(CardContent, { className: "space-y-4 px-4 pb-4", children: [_jsxs("div", { className: "flex items-center gap-6", children: [_jsx(PointsInput, { value: points || 1, onChange: (e) => setPoints(parseFloat(e.target.value) || 1) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "mwtp-isExample", checked: isExample || false, onCheckedChange: (checked) => setIsExample(!!checked) }), _jsx(Label, { htmlFor: "mwtp-isExample", className: "cursor-pointer text-sm font-medium text-gray-700", children: "\u0110\u00E1nh d\u1EA5u l\u00E0 c\u00E2u h\u1ECFi Example" })] })] }), (!groups || groups.length === 0) && (_jsx(WordBankEditor, { wordBank: wordBank, instruction: instruction, onWordBankChange: setWordBank, onInstructionChange: setInstruction })), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(ImageIcon, { className: "h-4 w-4 text-violet-500" }), "H\u00ECnh \u1EA3nh ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(FileUpload, { id: "mwtp-question-image", label: "", accept: "image/*", value: imageUrl || '', onChange: (url) => {
|
|
252
|
+
setImageUrl(url);
|
|
253
|
+
if (validationErrors.imageUrl) {
|
|
254
|
+
setValidationErrors((prev) => ({ ...prev, imageUrl: undefined }));
|
|
255
|
+
}
|
|
256
|
+
}, onPresignedUrlChange: setImagePreviewUrl, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), displayPreviewUrl && (_jsx(ImagePreview, { src: displayPreviewUrl, alt: "Preview", className: "mt-2 h-48 w-full max-w-md rounded-lg border border-gray-200" })), validationErrors.imageUrl && _jsx("p", { className: "text-sm text-red-600", children: validationErrors.imageUrl })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: "mwtp-correctAnswer", className: "flex items-center gap-2 text-base font-semibold text-gray-800", children: [_jsx(HelpCircle, { className: "h-4 w-4 text-green-500" }), "\u0110\u00E1p \u00E1n \u0111\u00FAng ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsxs("select", { id: "mwtp-correctAnswer", value: correctAnswer, onChange: (event) => {
|
|
257
|
+
setCorrectAnswer(event.target.value);
|
|
258
|
+
if (validationErrors.correctAnswer) {
|
|
259
|
+
setValidationErrors((prev) => ({ ...prev, correctAnswer: undefined }));
|
|
260
|
+
}
|
|
261
|
+
}, className: `flex h-10 w-full rounded-md border bg-white px-3 py-2 text-sm ${validationErrors.correctAnswer
|
|
262
|
+
? 'border-red-400 focus:border-red-500'
|
|
263
|
+
: 'border-gray-200 focus:border-green-300'}`, children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB trong word bank --" }), usableWords.map((word, index) => (_jsx("option", { value: word, children: word }, `${word}-${index}`)))] }), validationErrors.correctAnswer && (_jsx("p", { className: "text-sm text-red-600", children: validationErrors.correctAnswer })), validationErrors.wordBank && _jsx("p", { className: "text-sm text-red-600", children: validationErrors.wordBank })] }), externalErrors && externalErrors.length > 0 && (_jsxs("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: [_jsx("p", { className: "font-semibold text-red-800", children: "L\u1ED7i:" }), _jsx("ul", { className: "mt-1 list-inside list-disc", children: externalErrors.map((error, index) => (_jsx("li", { children: error }, index))) })] }))] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-lg shadow-amber-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-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 shadow-sm", children: _jsx(BookOpen, { className: "h-3.5 w-3.5 text-white" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CardTitle, { className: "text-base font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" }), _jsx("span", { className: "inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-600", children: "Kh\u00F4ng b\u1EAFt bu\u1ED9c" })] }), _jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: "Gi\u1EA3i th\u00EDch cho \u0111\u00E1p \u00E1n \u0111\u00FAng \u2014 hi\u1EC3n th\u1ECB sau khi h\u1ECDc sinh ho\u00E0n th\u00E0nh c\u00E2u h\u1ECFi" })] })] }) }), _jsx(CardContent, { className: "px-4 pb-4", children: _jsx(Textarea, { id: "mwtp-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, className: "min-h-[80px] resize-none border-gray-200 bg-white transition-all focus:border-amber-300 focus:ring-amber-200" }) })] })] }));
|
|
264
|
+
}
|
|
265
|
+
export function MatchWordToPictureCreator(props) {
|
|
266
|
+
return _jsx(MatchWordToPictureContent, { ...props });
|
|
267
|
+
}
|
package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { MatchWordToPictureGroupClientProps } from '../../_shared/types/match-word-to-picture.type';
|
|
2
|
+
export declare function MatchWordToPictureGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: MatchWordToPictureGroupClientProps): import("react").JSX.Element;
|
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;
|