@tinyweb_dev/oe-exam-sdk 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/QuestionRenderer.js +1 -5
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
- 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/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 +16 -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 +5 -0
- package/dist/components/exams/take/utils/question-transformers.js +6 -0
- 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/answer-the-question-group.type.d.ts +3 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +16 -17
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -4
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
- package/dist/components/questions/types/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 -32
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +76 -11
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
- package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +76 -7
- 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 +20 -8
- 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.d.ts +12 -2
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +188 -46
- 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 +13 -13
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +7 -6
- 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.d.ts +1 -1
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +39 -19
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +20 -19
- 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/AnswerTheQuestionGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +12 -2
- 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.d.ts +9 -1
- package/dist/components/questions/viewer/QuestionViewer.js +23 -10
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
- package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +1 -1
- package/dist/components/results/review-data.js +8 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +39 -5
- package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
- package/dist/shared/types/questions/answer-the-question-group.js +5 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
- package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
- package/package.json +1 -1
|
@@ -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,9 +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
27
|
export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
31
28
|
const items = questionData.items || [];
|
|
32
29
|
const handleSelect = (itemIndex, optionId) => {
|
|
@@ -38,34 +35,14 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
|
|
|
38
35
|
next[itemIndex] = optionId;
|
|
39
36
|
onAnswerChange(next);
|
|
40
37
|
};
|
|
41
|
-
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) => {
|
|
42
39
|
const selectedId = userAnswers[itemIndex];
|
|
43
40
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
44
|
-
return (_jsxs(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
: isSelected
|
|
51
|
-
? 'border-blue-400 bg-blue-50'
|
|
52
|
-
: 'border-gray-200 bg-white', 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', isReviewMode && isCorrect
|
|
53
|
-
? 'bg-green-500 text-white'
|
|
54
|
-
: isSelected
|
|
55
|
-
? 'bg-blue-500 text-white'
|
|
56
|
-
: 'bg-gray-100 text-gray-600'), children: optionLetter(optionIndex) }), optionImageSrc ? (_jsx(ClientImageItem, { url: optionImageSrc, alt: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx("span", { className: "text-xs text-gray-500", children: `Đáp án ${optionLetter(optionIndex)}` })), isReviewMode && isCorrect && _jsx(Check, { className: "h-4 w-4 text-green-600" })] }, option.id || optionIndex));
|
|
57
|
-
}) })) : (_jsx("div", { className: "space-y-2", children: item.options.map((option, optionIndex) => {
|
|
58
|
-
const isCorrect = option.id === item.correctAnswer;
|
|
59
|
-
const isSelected = option.id === selectedId;
|
|
60
|
-
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', isReviewMode && isCorrect
|
|
61
|
-
? 'border-green-400 bg-green-50'
|
|
62
|
-
: isSelected
|
|
63
|
-
? 'border-blue-400 bg-blue-50'
|
|
64
|
-
: 'border-gray-200 bg-white', 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', isReviewMode && isCorrect
|
|
65
|
-
? 'bg-green-500 text-white'
|
|
66
|
-
: isSelected
|
|
67
|
-
? 'bg-blue-500 text-white'
|
|
68
|
-
: 'bg-gray-100 text-gray-600'), 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" })] }, option.id || optionIndex));
|
|
69
|
-
}) }))] })] }, `${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}`));
|
|
70
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] }))] }));
|
|
71
48
|
}
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ChooseThenAnswerGroupClientProps } from '../../_shared/types/choose-then-answer-group.type';
|
|
2
|
-
export declare function ChooseThenAnswerGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: ChooseThenAnswerGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function ChooseThenAnswerGroupClient({ questionData, isReviewMode, userAnswers, itemResults, onAnswerChange, }: ChooseThenAnswerGroupClientProps): import("react").JSX.Element;
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js
CHANGED
|
@@ -1,20 +1,59 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { 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
|
}
|
|
8
|
-
|
|
9
|
+
function formatHalf(points) {
|
|
10
|
+
const half = points * 0.5;
|
|
11
|
+
return Number.isInteger(half) ? String(half) : half.toFixed(1);
|
|
12
|
+
}
|
|
13
|
+
function badgeClass(params) {
|
|
14
|
+
if (params.isExample) {
|
|
15
|
+
return 'bg-amber-500';
|
|
16
|
+
}
|
|
17
|
+
if (!params.isReviewMode || !params.hasAttempt) {
|
|
18
|
+
return 'bg-indigo-600';
|
|
19
|
+
}
|
|
20
|
+
if (params.partialScore != null) {
|
|
21
|
+
if (params.partialScore >= params.points - 1e-9) {
|
|
22
|
+
return 'bg-emerald-500';
|
|
23
|
+
}
|
|
24
|
+
if (params.partialScore > 0) {
|
|
25
|
+
return 'bg-amber-500';
|
|
26
|
+
}
|
|
27
|
+
return 'bg-red-500';
|
|
28
|
+
}
|
|
29
|
+
if (params.mappingCorrect || params.hasText) {
|
|
30
|
+
return 'bg-amber-500';
|
|
31
|
+
}
|
|
32
|
+
return 'bg-red-500';
|
|
33
|
+
}
|
|
34
|
+
export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], itemResults, onAnswerChange, }) {
|
|
9
35
|
const canEdit = Boolean(onAnswerChange) && !isReviewMode;
|
|
10
36
|
const items = questionData.items || [];
|
|
11
|
-
|
|
37
|
+
const hasAttempt = Boolean(itemResults?.length) ||
|
|
38
|
+
userAnswers.some((answer) => Boolean(answer?.optionId || answer?.text?.trim()));
|
|
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) => {
|
|
12
40
|
const stored = userAnswers[itemIndex] || emptyAnswer();
|
|
13
41
|
const current = item.isExample
|
|
14
42
|
? { optionId: item.optionId, text: stored.text }
|
|
15
43
|
: stored;
|
|
16
44
|
const expectedId = item.optionId;
|
|
45
|
+
const hasStudentText = current.text.trim() !== '';
|
|
46
|
+
const result = itemResults?.[itemIndex];
|
|
47
|
+
const mappingCorrect = Boolean(expectedId) && current.optionId === expectedId;
|
|
48
|
+
const points = typeof item.points === 'number' ? item.points : 1;
|
|
49
|
+
const halfLabel = formatHalf(points);
|
|
50
|
+
const sampleAnswers = (item.expectedAnswers || [])
|
|
51
|
+
.map((answer) => answer.trim())
|
|
52
|
+
.filter((answer) => answer !== '');
|
|
17
53
|
const parts = (item.question || '').split('{0}');
|
|
54
|
+
const showReview = isReviewMode && !item.isExample && Boolean(expectedId);
|
|
55
|
+
const showAnswerKey = showReview && !hasAttempt;
|
|
56
|
+
const showGradedReview = showReview && hasAttempt;
|
|
18
57
|
const setAnswer = (next) => {
|
|
19
58
|
if (!canEdit || item.isExample)
|
|
20
59
|
return;
|
|
@@ -23,12 +62,38 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
23
62
|
text: next.text ?? current.text,
|
|
24
63
|
});
|
|
25
64
|
};
|
|
26
|
-
return (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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({
|
|
66
|
+
isExample: item.isExample,
|
|
67
|
+
isReviewMode,
|
|
68
|
+
hasAttempt,
|
|
69
|
+
points,
|
|
70
|
+
partialScore: result?.partialScore,
|
|
71
|
+
mappingCorrect,
|
|
72
|
+
hasText: hasStudentText,
|
|
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) => {
|
|
74
|
+
const selected = current.optionId === option.id;
|
|
75
|
+
const isKey = option.id === expectedId;
|
|
76
|
+
const pickedCorrect = showGradedReview && selected && isKey;
|
|
77
|
+
const pickedWrong = showGradedReview && selected && !isKey;
|
|
78
|
+
const keyOnly = showGradedReview && !selected && isKey;
|
|
79
|
+
const officialCorrect = showAnswerKey && isKey;
|
|
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 &&
|
|
81
|
+
selected &&
|
|
82
|
+
'border-blue-500 bg-blue-500 text-white shadow-sm ring-2 ring-blue-100', !showReview &&
|
|
83
|
+
!selected &&
|
|
84
|
+
'border-gray-200 bg-white text-slate-800 hover:border-blue-300 hover:bg-blue-50', showAnswerKey &&
|
|
85
|
+
!isKey &&
|
|
86
|
+
'border-slate-200 bg-white text-slate-800', officialCorrect &&
|
|
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 &&
|
|
88
|
+
!selected &&
|
|
89
|
+
!isKey &&
|
|
90
|
+
'border-slate-200 bg-slate-50 text-slate-400', canEdit &&
|
|
91
|
+
!item.isExample &&
|
|
92
|
+
'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-200', (!canEdit || item.isExample) && 'cursor-default'), children: [option.text || option.id, pickedCorrect ? _jsx(Check, { className: "h-3 w-3" }) : null, pickedWrong ? _jsx(X, { className: "h-3 w-3" }) : null] })] }, option.id));
|
|
93
|
+
}) }), _jsx("span", { children: parts.slice(1).join('{0}') })] }), showReview && hasAttempt ? (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsxs("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', mappingCorrect
|
|
94
|
+
? 'bg-emerald-50 text-emerald-700'
|
|
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
|
|
96
|
+
? 'bg-emerald-50 text-emerald-700'
|
|
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}`));
|
|
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] }))] }));
|
|
34
99
|
}
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js
CHANGED
|
@@ -8,10 +8,10 @@ import { PointsInput } from '../../../../components/ui/points-input';
|
|
|
8
8
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
9
9
|
import { Switch } from '../../../../components/ui/switch';
|
|
10
10
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
11
|
-
import { ListChecks, Plus, Star, Trash2 } from 'lucide-react';
|
|
11
|
+
import { ListChecks, PenLine, Plus, Sparkles, Star, Trash2 } from 'lucide-react';
|
|
12
12
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
13
13
|
import { ChooseThenAnswerGroupClient } from './ChooseThenAnswerGroupClient';
|
|
14
|
-
import { createEmptyItem, createEmptyOption, sumGradablePoints, } from './map-choose-then-answer-group-data';
|
|
14
|
+
import { createEmptyItem, createEmptyOption, normalizeExpectedAnswers, sumGradablePoints, } from './map-choose-then-answer-group-data';
|
|
15
15
|
import { CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/choose-then-answer-group.type';
|
|
16
16
|
function normalizeItems(raw) {
|
|
17
17
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
@@ -29,6 +29,7 @@ function normalizeItems(raw) {
|
|
|
29
29
|
question: item.question || '',
|
|
30
30
|
options,
|
|
31
31
|
optionId: item.optionId || options[0]?.id || '',
|
|
32
|
+
expectedAnswers: normalizeExpectedAnswers(item.expectedAnswers),
|
|
32
33
|
isExample: item.isExample === true,
|
|
33
34
|
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
34
35
|
questionNumber: item.questionNumber || index + 1,
|
|
@@ -41,6 +42,7 @@ export function ChooseThenAnswerGroupCreator({ initialData, onChange, externalEr
|
|
|
41
42
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
42
43
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
43
44
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
45
|
+
const [previewAnswers, setPreviewAnswers] = useState([]);
|
|
44
46
|
const [errors, setErrors] = useState([]);
|
|
45
47
|
const onChangeRef = useRef(onChange);
|
|
46
48
|
const previousPayloadRef = useRef('');
|
|
@@ -156,6 +158,33 @@ export function ChooseThenAnswerGroupCreator({ initialData, onChange, externalEr
|
|
|
156
158
|
};
|
|
157
159
|
}));
|
|
158
160
|
};
|
|
161
|
+
const updateExpectedAnswer = (itemIndex, answerIndex, value) => {
|
|
162
|
+
setItems((prev) => prev.map((item, i) => {
|
|
163
|
+
if (i !== itemIndex)
|
|
164
|
+
return item;
|
|
165
|
+
const expectedAnswers = [...(item.expectedAnswers || [''])];
|
|
166
|
+
expectedAnswers[answerIndex] = value;
|
|
167
|
+
return { ...item, expectedAnswers };
|
|
168
|
+
}));
|
|
169
|
+
};
|
|
170
|
+
const addExpectedAnswer = (itemIndex) => {
|
|
171
|
+
setItems((prev) => prev.map((item, i) => {
|
|
172
|
+
if (i !== itemIndex)
|
|
173
|
+
return item;
|
|
174
|
+
return { ...item, expectedAnswers: [...(item.expectedAnswers || ['']), ''] };
|
|
175
|
+
}));
|
|
176
|
+
};
|
|
177
|
+
const removeExpectedAnswer = (itemIndex, answerIndex) => {
|
|
178
|
+
setItems((prev) => prev.map((item, i) => {
|
|
179
|
+
const current = item.expectedAnswers || [''];
|
|
180
|
+
if (i !== itemIndex || current.length <= 1)
|
|
181
|
+
return item;
|
|
182
|
+
return {
|
|
183
|
+
...item,
|
|
184
|
+
expectedAnswers: current.filter((_, j) => j !== answerIndex),
|
|
185
|
+
};
|
|
186
|
+
}));
|
|
187
|
+
};
|
|
159
188
|
const addItem = () => {
|
|
160
189
|
setItems((prev) => [...prev, createEmptyItem(prev.length + 1)]);
|
|
161
190
|
};
|
|
@@ -168,10 +197,29 @@ export function ChooseThenAnswerGroupCreator({ initialData, onChange, externalEr
|
|
|
168
197
|
})));
|
|
169
198
|
};
|
|
170
199
|
const displayErrors = [...errors, ...(externalErrors || [])];
|
|
200
|
+
const openPreview = () => {
|
|
201
|
+
setPreviewAnswers(items.map((item) => ({
|
|
202
|
+
optionId: item.isExample ? item.optionId : '',
|
|
203
|
+
text: '',
|
|
204
|
+
})));
|
|
205
|
+
setIsClientMode(true);
|
|
206
|
+
};
|
|
171
207
|
if (isClientMode) {
|
|
172
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(ChooseThenAnswerGroupClient, { questionData: buildPayload(),
|
|
208
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(ChooseThenAnswerGroupClient, { questionData: buildPayload(), userAnswers: previewAnswers, onAnswerChange: (itemIndex, next) => {
|
|
209
|
+
setPreviewAnswers((prev) => {
|
|
210
|
+
const answers = items.map((item, index) => prev[index] || {
|
|
211
|
+
optionId: item.isExample ? item.optionId : '',
|
|
212
|
+
text: '',
|
|
213
|
+
});
|
|
214
|
+
answers[itemIndex] = next;
|
|
215
|
+
return answers;
|
|
216
|
+
});
|
|
217
|
+
} })] }));
|
|
173
218
|
}
|
|
174
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("ul", { className: "list-disc space-y-1 rounded-md border border-red-200 bg-red-50 p-2 pl-6 text-xs text-red-700", children: displayErrors.map((error) => (_jsx("li", { children: error }, error))) })),
|
|
175
|
-
|
|
176
|
-
|
|
219
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("ul", { className: "list-disc space-y-1 rounded-md border border-red-200 bg-red-50 p-2 pl-6 text-xs text-red-700", children: displayErrors.map((error) => (_jsx("li", { children: error }, error))) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(ListChecks, { className: "h-3.5 w-3.5" }), title: "Ch\u1ECDn r\u1ED3i tr\u1EA3 l\u1EDDi (Nh\u00F3m)", preview: { onClick: openPreview } }), children: [_jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "cta-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "cta-title", value: title, onChange: (event) => setTitle(event.target.value), placeholder: "GRAMMAR", className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "cta-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "cta-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] })] }), _jsxs("p", { className: "mt-2.5 rounded-lg border border-indigo-100 bg-indigo-50/70 px-3 py-2 text-xs leading-relaxed text-indigo-800", children: ["M\u1ED7i c\u00E2u c\u00F3 2 ph\u1EA7n: ", _jsx("strong", { children: "ch\u1ECDn t\u1EEB \u0111\u00FAng" }), " trong c\u00E2u (mapping), r\u1ED3i", ' ', _jsx("strong", { children: "vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi" }), " (AI). V\u00ED d\u1EE5: Did you eat many | much for lunch today? \u2192 No, I didn\u2019t eat much for lunch today."] })] }), _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) => (_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("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: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Switch, { id: `cta-example-${index}`, checked: item.isExample === true, onCheckedChange: (checked) => updateItem(index, { isExample: checked }) }), _jsxs(Label, { htmlFor: `cta-example-${index}`, className: "flex cursor-pointer items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] })] }), !item.isExample && (_jsxs("div", { className: "space-y-0.5", children: [_jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
|
|
220
|
+
points: Number.parseInt(event.target.value, 10) || 1,
|
|
221
|
+
}) }), _jsx("p", { className: "text-[10px] leading-tight text-slate-500", children: "C\u1EA3 c\u00E2u: 50% ch\u1ECDn + 50% vi\u1EBFt" })] })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeItem(index), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { className: "space-y-2 rounded-lg border border-slate-200 bg-slate-50/60 px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-slate-600", children: [_jsx(ListChecks, { className: "h-3.5 w-3.5" }), "1. Ch\u1ECDn t\u1EEB \u0111\u00FAng"] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi" }), _jsx(Input, { value: item.question, onChange: (event) => updateItem(index, { question: event.target.value }), placeholder: "Did you eat {0} for lunch today?", className: "h-10 border-gray-200 bg-white" }), _jsxs("p", { className: "text-xs text-slate-500", children: ["D\u00F9ng ", '{0}', " cho ch\u1ED7 hi\u1EC7n chip ch\u1ECDn."] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "L\u1EF1a ch\u1ECDn" }), _jsx("p", { className: "text-xs text-slate-500", children: "Ch\u1ECDn radio \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u \u0111\u00E1p \u00E1n \u0111\u00FAng (chip)." }), item.options.map((option, optionIndex) => {
|
|
222
|
+
const isCorrect = item.optionId === option.id;
|
|
223
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: `cta-correct-${index}`, checked: isCorrect, onChange: () => updateItem(index, { optionId: option.id }), "aria-label": `Đáp án đúng: ${option.text || option.id}`, className: "h-4 w-4 shrink-0 accent-indigo-600" }), _jsx(Input, { value: option.text, onChange: (event) => updateOption(index, optionIndex, event.target.value), placeholder: `Lựa chọn ${optionIndex + 1}`, className: "h-10 border-gray-200 bg-white" }), isCorrect && (_jsx("span", { className: "shrink-0 text-[10px] font-semibold uppercase text-indigo-600", children: "\u0110\u00FAng" })), item.options.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeOption(index, optionIndex), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, option.id));
|
|
224
|
+
})] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => addOption(index), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Th\u00EAm l\u1EF1a ch\u1ECDn"] })] }), _jsxs("div", { className: "space-y-2 rounded-lg border border-violet-100 bg-violet-50/50 px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-violet-700", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), "2. Tr\u1EA3 l\u1EDDi c\u00E2u h\u1ECFi (AI)"] }), _jsxs("p", { className: "flex items-start gap-1.5 text-xs text-violet-800/80", children: [_jsx(PenLine, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), "H\u1ECDc sinh vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi c\u1EE7a m\u00ECnh. \u0110\u00E1p \u00E1n m\u1EABu gi\u00FAp AI ch\u1EA5m theo ngh\u0129a (\u0111\u1EC3 tr\u1ED1ng = ch\u1EA5m \u0111\u00FAng ch\u1EE7 \u0111\u1EC1)."] }), (item.expectedAnswers || ['']).map((answer, answerIndex) => (_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("textarea", { value: answer, onChange: (event) => updateExpectedAnswer(index, answerIndex, event.target.value), placeholder: "No, I didn\u2019t eat much for lunch today.", rows: 2, className: "min-h-[3.25rem] w-full resize-none rounded-md border border-gray-200 bg-white px-3 py-2 text-sm outline-none focus:border-violet-400 focus:ring-2 focus:ring-violet-100" }), (item.expectedAnswers || ['']).length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeExpectedAnswer(index, answerIndex), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `cta-expected-${index}-${answerIndex}`))), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => addExpectedAnswer(index), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Th\u00EAm \u0111\u00E1p \u00E1n m\u1EABu"] })] })] }, `${item.questionNumber}-${index}`))), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) })] })] })] }));
|
|
177
225
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { ChooseThenAnswerGroupData, ChooseThenAnswerGroupItemData, ChooseThenAnswerOption, ChooseThenAnswerStudentItemAnswer } from '../../_shared/types/choose-then-answer-group.type';
|
|
1
|
+
import type { ChooseThenAnswerGradeDetail, ChooseThenAnswerGroupData, ChooseThenAnswerGroupItemData, ChooseThenAnswerItemResult, ChooseThenAnswerOption, ChooseThenAnswerStudentItemAnswer } from '../../_shared/types/choose-then-answer-group.type';
|
|
2
2
|
export declare function nextPrefixedId(prefix: string, existing: string[]): string;
|
|
3
3
|
export declare function sumGradablePoints(items: ChooseThenAnswerGroupItemData[]): number;
|
|
4
4
|
export declare function createEmptyOption(existingIds: string[]): ChooseThenAnswerOption;
|
|
5
|
+
export declare function normalizeExpectedAnswers(value: unknown): string[];
|
|
5
6
|
export declare function createEmptyItem(questionNumber: number): ChooseThenAnswerGroupItemData;
|
|
6
7
|
export declare function parseItemAnswer(raw: unknown): {
|
|
7
8
|
optionId: string;
|
|
9
|
+
expectedAnswers: string[];
|
|
8
10
|
};
|
|
9
11
|
export declare function parseStudentAnswer(raw: unknown): ChooseThenAnswerStudentItemAnswer;
|
|
10
12
|
export declare function mapApiItem(item: unknown, index: number, fallbackAnswer?: unknown): ChooseThenAnswerGroupItemData;
|
|
11
13
|
export declare function mapQuestionToChooseThenAnswerGroupData(question: Record<string, unknown> | null | undefined): ChooseThenAnswerGroupData;
|
|
14
|
+
export declare function toCtaItemResults(details: ChooseThenAnswerGradeDetail[] | undefined, items: ChooseThenAnswerGroupItemData[], userAnswers: ChooseThenAnswerStudentItemAnswer[]): ChooseThenAnswerItemResult[] | undefined;
|
|
@@ -28,6 +28,16 @@ export function createEmptyOption(existingIds) {
|
|
|
28
28
|
text: '',
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
export function normalizeExpectedAnswers(value) {
|
|
32
|
+
if (!Array.isArray(value)) {
|
|
33
|
+
return [''];
|
|
34
|
+
}
|
|
35
|
+
const filled = value
|
|
36
|
+
.filter((item) => typeof item === 'string')
|
|
37
|
+
.map((item) => item.trim())
|
|
38
|
+
.filter((item) => item !== '');
|
|
39
|
+
return filled.length > 0 ? filled : [''];
|
|
40
|
+
}
|
|
31
41
|
export function createEmptyItem(questionNumber) {
|
|
32
42
|
const first = createEmptyOption([]);
|
|
33
43
|
const second = createEmptyOption([first.id]);
|
|
@@ -35,6 +45,7 @@ export function createEmptyItem(questionNumber) {
|
|
|
35
45
|
question: '',
|
|
36
46
|
options: [first, second],
|
|
37
47
|
optionId: first.id,
|
|
48
|
+
expectedAnswers: [''],
|
|
38
49
|
isExample: false,
|
|
39
50
|
points: 1,
|
|
40
51
|
questionNumber,
|
|
@@ -42,8 +53,11 @@ export function createEmptyItem(questionNumber) {
|
|
|
42
53
|
}
|
|
43
54
|
export function parseItemAnswer(raw) {
|
|
44
55
|
if (!isRecord(raw))
|
|
45
|
-
return { optionId: '' };
|
|
46
|
-
return {
|
|
56
|
+
return { optionId: '', expectedAnswers: [''] };
|
|
57
|
+
return {
|
|
58
|
+
optionId: asString(raw.optionId),
|
|
59
|
+
expectedAnswers: normalizeExpectedAnswers(raw.expectedAnswers),
|
|
60
|
+
};
|
|
47
61
|
}
|
|
48
62
|
export function parseStudentAnswer(raw) {
|
|
49
63
|
if (!isRecord(raw))
|
|
@@ -78,6 +92,7 @@ export function mapApiItem(item, index, fallbackAnswer) {
|
|
|
78
92
|
question: asString(content.question) || asString(record.question),
|
|
79
93
|
options,
|
|
80
94
|
optionId: parsed.optionId || options[0]?.id || '',
|
|
95
|
+
expectedAnswers: parsed.expectedAnswers,
|
|
81
96
|
isExample,
|
|
82
97
|
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
83
98
|
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
@@ -125,3 +140,25 @@ export function mapQuestionToChooseThenAnswerGroupData(question) {
|
|
|
125
140
|
points: 1,
|
|
126
141
|
};
|
|
127
142
|
}
|
|
143
|
+
export function toCtaItemResults(details, items, userAnswers) {
|
|
144
|
+
if (!details?.length) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const byId = new Map(details.map((detail) => [detail.itemId, detail]));
|
|
148
|
+
return items.map((item, index) => {
|
|
149
|
+
const detail = byId.get(`item-${index}`);
|
|
150
|
+
const mappingCorrect = Boolean(item.optionId) && userAnswers[index]?.optionId === item.optionId;
|
|
151
|
+
const half = (typeof item.points === 'number' ? item.points : 1) * 0.5;
|
|
152
|
+
let aiCorrect;
|
|
153
|
+
if (detail?.partialScore != null) {
|
|
154
|
+
const mappingHalf = mappingCorrect ? half : 0;
|
|
155
|
+
aiCorrect = detail.partialScore - mappingHalf > 1e-9;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
mappingCorrect,
|
|
159
|
+
aiCorrect,
|
|
160
|
+
partialScore: detail?.partialScore,
|
|
161
|
+
feedback: detail?.feedback,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION, CHOOSE_THEN_ANSWER_ITEM_TYPE, } from '../../_shared/types/choose-then-answer-group.type';
|
|
2
2
|
import { sumGradablePoints } from './map-choose-then-answer-group-data';
|
|
3
|
+
function filledExpectedAnswers(values) {
|
|
4
|
+
return (values || []).map((value) => value.trim()).filter((value) => value !== '');
|
|
5
|
+
}
|
|
3
6
|
export const transformChooseThenAnswerGroup = (question) => {
|
|
4
7
|
const answerData = question.answer;
|
|
5
8
|
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
@@ -7,25 +10,35 @@ export const transformChooseThenAnswerGroup = (question) => {
|
|
|
7
10
|
return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
|
|
8
11
|
}
|
|
9
12
|
const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
|
|
10
|
-
const apiItems = items.map((item, index) =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
const apiItems = items.map((item, index) => {
|
|
14
|
+
const expectedAnswers = filledExpectedAnswers(item.expectedAnswers);
|
|
15
|
+
return {
|
|
16
|
+
questionType: CHOOSE_THEN_ANSWER_ITEM_TYPE,
|
|
17
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
18
|
+
content: {
|
|
19
|
+
question: item.question || '',
|
|
20
|
+
options: (item.options || []).map((option) => ({
|
|
21
|
+
id: option.id || '',
|
|
22
|
+
text: option.text || '',
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
correctAnswer: {
|
|
26
|
+
answer: {
|
|
27
|
+
optionId: item.optionId || '',
|
|
28
|
+
...(expectedAnswers.length > 0 ? { expectedAnswers } : {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
32
|
+
questionNumber: item.questionNumber || index + 1,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
24
35
|
return {
|
|
25
36
|
apiContent: {
|
|
26
37
|
meta: {
|
|
27
38
|
instruction: answerData?.instruction || CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION,
|
|
28
39
|
...(title ? { title } : {}),
|
|
40
|
+
gradingType: 'AI',
|
|
41
|
+
isAiGraded: true,
|
|
29
42
|
},
|
|
30
43
|
items: apiItems,
|
|
31
44
|
},
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CrossOutWordGroupClientProps } from '../../_shared/types/cross-out-word-group.type';
|
|
2
|
-
export declare function CrossOutWordGroupClient({ questionData, isReviewMode, userAnswers, }: CrossOutWordGroupClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function CrossOutWordGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: CrossOutWordGroupClientProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,12 @@
|
|
|
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
|
+
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
6
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
7
|
+
function normalizeWord(word) {
|
|
8
|
+
return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
|
|
9
|
+
}
|
|
5
10
|
function formatAnswer(answer) {
|
|
6
11
|
if (!answer)
|
|
7
12
|
return '—';
|
|
@@ -15,13 +20,77 @@ function formatAnswer(answer) {
|
|
|
15
20
|
return 'câu đúng (không gạch)';
|
|
16
21
|
return `gạch "${answer.crossOut}"${posInfo}`;
|
|
17
22
|
}
|
|
18
|
-
|
|
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
|
+
}
|
|
31
|
+
export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
19
32
|
const items = questionData.items || [];
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
const emit = (itemIndex, nextItem) => {
|
|
34
|
+
const item = items[itemIndex];
|
|
35
|
+
if (isReviewMode || item?.isExample || !onAnswerChange) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const next = items.map((_, index) => userAnswers[index] ?? null);
|
|
39
|
+
next[itemIndex] = nextItem;
|
|
40
|
+
onAnswerChange(next);
|
|
41
|
+
};
|
|
42
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => {
|
|
22
43
|
const isReplace = item.type === 'CROSS_OUT_AND_REPLACE';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
|
|
45
|
+
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
46
|
+
const tokens = tokenizeCrossOutQuestion(item.question || '');
|
|
47
|
+
const selectedIndex = userValue?.wordIndex;
|
|
48
|
+
const selectedWord = userValue?.crossOut || '';
|
|
49
|
+
const replaceWith = userValue?.replaceWith || '';
|
|
50
|
+
const correct = item.correctAnswer;
|
|
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'
|
|
75
|
+
? selectedIndex === tokenIndex
|
|
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
|
|
88
|
+
? 'border border-blue-200 bg-blue-100 font-bold text-blue-700 line-through decoration-blue-600'
|
|
89
|
+
: isCorrectTarget
|
|
90
|
+
? 'border-2 border-dashed border-emerald-500 bg-emerald-50 font-bold text-emerald-800'
|
|
91
|
+
: canSelect
|
|
92
|
+
? 'cursor-pointer hover:bg-blue-50 hover:text-blue-700'
|
|
93
|
+
: 'cursor-default'), children: token }, `${item.questionNumber}-token-${tokenIndex}`));
|
|
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}`));
|
|
26
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] }))] }));
|
|
27
96
|
}
|