@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.11
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 +7 -7
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.js +36 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +4 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
- package/dist/components/exams/take/utils/answer-transformers.js +17 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +32 -3
- package/dist/components/exams/take/utils/question-transformers.js +51 -2
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +14 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +2 -2
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
- package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
- package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
- package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
- package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
- package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
- package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
- package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +24 -35
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +16 -3
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +2 -1
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +25 -6
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +1 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +22 -10
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +60 -18
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +2 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +24 -5
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +8 -4
- package/dist/components/questions/types/matching-with-lines-group/transform.js +5 -4
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
- package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
- package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +21 -23
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
- package/dist/components/questions/viewer/QuestionViewer.js +2 -2
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/results/renderers/review-question-body-movers.js +7 -3
- package/dist/components/results/review-data.js +20 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +48 -7
- package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +2 -2
- package/package.json +1 -1
package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
-
import { Check, X,
|
|
4
|
+
import { Check, X, Lightbulb, ClipboardList, Users, BookOpen } from 'lucide-react';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
7
|
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
8
8
|
import { isRichTextEmpty, richTextToPlainText } from '../../../../shared/lib/rich-text';
|
|
9
9
|
import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, optionToString } from './normalize';
|
|
10
|
+
import { KidChoiceRow, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
|
|
10
11
|
// Helper component to resolve presigned URL for each image option
|
|
11
12
|
function OptionImage({ src, alt, className }) {
|
|
12
13
|
const { previewUrl, isLoading } = usePresignedFileUrl(src);
|
|
@@ -110,41 +111,19 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
|
|
|
110
111
|
.split('\n')
|
|
111
112
|
.map((line) => `<p class="last:mb-0">${line}</p>`)
|
|
112
113
|
.join(''),
|
|
113
|
-
} })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
? 'border-green-400 bg-green-50'
|
|
127
|
-
: isSelected
|
|
128
|
-
? 'border-red-400 bg-red-50'
|
|
129
|
-
: 'border-gray-200 bg-gray-50'
|
|
130
|
-
: isSelected
|
|
131
|
-
? 'border-blue-500 bg-blue-50 shadow-sm ring-2 ring-blue-100'
|
|
132
|
-
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm'), children: [_jsx("div", { className: cn('flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-bold transition-all', isReviewMode
|
|
133
|
-
? isCorrectAnswer
|
|
134
|
-
? 'bg-green-500 text-white'
|
|
135
|
-
: isSelected
|
|
136
|
-
? 'bg-red-500 text-white'
|
|
137
|
-
: 'bg-gray-200 text-gray-600'
|
|
138
|
-
: isSelected
|
|
139
|
-
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm'
|
|
140
|
-
: 'bg-gray-100 text-gray-600 group-hover:bg-gray-200'), children: letter }), questionData.optionType === 'image' ? (_jsx("div", { className: "flex-1 flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx(RichTextHtml, { html: optionText, className: cn('flex-1 text-sm font-medium', isReviewMode && isCorrectAnswer
|
|
141
|
-
? 'text-green-800'
|
|
142
|
-
: isReviewMode && isSelected && !isCorrectAnswer
|
|
143
|
-
? 'text-red-800'
|
|
144
|
-
: 'text-gray-700') })), isReviewMode && (isCorrectAnswer || isSelected) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', isCorrectAnswer ? 'bg-green-500' : 'bg-red-500'), children: isCorrectAnswer
|
|
145
|
-
? _jsx(Check, { className: "h-4 w-4 text-white" })
|
|
146
|
-
: _jsx(X, { className: "h-4 w-4 text-white" }) }))] }, index));
|
|
147
|
-
})] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: isMultipleAnswers ? 'Vui lòng chọn ít nhất một đáp án' : 'Vui lòng chọn một đáp án' })] }))] })] }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2", children: [_jsx(Lightbulb, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600" }), _jsxs("p", { className: "text-xs text-gray-700", children: [_jsx("span", { className: "font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch: " }), questionData.explanation] })] })), isReviewMode && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-slate-500 to-gray-600 text-white", children: _jsx(ClipboardList, { className: "h-3.5 w-3.5" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "T\u1ED5ng k\u1EBFt" })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-gray-50 p-3", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n c\u1EE7a b\u1EA1n:" }), _jsx("span", { className: cn('text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : hasAnswered ? 'text-red-600' : 'text-gray-500'), children: formatAnswerLabel(selectedAnswer) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-lg bg-green-50 p-3 border border-green-100", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-green-600", children: formatAnswerLabel(normalizedCorrectAnswers) })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-lg p-3 border', isAnswerCorrect
|
|
114
|
+
} })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs(KidQuestionCard, { title: isMultipleAnswers ? 'Chọn các đáp án đúng' : 'Chọn đáp án đúng', points: questionData.points, review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx(KidPrompt, { children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-sm font-semibold text-gray-800" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
|
|
115
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
116
|
+
_jsx("img", { src: imagePreviewUrl || cleanImageUrl, alt: "Question illustration", className: "max-h-56 w-auto object-contain rounded-md", onError: (e) => {
|
|
117
|
+
const target = e.target;
|
|
118
|
+
target.onerror = null;
|
|
119
|
+
target.src = '/images/placeholder-image.svg';
|
|
120
|
+
} })) })] })), showOptions && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("span", { className: "text-xs font-medium text-gray-500 uppercase tracking-wider", children: isMultipleAnswers ? 'Chọn một hoặc nhiều đáp án' : 'Chọn đáp án' }), displayOptions.map((option, index) => {
|
|
121
|
+
const isSelected = selectedAnswer.includes(index);
|
|
122
|
+
const isCorrectAnswer = isCorrect(index);
|
|
123
|
+
const letter = optionLetter(index);
|
|
124
|
+
const optionText = optionToString(option);
|
|
125
|
+
return (_jsx(KidChoiceRow, { letter: letter, selected: isSelected, isReviewMode: isReviewMode, isCorrect: isCorrectAnswer, onClick: () => handleAnswerChange(index), children: questionData.optionType === 'image' ? (_jsx("div", { className: "flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx(RichTextHtml, { html: optionText })) }, index));
|
|
126
|
+
})] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: isMultipleAnswers ? 'Vui lòng chọn ít nhất một đáp án' : 'Vui lòng chọn một đáp án' })] }))] }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2", children: [_jsx(Lightbulb, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600" }), _jsxs("p", { className: "text-xs text-gray-700", children: [_jsx("span", { className: "font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch: " }), questionData.explanation] })] })), isReviewMode && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-slate-500 to-gray-600 text-white", children: _jsx(ClipboardList, { className: "h-3.5 w-3.5" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "T\u1ED5ng k\u1EBFt" })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-gray-50 p-3", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n c\u1EE7a b\u1EA1n:" }), _jsx("span", { className: cn('text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : hasAnswered ? 'text-red-600' : 'text-gray-500'), children: formatAnswerLabel(selectedAnswer) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-lg bg-green-50 p-3 border border-green-100", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-green-600", children: formatAnswerLabel(normalizedCorrectAnswers) })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-lg p-3 border', isAnswerCorrect
|
|
148
127
|
? 'bg-green-50 border-green-200'
|
|
149
128
|
: 'bg-red-50 border-red-200'), children: [_jsx("span", { className: "text-sm text-gray-600", children: "K\u1EBFt qu\u1EA3:" }), _jsx("div", { className: cn('flex items-center gap-1.5 text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : 'text-red-600'), children: isAnswerCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4" }), _jsx("span", { children: "Ch\u00EDnh x\u00E1c" })] })) : (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-4 w-4" }), _jsx("span", { children: "Sai" })] })) })] })] })] }))] }));
|
|
150
129
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen,
|
|
4
|
-
import { cn } from '../../../../shared/lib/utils';
|
|
3
|
+
import { BookOpen, Lightbulb, Volume2 } from 'lucide-react';
|
|
5
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
5
|
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
7
6
|
import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
|
|
8
7
|
import { getChooseAnswerGroupOptionImageSrc, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
8
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
|
|
9
9
|
function ClientImageItem({ url, alt }) {
|
|
10
10
|
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
11
11
|
const src = previewUrl ||
|
|
@@ -24,21 +24,6 @@ function ClientAudio({ url }) {
|
|
|
24
24
|
}
|
|
25
25
|
return (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-indigo-100 bg-indigo-50/70 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-indigo-600" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-indigo-100" })) : (_jsx("audio", { controls: true, className: "h-9 w-full", src: src, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
|
|
26
26
|
}
|
|
27
|
-
function optionLetter(index) {
|
|
28
|
-
return String.fromCharCode(65 + index);
|
|
29
|
-
}
|
|
30
|
-
function reviewOptionTone(isReviewMode, isCorrect, isSelected) {
|
|
31
|
-
if (isReviewMode && isCorrect) {
|
|
32
|
-
return { border: 'border-green-400 bg-green-50', badge: 'bg-green-500 text-white' };
|
|
33
|
-
}
|
|
34
|
-
if (isReviewMode && isSelected) {
|
|
35
|
-
return { border: 'border-red-400 bg-red-50', badge: 'bg-red-500 text-white' };
|
|
36
|
-
}
|
|
37
|
-
if (isSelected) {
|
|
38
|
-
return { border: 'border-blue-400 bg-blue-50', badge: 'bg-blue-500 text-white' };
|
|
39
|
-
}
|
|
40
|
-
return { border: 'border-gray-200 bg-white', badge: 'bg-gray-100 text-gray-600' };
|
|
41
|
-
}
|
|
42
27
|
export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
43
28
|
const items = questionData.items || [];
|
|
44
29
|
const handleSelect = (itemIndex, optionId) => {
|
|
@@ -50,20 +35,14 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
|
|
|
50
35
|
next[itemIndex] = optionId;
|
|
51
36
|
onAnswerChange(next);
|
|
52
37
|
};
|
|
53
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
38
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), toChooseAnswerGroupImageUrls(questionData.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `group-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "overflow-x-auto", children: _jsx(RichTextHtml, { html: questionData.passage, className: "text-gray-800 leading-relaxed" }) })] })), items.map((item, itemIndex) => {
|
|
54
39
|
const selectedId = userAnswers[itemIndex];
|
|
55
40
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
56
|
-
return (_jsxs(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}) })) : (_jsx("div", { className: "space-y-2", children: item.options.map((option, optionIndex) => {
|
|
63
|
-
const isCorrect = option.id === item.correctAnswer;
|
|
64
|
-
const isSelected = option.id === selectedId;
|
|
65
|
-
const tone = reviewOptionTone(isReviewMode, isCorrect, isSelected);
|
|
66
|
-
return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, option.id), className: cn('flex w-full items-center gap-3 rounded-lg border-2 px-3 py-2.5 text-sm text-left', tone.border, canSelect && 'cursor-pointer hover:border-blue-300', !canSelect && 'cursor-default'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', tone.badge), children: optionLetter(optionIndex) }), isRichTextEmpty(option.text) ? (_jsx("span", { className: "flex-1 text-gray-800", children: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx(RichTextHtml, { html: option.text, className: "flex-1 text-gray-800" })), isReviewMode && isCorrect ? (_jsx(Check, { className: "h-4 w-4 text-green-600" })) : isReviewMode && isSelected ? (_jsx(X, { className: "h-4 w-4 text-red-600" })) : null] }, option.id || optionIndex));
|
|
67
|
-
}) }))] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
41
|
+
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, children: [!isRichTextEmpty(item.question) && (_jsx(KidPrompt, { children: _jsx(RichTextHtml, { html: item.question, className: "text-sm font-semibold leading-relaxed text-gray-800" }) })), toChooseAnswerGroupImageUrls(item.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `${item.questionNumber}-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx("div", { className: "space-y-1.5", children: item.options.map((option, optionIndex) => {
|
|
42
|
+
const isCorrect = option.id === item.correctAnswer;
|
|
43
|
+
const isSelected = option.id === selectedId;
|
|
44
|
+
const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
|
|
45
|
+
return (_jsx(KidChoiceRow, { letter: optionLetter(optionIndex), selected: isSelected, isReviewMode: isReviewMode, isCorrect: isReviewMode && isCorrect, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, option.id) : undefined, children: item.optionType === 'image' ? (optionImageSrc ? (_jsx(ClientImageItem, { url: optionImageSrc, alt: `Đáp án ${optionLetter(optionIndex)}` })) : (`Đáp án ${optionLetter(optionIndex)}`)) : isRichTextEmpty(option.text) ? (`Đáp án ${optionLetter(optionIndex)}`) : (_jsx(RichTextHtml, { html: option.text })) }, option.id || optionIndex));
|
|
46
|
+
}) })] }, `${item.questionNumber}-${itemIndex}`));
|
|
68
47
|
}), 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] }))] }));
|
|
69
48
|
}
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Check, Lightbulb,
|
|
3
|
+
import { Check, Lightbulb, X } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
5
6
|
function emptyAnswer() {
|
|
6
7
|
return { optionId: '', text: '' };
|
|
7
8
|
}
|
|
@@ -35,7 +36,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
35
36
|
const items = questionData.items || [];
|
|
36
37
|
const hasAttempt = Boolean(itemResults?.length) ||
|
|
37
38
|
userAnswers.some((answer) => Boolean(answer?.optionId || answer?.text?.trim()));
|
|
38
|
-
return (_jsxs("div", { className: "space-y-2", children: [(questionData.title || questionData.instruction) && (_jsxs(
|
|
39
|
+
return (_jsxs("div", { className: "space-y-2", children: [(questionData.title || questionData.instruction) && (_jsxs(KidInstruction, { children: [questionData.title ? (_jsx("p", { className: "mb-1 text-[11px] font-semibold text-indigo-500", children: questionData.title })) : null, questionData.instruction] })), items.map((item, itemIndex) => {
|
|
39
40
|
const stored = userAnswers[itemIndex] || emptyAnswer();
|
|
40
41
|
const current = item.isExample
|
|
41
42
|
? { optionId: item.optionId, text: stored.text }
|
|
@@ -61,7 +62,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
61
62
|
text: next.text ?? current.text,
|
|
62
63
|
});
|
|
63
64
|
};
|
|
64
|
-
return (_jsx(
|
|
65
|
+
return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: points, isExample: item.isExample, children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: cn('mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white', badgeClass({
|
|
65
66
|
isExample: item.isExample,
|
|
66
67
|
isReviewMode,
|
|
67
68
|
hasAttempt,
|
|
@@ -69,7 +70,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
69
70
|
partialScore: result?.partialScore,
|
|
70
71
|
mappingCorrect,
|
|
71
72
|
hasText: hasStudentText,
|
|
72
|
-
})), children: item.questionNumber || itemIndex + 1 }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [
|
|
73
|
+
})), children: item.questionNumber || itemIndex + 1 }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsxs("p", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-1.5 text-sm font-semibold leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "inline-flex items-center gap-1", children: item.options.map((option, optionIndex) => {
|
|
73
74
|
const selected = current.optionId === option.id;
|
|
74
75
|
const isKey = option.id === expectedId;
|
|
75
76
|
const pickedCorrect = showGradedReview && selected && isKey;
|
|
@@ -78,9 +79,9 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
78
79
|
const officialCorrect = showAnswerKey && isKey;
|
|
79
80
|
return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [optionIndex > 0 && (_jsx("span", { className: "text-sm font-medium text-slate-300", children: "|" })), _jsxs("button", { type: "button", disabled: !canEdit || item.isExample, onClick: () => setAnswer({ optionId: option.id }), className: cn('inline-flex min-w-[3.5rem] items-center justify-center gap-1 rounded-full border px-2.5 py-1 text-sm font-semibold transition-colors', !showReview &&
|
|
80
81
|
selected &&
|
|
81
|
-
'border-
|
|
82
|
+
'border-blue-500 bg-blue-500 text-white shadow-sm ring-2 ring-blue-100', !showReview &&
|
|
82
83
|
!selected &&
|
|
83
|
-
'border-
|
|
84
|
+
'border-gray-200 bg-white text-slate-800 hover:border-blue-300 hover:bg-blue-50', showAnswerKey &&
|
|
84
85
|
!isKey &&
|
|
85
86
|
'border-slate-200 bg-white text-slate-800', officialCorrect &&
|
|
86
87
|
'border-2 border-emerald-500 bg-white text-slate-800', pickedCorrect && 'border-emerald-600 bg-emerald-600 text-white', pickedWrong && 'border-red-500 bg-red-500 text-white', keyOnly && 'border-emerald-500 bg-white text-emerald-800', showGradedReview &&
|
|
@@ -93,6 +94,6 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
93
94
|
? 'bg-emerald-50 text-emerald-700'
|
|
94
95
|
: 'bg-red-50 text-red-700'), children: ["Ch\u1ECDn ", mappingCorrect ? halfLabel : '0'] }), result?.aiCorrect !== undefined ? (_jsxs("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', result.aiCorrect
|
|
95
96
|
? 'bg-emerald-50 text-emerald-700'
|
|
96
|
-
: 'bg-red-50 text-red-700'), children: ["Vi\u1EBFt ", result.aiCorrect ? halfLabel : '0'] })) : null] })) : null, canEdit || hasStudentText || (isReviewMode && hasAttempt) ? (_jsxs("label", { className: "block space-y-1", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "
|
|
97
|
+
: 'bg-red-50 text-red-700'), children: ["Vi\u1EBFt ", result.aiCorrect ? halfLabel : '0'] })) : null] })) : null, canEdit || hasStudentText || (isReviewMode && hasAttempt) ? (_jsxs("label", { className: "block space-y-1", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "C\u00E2u tr\u1EA3 l\u1EDDi c\u1EE7a b\u1EA1n" }), isReviewMode && !hasStudentText ? (_jsx("p", { className: "rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-2 text-sm italic text-slate-400", children: "Ch\u01B0a vi\u1EBFt" })) : (_jsx("textarea", { value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: "Vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y...", rows: 2, className: "min-h-0 w-full resize-none rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed text-slate-800 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 disabled:bg-slate-50" }))] })) : null, isReviewMode && sampleAnswers.length > 0 ? (_jsxs("p", { className: "text-xs text-slate-500", children: [_jsx("span", { className: "font-semibold", children: "M\u1EABu:" }), " ", sampleAnswers.join(' · ')] })) : null] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
97
98
|
}), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
|
98
99
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
5
|
+
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
6
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
7
|
function normalizeWord(word) {
|
|
9
8
|
return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
|
|
10
9
|
}
|
|
@@ -21,6 +20,14 @@ function formatAnswer(answer) {
|
|
|
21
20
|
return 'câu đúng (không gạch)';
|
|
22
21
|
return `gạch "${answer.crossOut}"${posInfo}`;
|
|
23
22
|
}
|
|
23
|
+
function isCorrectToken(correct, tokenIndex, plain) {
|
|
24
|
+
if (!correct)
|
|
25
|
+
return false;
|
|
26
|
+
if (typeof correct.wordIndex === 'number' && correct.wordIndex >= 0) {
|
|
27
|
+
return correct.wordIndex === tokenIndex;
|
|
28
|
+
}
|
|
29
|
+
return Boolean(correct.crossOut) && normalizeWord(correct.crossOut) === plain && plain !== '';
|
|
30
|
+
}
|
|
24
31
|
export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
25
32
|
const items = questionData.items || [];
|
|
26
33
|
const emit = (itemIndex, nextItem) => {
|
|
@@ -32,47 +39,58 @@ export function CrossOutWordGroupClient({ questionData, isReviewMode = false, us
|
|
|
32
39
|
next[itemIndex] = nextItem;
|
|
33
40
|
onAnswerChange(next);
|
|
34
41
|
};
|
|
35
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
42
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => {
|
|
36
43
|
const isReplace = item.type === 'CROSS_OUT_AND_REPLACE';
|
|
37
44
|
const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
|
|
38
45
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
39
|
-
const tokens =
|
|
46
|
+
const tokens = tokenizeCrossOutQuestion(item.question || '');
|
|
40
47
|
const selectedIndex = userValue?.wordIndex;
|
|
41
48
|
const selectedWord = userValue?.crossOut || '';
|
|
42
49
|
const replaceWith = userValue?.replaceWith || '';
|
|
43
50
|
const correct = item.correctAnswer;
|
|
44
|
-
return (_jsxs(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, headerExtra: isReviewMode ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
|
|
52
|
+
? 'bg-violet-100 text-violet-700'
|
|
53
|
+
: 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })) : null, children: [isReplace ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-semibold text-slate-600", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: !canSelect, onChange: (event) => emit(itemIndex, {
|
|
54
|
+
type: item.type,
|
|
55
|
+
wordIndex: selectedIndex ?? null,
|
|
56
|
+
crossOut: selectedWord,
|
|
57
|
+
replaceWith: event.target.value,
|
|
58
|
+
}), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 min-w-[10rem] rounded-lg border border-slate-200 px-2.5 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 disabled:bg-slate-50" })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-sm font-medium leading-relaxed text-slate-800", children: tokens.map((token, tokenIndex) => {
|
|
59
|
+
const plain = normalizeWord(token);
|
|
60
|
+
const isSelected = typeof selectedIndex === 'number'
|
|
61
|
+
? selectedIndex === tokenIndex
|
|
62
|
+
: Boolean(selectedWord) && normalizeWord(selectedWord) === plain && plain !== '';
|
|
63
|
+
const isCorrectTarget = isReviewMode && !item.isExample && correct?.wordIndex === tokenIndex;
|
|
64
|
+
const isSelectedCorrect = isReviewMode &&
|
|
65
|
+
!item.isExample &&
|
|
66
|
+
isSelected &&
|
|
67
|
+
isCorrectToken(correct, tokenIndex, plain);
|
|
68
|
+
const isSelectedWrong = isReviewMode &&
|
|
69
|
+
!item.isExample &&
|
|
70
|
+
isSelected &&
|
|
71
|
+
Boolean(correct) &&
|
|
72
|
+
!isSelectedCorrect;
|
|
73
|
+
return (_jsx("button", { type: "button", disabled: !canSelect || plain === '', onClick: () => {
|
|
74
|
+
const already = typeof selectedIndex === 'number'
|
|
54
75
|
? selectedIndex === tokenIndex
|
|
55
|
-
:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...(isReplace ? { replaceWith } : {}),
|
|
68
|
-
});
|
|
69
|
-
}, className: cn('rounded-lg px-2 py-1 transition-colors', isSelected
|
|
76
|
+
: selectedWord === token;
|
|
77
|
+
emit(itemIndex, {
|
|
78
|
+
type: item.type,
|
|
79
|
+
wordIndex: already ? null : tokenIndex,
|
|
80
|
+
crossOut: already ? '' : token,
|
|
81
|
+
...(isReplace ? { replaceWith } : {}),
|
|
82
|
+
});
|
|
83
|
+
}, className: cn('rounded-lg px-2 py-1 transition-colors', isSelectedWrong
|
|
84
|
+
? 'border border-rose-200 bg-rose-100 font-bold text-rose-700 line-through decoration-rose-600'
|
|
85
|
+
: isSelectedCorrect
|
|
86
|
+
? 'border border-emerald-300 bg-emerald-100 font-bold text-emerald-800 line-through decoration-emerald-600'
|
|
87
|
+
: isSelected
|
|
70
88
|
? 'border border-blue-200 bg-blue-100 font-bold text-blue-700 line-through decoration-blue-600'
|
|
71
89
|
: isCorrectTarget
|
|
72
90
|
? 'border-2 border-dashed border-emerald-500 bg-emerald-50 font-bold text-emerald-800'
|
|
73
91
|
: canSelect
|
|
74
92
|
? 'cursor-pointer hover:bg-blue-50 hover:text-blue-700'
|
|
75
93
|
: 'cursor-default'), children: token }, `${item.questionNumber}-token-${tokenIndex}`));
|
|
76
|
-
|
|
94
|
+
}) }), isReviewMode ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("span", { className: "font-medium", children: "\u0110\u00E1p \u00E1n: " }), formatAnswer(item.correctAnswer)] })) : null] }, `${item.questionNumber}-${itemIndex}`));
|
|
77
95
|
}), 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] }))] }));
|
|
78
96
|
}
|
|
@@ -13,6 +13,8 @@ import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
|
13
13
|
import { HelpCircle, Plus, Star, Trash2 } from 'lucide-react';
|
|
14
14
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
15
15
|
import { CrossOutWordGroupClient } from './CrossOutWordGroupClient';
|
|
16
|
+
import { createCrossOutAnswer, createEmptyCrossOutItem, DEFAULT_CROSS_OUT_INSTRUCTION, } from './map-cross-out-word-group-data';
|
|
17
|
+
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
16
18
|
function parsePointsValue(raw, allowZero = false) {
|
|
17
19
|
if (raw === '')
|
|
18
20
|
return allowZero ? 0 : 1;
|
|
@@ -22,43 +24,9 @@ function parsePointsValue(raw, allowZero = false) {
|
|
|
22
24
|
const min = allowZero ? 0 : 1;
|
|
23
25
|
return Math.max(min, parsed);
|
|
24
26
|
}
|
|
25
|
-
const DEFAULT_INSTRUCTION = 'Cross out the extra or incorrect word. If the word is incorrect, write the correct word. If the line is correct, leave it blank.';
|
|
26
|
-
function tokenize(sentence) {
|
|
27
|
-
return sentence.trim().split(/\s+/).filter(Boolean);
|
|
28
|
-
}
|
|
29
|
-
function createAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
|
|
30
|
-
const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
|
|
31
|
-
? wordIndex
|
|
32
|
-
: wordIndex === null
|
|
33
|
-
? null
|
|
34
|
-
: undefined;
|
|
35
|
-
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
36
|
-
return {
|
|
37
|
-
type,
|
|
38
|
-
...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
|
|
39
|
-
crossOut,
|
|
40
|
-
replaceWith,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
type: 'CROSS_OUT',
|
|
45
|
-
...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
|
|
46
|
-
crossOut,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function createEmptyItem(questionNumber) {
|
|
50
|
-
return {
|
|
51
|
-
type: 'CROSS_OUT',
|
|
52
|
-
question: '',
|
|
53
|
-
correctAnswer: createAnswer('CROSS_OUT'),
|
|
54
|
-
isExample: false,
|
|
55
|
-
points: 1,
|
|
56
|
-
questionNumber,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
27
|
function normalizeItems(rawItems) {
|
|
60
28
|
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
61
|
-
return [
|
|
29
|
+
return [createEmptyCrossOutItem(1)];
|
|
62
30
|
}
|
|
63
31
|
return rawItems.map((item, index) => {
|
|
64
32
|
const type = item.type === 'CROSS_OUT_AND_REPLACE' ||
|
|
@@ -66,7 +34,7 @@ function normalizeItems(rawItems) {
|
|
|
66
34
|
? 'CROSS_OUT_AND_REPLACE'
|
|
67
35
|
: 'CROSS_OUT';
|
|
68
36
|
const rawIndex = item.correctAnswer?.wordIndex;
|
|
69
|
-
const correctAnswer =
|
|
37
|
+
const correctAnswer = createCrossOutAnswer(type, item.correctAnswer?.crossOut || '', item.correctAnswer?.replaceWith || '', rawIndex);
|
|
70
38
|
return {
|
|
71
39
|
type,
|
|
72
40
|
question: item.question || '',
|
|
@@ -81,7 +49,7 @@ function sumPoints(items) {
|
|
|
81
49
|
return items.reduce((total, item) => item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1), 0);
|
|
82
50
|
}
|
|
83
51
|
export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
84
|
-
const [instruction, setInstruction] = useState(initialData?.instruction ||
|
|
52
|
+
const [instruction, setInstruction] = useState(initialData?.instruction || DEFAULT_CROSS_OUT_INSTRUCTION);
|
|
85
53
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
86
54
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
87
55
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
@@ -121,7 +89,20 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
121
89
|
if (!item.question.trim()) {
|
|
122
90
|
nextErrors.push(`Câu ${index + 1}: Nội dung câu không được để trống.`);
|
|
123
91
|
}
|
|
92
|
+
const wordIndex = item.correctAnswer.wordIndex;
|
|
93
|
+
if (wordIndex === undefined) {
|
|
94
|
+
nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch hoặc để trống nếu câu đúng (wordIndex).`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof wordIndex === 'number') {
|
|
97
|
+
const tokens = tokenizeCrossOutQuestion(item.question);
|
|
98
|
+
if (tokens[wordIndex] !== item.correctAnswer.crossOut) {
|
|
99
|
+
nextErrors.push(`Câu ${index + 1}: Từ gạch phải khớp token tại wordIndex.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
124
102
|
if (item.type === 'CROSS_OUT_AND_REPLACE') {
|
|
103
|
+
if (typeof wordIndex !== 'number') {
|
|
104
|
+
nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch (wordIndex).`);
|
|
105
|
+
}
|
|
125
106
|
if (!item.correctAnswer.crossOut.trim()) {
|
|
126
107
|
nextErrors.push(`Câu ${index + 1}: Cần chọn hoặc nhập từ cần gạch (crossOut).`);
|
|
127
108
|
}
|
|
@@ -160,7 +141,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
160
141
|
next[index] = {
|
|
161
142
|
...current,
|
|
162
143
|
type,
|
|
163
|
-
correctAnswer:
|
|
144
|
+
correctAnswer: createCrossOutAnswer(type, current.correctAnswer.crossOut, current.correctAnswer.replaceWith || '', current.correctAnswer.wordIndex),
|
|
164
145
|
};
|
|
165
146
|
return next;
|
|
166
147
|
});
|
|
@@ -174,7 +155,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
174
155
|
const type = current.type;
|
|
175
156
|
next[index] = {
|
|
176
157
|
...current,
|
|
177
|
-
correctAnswer:
|
|
158
|
+
correctAnswer: createCrossOutAnswer(type, patch.crossOut !== undefined ? patch.crossOut : current.correctAnswer.crossOut, patch.replaceWith !== undefined
|
|
178
159
|
? patch.replaceWith
|
|
179
160
|
: current.correctAnswer.replaceWith || '', patch.wordIndex !== undefined ? patch.wordIndex : current.correctAnswer.wordIndex),
|
|
180
161
|
};
|
|
@@ -194,15 +175,14 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
194
175
|
});
|
|
195
176
|
}
|
|
196
177
|
else {
|
|
197
|
-
const cleanWord = token.replace(/[^\p{L}\p{N}'-]/gu, '') || token;
|
|
198
178
|
updateItemAnswer(itemIndex, {
|
|
199
179
|
wordIndex: tokenIndex,
|
|
200
|
-
crossOut:
|
|
180
|
+
crossOut: token,
|
|
201
181
|
});
|
|
202
182
|
}
|
|
203
183
|
};
|
|
204
184
|
const addItem = () => {
|
|
205
|
-
setItems((prev) => [...prev,
|
|
185
|
+
setItems((prev) => [...prev, createEmptyCrossOutItem(prev.length + 1)]);
|
|
206
186
|
};
|
|
207
187
|
const removeItem = (index) => {
|
|
208
188
|
setItems((prev) => {
|
|
@@ -217,8 +197,8 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
217
197
|
if (isClientMode) {
|
|
218
198
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(CrossOutWordGroupClient, { questionData: currentPayload })] }));
|
|
219
199
|
}
|
|
220
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder:
|
|
221
|
-
const tokens =
|
|
200
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: DEFAULT_CROSS_OUT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] }) }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["Danh s\u00E1ch c\u00E2u (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u00E2u"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => {
|
|
201
|
+
const tokens = tokenizeCrossOutQuestion(item.question);
|
|
222
202
|
const selectedWordIndex = item.correctAnswer.wordIndex;
|
|
223
203
|
return (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", 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 ", index + 1] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("div", { className: "w-52", children: _jsxs(Select, { value: item.type, onValueChange: (value) => updateItemType(index, value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Lo\u1EA1i c\u00E2u" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "CROSS_OUT", children: "Ch\u1EC9 g\u1EA1ch (CROSS_OUT)" }), _jsx(SelectItem, { value: "CROSS_OUT_AND_REPLACE", children: "G\u1EA1ch + thay (CROSS_OUT_AND_REPLACE)" })] })] }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, {
|
|
224
204
|
isExample: checked,
|
package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js
CHANGED
|
@@ -14,20 +14,18 @@ function normalizeMode(type) {
|
|
|
14
14
|
export function createCrossOutAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
|
|
15
15
|
const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
|
|
16
16
|
? wordIndex
|
|
17
|
-
:
|
|
18
|
-
? null
|
|
19
|
-
: undefined;
|
|
17
|
+
: null;
|
|
20
18
|
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
21
19
|
return {
|
|
22
20
|
type,
|
|
23
|
-
|
|
21
|
+
wordIndex: parsedIndex,
|
|
24
22
|
crossOut,
|
|
25
23
|
replaceWith,
|
|
26
24
|
};
|
|
27
25
|
}
|
|
28
26
|
return {
|
|
29
27
|
type: 'CROSS_OUT',
|
|
30
|
-
|
|
28
|
+
wordIndex: parsedIndex,
|
|
31
29
|
crossOut,
|
|
32
30
|
};
|
|
33
31
|
}
|
|
@@ -47,10 +45,12 @@ export function mapCrossOutWordGroupItem(item, index, fallbackAnswer) {
|
|
|
47
45
|
const correctAnswerRaw = asRecord(record.correctAnswer);
|
|
48
46
|
const answerObj = isRecord(correctAnswerRaw.answer)
|
|
49
47
|
? correctAnswerRaw.answer
|
|
50
|
-
:
|
|
51
|
-
?
|
|
52
|
-
:
|
|
53
|
-
|
|
48
|
+
: 'crossOut' in correctAnswerRaw || 'wordIndex' in correctAnswerRaw
|
|
49
|
+
? correctAnswerRaw
|
|
50
|
+
: isRecord(fallbackAnswer)
|
|
51
|
+
? fallbackAnswer
|
|
52
|
+
: {};
|
|
53
|
+
const type = normalizeMode(content.type || record.type || answerObj.type);
|
|
54
54
|
const isExample = record.isExample === true;
|
|
55
55
|
const rawIndex = typeof answerObj.wordIndex === 'number'
|
|
56
56
|
? answerObj.wordIndex
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function normalizeCrossOutQuestionText(question) {
|
|
2
|
+
return question
|
|
3
|
+
.replace(/\\n/g, ' ')
|
|
4
|
+
.replace(/\r\n|\r|\n/g, ' ')
|
|
5
|
+
.replace(/\s+/g, ' ')
|
|
6
|
+
.trim();
|
|
7
|
+
}
|
|
8
|
+
export function tokenizeCrossOutQuestion(question) {
|
|
9
|
+
const normalized = normalizeCrossOutQuestionText(question);
|
|
10
|
+
return normalized ? normalized.split(' ').filter(Boolean) : [];
|
|
11
|
+
}
|