@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
|
@@ -4,18 +4,18 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { Label } from '../../../../components/ui/label';
|
|
6
6
|
import { Input } from '../../../../components/ui/input';
|
|
7
|
-
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
7
|
import { PointsInput } from '../../../../components/ui/points-input';
|
|
9
8
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
10
9
|
import { Switch } from '../../../../components/ui/switch';
|
|
11
10
|
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
12
11
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
13
|
-
import {
|
|
12
|
+
import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
|
|
13
|
+
import { BookOpen, HelpCircle, Plus, Star, Trash2 } from 'lucide-react';
|
|
14
14
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
15
15
|
import { FillInBlankGroupClient } from './FillInBlankGroupClient';
|
|
16
16
|
import { FillInBlankGroupMediaField } from './FillInBlankGroupMediaField';
|
|
17
17
|
import { createEmptyFillInBlankGroupItem } from './map-fill-in-blank-group-data';
|
|
18
|
-
import { blankSlotCount, extractPlaceholderIndices, fromFillInBlankImageUrls, hasAnyBlankOptions,
|
|
18
|
+
import { blankSlotCount, extractPlaceholderIndices, fromFillInBlankImageUrls, hasAnyBlankOptions, normalizeAnswersMatrix, normalizeWordBank, parseSlashAnswers, toFillInBlankImageUrls, } from './blank-utils';
|
|
19
19
|
import { FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/fill-in-blank-group.type';
|
|
20
20
|
function parsePointsValue(raw, allowZero = false) {
|
|
21
21
|
if (raw === '')
|
|
@@ -26,16 +26,69 @@ function parsePointsValue(raw, allowZero = false) {
|
|
|
26
26
|
const min = allowZero ? 0 : 1;
|
|
27
27
|
return Math.max(min, parsed);
|
|
28
28
|
}
|
|
29
|
+
function padEditorWordBank(raw, requiredLength) {
|
|
30
|
+
if (requiredLength <= 0 || !Array.isArray(raw))
|
|
31
|
+
return undefined;
|
|
32
|
+
const next = raw.slice(0, requiredLength);
|
|
33
|
+
while (next.length < requiredLength)
|
|
34
|
+
next.push(null);
|
|
35
|
+
return next.some((entry) => entry && entry.options.length > 0) ? next : undefined;
|
|
36
|
+
}
|
|
37
|
+
function editorOptionSlots(options) {
|
|
38
|
+
if (!options || options.length === 0)
|
|
39
|
+
return ['', ''];
|
|
40
|
+
if (options.length === 1)
|
|
41
|
+
return [options[0], ''];
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
function editorAnswerSlots(alts) {
|
|
45
|
+
if (!alts || alts.length === 0)
|
|
46
|
+
return [''];
|
|
47
|
+
return alts.map((alt) => (alt == null ? '' : String(alt)));
|
|
48
|
+
}
|
|
49
|
+
function writeBlankAnswers(item, blankIndex, alts) {
|
|
50
|
+
const requiredLength = blankSlotCount(item.question);
|
|
51
|
+
const next = [...item.answers];
|
|
52
|
+
while (next.length < requiredLength)
|
|
53
|
+
next.push(['']);
|
|
54
|
+
next[blankIndex] = alts.length > 0 ? alts : [''];
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
function blankHasOptions(options) {
|
|
58
|
+
return Boolean(options?.some((option) => option.trim()));
|
|
59
|
+
}
|
|
60
|
+
function optionEditorKey(itemIndex, blankIndex) {
|
|
61
|
+
return `${itemIndex}-${blankIndex}`;
|
|
62
|
+
}
|
|
63
|
+
function writeBlankOptions(item, blankIndex, options) {
|
|
64
|
+
const requiredLength = blankSlotCount(item.question);
|
|
65
|
+
const next = [...(item.wordBank || [])];
|
|
66
|
+
while (next.length < requiredLength)
|
|
67
|
+
next.push(null);
|
|
68
|
+
next[blankIndex] = { options };
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
function clearBlankOptions(item, blankIndex) {
|
|
72
|
+
const requiredLength = blankSlotCount(item.question);
|
|
73
|
+
const next = [...(item.wordBank || [])];
|
|
74
|
+
while (next.length < requiredLength)
|
|
75
|
+
next.push(null);
|
|
76
|
+
next[blankIndex] = null;
|
|
77
|
+
return next.some((entry) => entry && entry.options.some((option) => option.trim()))
|
|
78
|
+
? next
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
29
81
|
function syncItemAnswers(item) {
|
|
30
82
|
const indices = extractPlaceholderIndices(item.question || '');
|
|
31
83
|
const requiredLength = blankSlotCount(item.question || '');
|
|
32
|
-
const wordBank =
|
|
84
|
+
const wordBank = padEditorWordBank(item.wordBank, requiredLength);
|
|
85
|
+
const rawAnswers = Array.isArray(item.answers) ? item.answers : [];
|
|
33
86
|
return {
|
|
34
87
|
...item,
|
|
35
|
-
answers:
|
|
88
|
+
answers: Array.from({ length: requiredLength }, (_, index) => {
|
|
36
89
|
if (!indices.includes(index))
|
|
37
90
|
return [];
|
|
38
|
-
return
|
|
91
|
+
return editorAnswerSlots(rawAnswers[index]);
|
|
39
92
|
}),
|
|
40
93
|
...(wordBank ? { wordBank } : { wordBank: undefined }),
|
|
41
94
|
};
|
|
@@ -66,6 +119,8 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
66
119
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
67
120
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
68
121
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
122
|
+
const [showPassage, setShowPassage] = useState(!isRichTextEmpty(initialData?.passage));
|
|
123
|
+
const [openOptionKeys, setOpenOptionKeys] = useState(() => new Set());
|
|
69
124
|
const [errors, setErrors] = useState([]);
|
|
70
125
|
const onChangeRef = useRef(onChange);
|
|
71
126
|
const previousPayloadRef = useRef('');
|
|
@@ -84,7 +139,11 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
84
139
|
showHints: nextShowHints,
|
|
85
140
|
hints: parseHints(nextHintsText),
|
|
86
141
|
...(viewMode ? { viewMode } : {}),
|
|
87
|
-
items: nextItems
|
|
142
|
+
items: nextItems.map((item) => ({
|
|
143
|
+
...item,
|
|
144
|
+
answers: normalizeAnswersMatrix(item.answers, blankSlotCount(item.question)),
|
|
145
|
+
wordBank: normalizeWordBank(item.wordBank, blankSlotCount(item.question)),
|
|
146
|
+
})),
|
|
88
147
|
explanation: nextExplanation,
|
|
89
148
|
points: sumPoints(nextItems),
|
|
90
149
|
};
|
|
@@ -122,7 +181,9 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
122
181
|
if (!alts.some((alt) => alt.trim())) {
|
|
123
182
|
nextErrors.push(`Câu ${index + 1}: Blank {${blankIndex}} cần ít nhất một đáp án.`);
|
|
124
183
|
}
|
|
125
|
-
const options = item.wordBank?.[blankIndex]?.options || []
|
|
184
|
+
const options = (item.wordBank?.[blankIndex]?.options || [])
|
|
185
|
+
.map((option) => option.trim())
|
|
186
|
+
.filter(Boolean);
|
|
126
187
|
if (options.length >= 2) {
|
|
127
188
|
const missing = alts
|
|
128
189
|
.map((alt) => alt.trim())
|
|
@@ -172,23 +233,45 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
172
233
|
if (isClientMode) {
|
|
173
234
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(FillInBlankGroupClient, { questionData: currentPayload })] }));
|
|
174
235
|
}
|
|
175
|
-
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))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "\u0110i\u1EC1n ch\u1ED7 tr\u1ED1ng (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: FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (
|
|
236
|
+
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))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "\u0110i\u1EC1n ch\u1ED7 tr\u1ED1ng (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: FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] }), showPassage ? (_jsxs("div", { className: "relative space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setShowPassage(false), className: "h-7 px-2 text-xs text-gray-500 hover:text-gray-700", children: "\u1EA8n" })] }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setShowPassage(true), className: "h-8 gap-1.5 border-indigo-200 px-2.5 text-xs text-indigo-700 hover:bg-indigo-50 hover:text-indigo-800", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5" }), isRichTextEmpty(passage) ? 'Thêm bài đọc' : 'Hiện bài đọc'] })), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsx(FillInBlankGroupMediaField, { id: "fib-group-audio", label: "Audio chung (optional)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl }), _jsx(FillInBlankGroupMediaField, { id: "fib-group-image", label: "\u1EA2nh chung (optional)", accept: "image/*", value: toFillInBlankImageUrls(imageUrl)[0] || '', onChange: setImageUrl })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "show-hints", checked: showHints, onCheckedChange: setShowHints }), _jsx(Label, { htmlFor: "show-hints", children: "Hi\u1EC7n h\u1ED9p t\u1EEB g\u1EE3i \u00FD chung" })] }), showHints && (_jsx(Input, { value: hintsText, onChange: (e) => setHintsText(e.target.value), placeholder: "am / is / are", 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) => (_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 items-center justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-gray-700", 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 }), "C\u00E2u ", index + 1] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `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 && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
|
|
176
237
|
points: parsePointsValue(event.target.value, true),
|
|
177
|
-
}) }) })), 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", { children: [_jsxs(Label, { htmlFor: `question-${index}`, children: ["
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
238
|
+
}) }) })), 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", { children: [_jsxs(Label, { htmlFor: `question-${index}`, children: ["N\u1ED9i dung c\u00E2u (d\u00F9ng ", '{0}', ", ", '{1}', "...)"] }), _jsx(Input, { id: `question-${index}`, value: item.question, onChange: (e) => updateItem(index, { question: e.target.value }), placeholder: "{0}", className: "mt-1 h-10 border-gray-200 bg-white" })] }), _jsx("div", { className: "grid grid-cols-2 gap-2.5", children: extractPlaceholderIndices(item.question).map((blankIndex) => {
|
|
239
|
+
const optionKey = optionEditorKey(index, blankIndex);
|
|
240
|
+
const showOptions = openOptionKeys.has(optionKey);
|
|
241
|
+
const slots = editorOptionSlots(item.wordBank?.[blankIndex]?.options);
|
|
242
|
+
const answerSlots = editorAnswerSlots(item.answers[blankIndex]);
|
|
243
|
+
return (_jsxs("div", { className: "min-w-0 space-y-2 rounded-md border border-gray-100 bg-slate-50/70 px-2.5 py-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { children: ["\u0110\u00E1p \u00E1n ", '{' + blankIndex + '}'] }), answerSlots.map((alt, altIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: `answer-${index}-${blankIndex}-${altIndex}`, value: alt, onChange: (e) => {
|
|
244
|
+
const next = [...answerSlots];
|
|
245
|
+
next[altIndex] = e.target.value;
|
|
246
|
+
updateItem(index, { answers: writeBlankAnswers(item, blankIndex, next) });
|
|
247
|
+
}, placeholder: `Đáp án ${altIndex + 1}`, className: "h-10 min-w-0 border-gray-200 bg-white" }), answerSlots.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => updateItem(index, {
|
|
248
|
+
answers: writeBlankAnswers(item, blankIndex, answerSlots.filter((_, slotIndex) => slotIndex !== altIndex)),
|
|
249
|
+
}), className: "h-7 w-7 shrink-0 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `${index}-${blankIndex}-${altIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => updateItem(index, {
|
|
250
|
+
answers: writeBlankAnswers(item, blankIndex, [...answerSlots, '']),
|
|
251
|
+
}), 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"] })] }), showOptions ? (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs(Label, { children: ["L\u1EF1a ch\u1ECDn ", '{' + blankIndex + '}'] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
252
|
+
setOpenOptionKeys((prev) => {
|
|
253
|
+
const next = new Set(prev);
|
|
254
|
+
next.delete(optionKey);
|
|
255
|
+
return next;
|
|
256
|
+
});
|
|
257
|
+
if (!blankHasOptions(item.wordBank?.[blankIndex]?.options)) {
|
|
258
|
+
updateItem(index, { wordBank: clearBlankOptions(item, blankIndex) });
|
|
259
|
+
}
|
|
260
|
+
}, className: "h-7 px-2 text-xs text-gray-500 hover:text-gray-700", children: "\u1EA8n" })] }), _jsx("p", { className: "text-xs text-slate-500", children: "M\u1ED7i l\u1EF1a ch\u1ECDn m\u1ED9t \u00F4. C\u1EA7n \u00EDt nh\u1EA5t 2 \u0111\u1EC3 hi\u1EC7n dropdown." }), slots.map((option, optionIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: `options-${index}-${blankIndex}-${optionIndex}`, value: option, onChange: (e) => {
|
|
261
|
+
const next = [...slots];
|
|
262
|
+
next[optionIndex] = e.target.value;
|
|
263
|
+
updateItem(index, { wordBank: writeBlankOptions(item, blankIndex, next) });
|
|
264
|
+
}, placeholder: `Lựa chọn ${optionIndex + 1}`, className: "h-10 min-w-0 border-gray-200 bg-white" }), slots.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => updateItem(index, {
|
|
265
|
+
wordBank: writeBlankOptions(item, blankIndex, slots.filter((_, slotIndex) => slotIndex !== optionIndex)),
|
|
266
|
+
}), className: "h-7 w-7 shrink-0 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `${index}-${blankIndex}-${optionIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => updateItem(index, {
|
|
267
|
+
wordBank: writeBlankOptions(item, blankIndex, [...slots, '']),
|
|
268
|
+
}), 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(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
269
|
+
setOpenOptionKeys((prev) => new Set(prev).add(optionKey));
|
|
270
|
+
if (!item.wordBank?.[blankIndex]?.options?.length) {
|
|
271
|
+
updateItem(index, { wordBank: writeBlankOptions(item, blankIndex, ['', '']) });
|
|
272
|
+
}
|
|
273
|
+
}, className: "h-8 gap-1.5 border-indigo-200 px-2.5 text-xs text-indigo-700 hover:bg-indigo-50 hover:text-indigo-800", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), blankHasOptions(item.wordBank?.[blankIndex]?.options)
|
|
274
|
+
? 'Hiện lựa chọn'
|
|
275
|
+
: 'Thêm lựa chọn'] }))] }, blankIndex));
|
|
276
|
+
}) }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsx(FillInBlankGroupMediaField, { id: `fib-item-image-${index}`, label: "\u1EA2nh item (optional)", accept: "image/*", value: Array.isArray(item.imageUrl) ? (item.imageUrl[0] || '') : (item.imageUrl || ''), onChange: (url) => updateItem(index, { imageUrl: url }) }), !audioUrl && (_jsx(FillInBlankGroupMediaField, { id: `fib-item-audio-${index}`, label: "Audio item (khi kh\u00F4ng c\u00F3 audio nh\u00F3m)", accept: "audio/*", value: item.audioUrl || '', onChange: (url) => updateItem(index, { audioUrl: url }) }))] })] }, index))), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) })] })] })] }));
|
|
194
277
|
}
|
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { Button } from '../../../../components/ui/button';
|
|
5
4
|
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
6
5
|
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
7
6
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
|
-
|
|
7
|
+
function isDisplayableUrl(value) {
|
|
8
|
+
return (value.startsWith('http://') ||
|
|
9
|
+
value.startsWith('https://') ||
|
|
10
|
+
value.startsWith('blob:') ||
|
|
11
|
+
value.startsWith('data:'));
|
|
12
|
+
}
|
|
13
|
+
function toStorageKey(value) {
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return '';
|
|
17
|
+
if (isDisplayableUrl(trimmed))
|
|
18
|
+
return trimmed;
|
|
19
|
+
if (trimmed.startsWith('/images/') || trimmed.startsWith('/_next/')) {
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
return trimmed.replace(/^\/+/, '');
|
|
23
|
+
}
|
|
9
24
|
function resolveDisplayUrl(fileUrl, previewOverride, fetchedPreviewUrl) {
|
|
10
25
|
if (previewOverride)
|
|
11
26
|
return previewOverride;
|
|
12
27
|
if (fetchedPreviewUrl)
|
|
13
28
|
return fetchedPreviewUrl;
|
|
14
|
-
if (
|
|
29
|
+
if (isDisplayableUrl(fileUrl))
|
|
15
30
|
return fileUrl;
|
|
16
31
|
return '';
|
|
17
32
|
}
|
|
18
33
|
export function FillInBlankGroupMediaField({ id, label, accept, value, onChange, }) {
|
|
19
34
|
const [previewOverride, setPreviewOverride] = useState('');
|
|
20
|
-
const
|
|
35
|
+
const storageKey = toStorageKey(value);
|
|
36
|
+
const { previewUrl: fetchedPreviewUrl, isLoading } = usePresignedFileUrl(storageKey);
|
|
21
37
|
const displayUrl = resolveDisplayUrl(value, previewOverride, fetchedPreviewUrl);
|
|
22
38
|
const isImage = accept === 'image/*';
|
|
23
|
-
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: accept === 'image/*' ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (
|
|
24
|
-
setPreviewOverride('');
|
|
25
|
-
onChange('');
|
|
26
|
-
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })), value && !isImage && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("audio", { controls: true, src: displayUrl || value, className: "h-8 max-w-full" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
27
|
-
setPreviewOverride('');
|
|
28
|
-
onChange('');
|
|
29
|
-
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }))] }));
|
|
39
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: accept === 'image/*' ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-20 w-20 animate-pulse rounded-md bg-slate-100" })) : (_jsx(ImagePreview, { src: displayUrl, alt: label, className: "h-20 w-20 rounded-md object-contain" }))), value && !isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-8 w-48 animate-pulse rounded-md bg-slate-100" })) : (_jsx("audio", { controls: true, src: displayUrl, className: "h-8 max-w-full" })))] }));
|
|
30
40
|
}
|
|
@@ -13,6 +13,7 @@ export declare function extractPlaceholderIndices(question: string): number[];
|
|
|
13
13
|
export declare function blankSlotCount(question: string): number;
|
|
14
14
|
export declare function normalizeBlankAlts(value: unknown): string[];
|
|
15
15
|
export declare function normalizeAnswersMatrix(raw: unknown, requiredLength?: number): string[][];
|
|
16
|
+
export declare function acceptedAnswerList(alts: string[] | undefined): string[];
|
|
16
17
|
export declare function joinSlashAnswers(alts: string[] | undefined): string;
|
|
17
18
|
export declare function parseSlashAnswers(text: string): string[];
|
|
18
19
|
export declare function splitStem(question: string): FillBlankStemPart[];
|
|
@@ -47,8 +47,11 @@ export function normalizeAnswersMatrix(raw, requiredLength) {
|
|
|
47
47
|
}
|
|
48
48
|
return result;
|
|
49
49
|
}
|
|
50
|
+
export function acceptedAnswerList(alts) {
|
|
51
|
+
return (alts || []).map((item) => item.trim()).filter(Boolean);
|
|
52
|
+
}
|
|
50
53
|
export function joinSlashAnswers(alts) {
|
|
51
|
-
return (alts
|
|
54
|
+
return acceptedAnswerList(alts).join(' / ');
|
|
52
55
|
}
|
|
53
56
|
export function parseSlashAnswers(text) {
|
|
54
57
|
return text
|
package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Input } from '../../../../components/ui/input';
|
|
|
6
6
|
import { Check, X, Grid3X3, Trophy, AlertCircle, Lightbulb } from 'lucide-react';
|
|
7
7
|
import { toast } from 'sonner';
|
|
8
8
|
import { cn } from '../../../../shared/lib/utils';
|
|
9
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
9
10
|
function FillMissingWordsInGridClientContent({ questionData, onSubmit, isReviewMode = false, userAnswer = {}, explanation, }) {
|
|
10
11
|
const { content, answer } = questionData;
|
|
11
12
|
const { grid, rows, columns, disabledCells = [] } = content;
|
|
@@ -78,37 +79,37 @@ function FillMissingWordsInGridClientContent({ questionData, onSubmit, isReviewM
|
|
|
78
79
|
}, [missingPositions, userAnswers, correctAnswers, caseSensitive]);
|
|
79
80
|
const score = isReviewMode ? calculateScore() : null;
|
|
80
81
|
const scorePercentage = score ? Math.round((score.correct / score.total) * 100) : 0;
|
|
81
|
-
return (_jsxs(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
82
|
+
return (_jsxs(KidQuestionCard, { icon: _jsx(Grid3X3, { className: "h-3.5 w-3.5" }), title: "\u0110i\u1EC1n t\u1EEB c\u00F2n thi\u1EBFu v\u00E0o b\u1EA3ng", review: isReviewMode && score ? (score.correct === score.total ? 'correct' : 'wrong') : null, headerExtra: isReviewMode && score ? (_jsxs("div", { className: cn('flex items-center gap-2 rounded-lg px-2 py-1', scorePercentage >= 80 ? 'bg-green-50 border border-green-200' :
|
|
83
|
+
scorePercentage >= 50 ? 'bg-yellow-50 border border-yellow-200' :
|
|
84
|
+
'bg-red-50 border border-red-200'), children: [_jsx(Trophy, { className: cn('h-4 w-4', scorePercentage >= 80 ? 'text-green-500' :
|
|
85
|
+
scorePercentage >= 50 ? 'text-yellow-500' :
|
|
86
|
+
'text-red-500') }), _jsxs("span", { className: cn('text-xs font-bold', scorePercentage >= 80 ? 'text-green-600' :
|
|
87
|
+
scorePercentage >= 50 ? 'text-yellow-600' :
|
|
88
|
+
'text-red-600'), children: [score.correct, "/", score.total] })] })) : undefined, children: [_jsx("div", { className: "overflow-x-auto rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100 p-4", children: _jsxs("div", { className: "inline-block min-w-full", children: [_jsxs("div", { className: "grid gap-1.5 mb-2", style: { gridTemplateColumns: `48px repeat(${columns}, minmax(80px, 1fr))` }, children: [_jsx("div", { className: "h-8" }), Array.from({ length: columns }, (_, i) => (_jsx("div", { className: "flex h-8 items-center justify-center rounded-lg bg-gradient-to-br from-gray-100 to-gray-200 text-xs font-bold text-gray-600 shadow-sm", children: i + 1 }, `col-header-${i}`)))] }), _jsx("div", { className: "grid gap-1.5", children: Array.from({ length: rows }, (_, rowIndex) => (_jsxs("div", { className: "grid gap-1.5", style: { gridTemplateColumns: `48px repeat(${columns}, minmax(80px, 1fr))` }, children: [_jsx("div", { className: "flex h-14 items-center justify-center rounded-lg bg-gradient-to-br from-gray-100 to-gray-200 text-xs font-bold text-gray-600 shadow-sm", children: rowIndex + 1 }), Array.from({ length: columns }, (_, colIndex) => {
|
|
89
|
+
const isMissing = isMissingCell(rowIndex, colIndex);
|
|
90
|
+
const isDisabled = isDisabledCell(rowIndex, colIndex);
|
|
91
|
+
const cellValue = grid[rowIndex]?.[colIndex] || '';
|
|
92
|
+
const position = `${rowIndex + 1}-${colIndex + 1}`;
|
|
93
|
+
const userValue = userAnswers[position] || '';
|
|
94
|
+
// Determine if answer is correct in review mode
|
|
95
|
+
const answerStatus = isReviewMode ? isAnswerCorrect(rowIndex, colIndex) : null;
|
|
96
|
+
return (_jsx("div", { className: cn('relative flex h-14 items-center justify-center rounded-lg border-2 transition-all', isDisabled && 'border-gray-300 bg-gray-200', isMissing && !isReviewMode && 'border-blue-400 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm', isMissing && isReviewMode && answerStatus === true && 'border-green-400 bg-gradient-to-br from-green-50 to-emerald-50', isMissing && isReviewMode && answerStatus === false && 'border-red-400 bg-gradient-to-br from-red-50 to-rose-50', isMissing && isReviewMode && answerStatus === null && 'border-yellow-400 bg-gradient-to-br from-yellow-50 to-amber-50', !isMissing && !isDisabled && 'border-gray-200 bg-white'), children: isDisabled ? (
|
|
97
|
+
// Disabled cell - empty
|
|
98
|
+
_jsx("span", { className: "text-lg font-semibold text-gray-400" })) : isMissing ? (
|
|
99
|
+
// Missing cell - input field
|
|
100
|
+
_jsxs("div", { className: "relative w-full h-full flex items-center justify-center px-1", children: [_jsx(Input, { value: userValue, onChange: (e) => handleInputChange(rowIndex, colIndex, e.target.value), disabled: isReviewMode, className: cn('h-10 w-full text-center text-base font-semibold border-0 bg-transparent rounded-md', 'focus:ring-2 focus:ring-offset-0', !isReviewMode && 'focus:ring-blue-400', isReviewMode && answerStatus === true && 'text-green-700', isReviewMode && answerStatus === false && 'text-red-700', isReviewMode && answerStatus === null && 'text-yellow-700'), maxLength: 20, placeholder: "?" }), isReviewMode && answerStatus !== null && (_jsx("div", { className: cn('absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full shadow-sm', answerStatus === true ? 'bg-green-500' : 'bg-red-500'), children: answerStatus === true ? (_jsx(Check, { className: "h-3 w-3 text-white" })) : (_jsx(X, { className: "h-3 w-3 text-white" })) }))] })) : (
|
|
101
|
+
// Regular cell - display value
|
|
102
|
+
_jsx("span", { className: "text-base font-semibold text-gray-700", children: cellValue })) }, `cell-${rowIndex}-${colIndex}`));
|
|
103
|
+
})] }, `row-${rowIndex}`))) })] }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-4 text-xs", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-4 w-4 rounded border-2 border-blue-400 bg-blue-50" }), _jsx("span", { className: "text-gray-600", children: "\u00D4 c\u1EA7n \u0111i\u1EC1n" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-4 w-4 rounded border-2 border-gray-200 bg-white" }), _jsx("span", { className: "text-gray-600", children: "\u00D4 c\u00F3 s\u1EB5n" })] }), disabledCells.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-4 w-4 rounded border-2 border-gray-300 bg-gray-200" }), _jsx("span", { className: "text-gray-600", children: "\u00D4 b\u1ECB v\u00F4 hi\u1EC7u" })] }))] }), isReviewMode && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-slate-50 overflow-hidden", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-white px-4 py-3", children: [_jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-blue-500 text-white", children: _jsx(Check, { className: "h-3.5 w-3.5" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" })] }), _jsx("div", { className: "p-4", children: _jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5", children: missingPositions.map((position) => {
|
|
104
|
+
const userAns = userAnswers[position] || '';
|
|
105
|
+
const correctAns = correctAnswers[position] || '';
|
|
106
|
+
const isCorrect = caseSensitive
|
|
107
|
+
? userAns.trim() === correctAns.trim()
|
|
108
|
+
: userAns.trim().toLowerCase() === correctAns.trim().toLowerCase();
|
|
109
|
+
return (_jsxs("div", { className: cn('rounded-lg border-2 p-3 transition-all', isCorrect
|
|
110
|
+
? 'border-green-300 bg-gradient-to-br from-green-50 to-emerald-50'
|
|
111
|
+
: 'border-red-300 bg-gradient-to-br from-red-50 to-rose-50'), children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsxs("span", { className: cn('inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium', isCorrect ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'), children: ["\u00D4 ", position] }), isCorrect ? (_jsx(Check, { className: "h-4 w-4 text-green-500" })) : (_jsx(X, { className: "h-4 w-4 text-red-500" }))] }), _jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-500", children: "\u0110\u00FAng:" }), _jsx("span", { className: cn('font-semibold', isCorrect ? 'text-green-700' : 'text-gray-800'), children: correctAns })] }), !isCorrect && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-500", children: "B\u1EA1n:" }), _jsx("span", { className: "font-medium text-red-600", children: userAns || '(trống)' })] }))] })] }, position));
|
|
112
|
+
}) }) })] })), !isReviewMode && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [_jsx(AlertCircle, { className: "h-4 w-4" }), _jsxs("span", { children: ["\u0110\u00E3 \u0111i\u1EC1n: ", Object.values(userAnswers).filter(v => v.trim() !== '').length, "/", missingPositions.length, " \u00F4"] })] }), onSubmit && (_jsxs(Button, { onClick: handleSubmit, className: "bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white shadow-sm", children: [_jsx(Check, { className: "h-4 w-4 mr-2" }), "N\u1ED9p \u0111\u00E1p \u00E1n"] }))] })), isReviewMode && explanation && (_jsx("div", { className: "rounded-xl bg-gradient-to-r from-blue-50 to-indigo-50 p-4 border border-blue-200", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-white", children: _jsx(Lightbulb, { className: "h-3.5 w-3.5" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-xs font-semibold text-blue-700 uppercase tracking-wider", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-gray-700 leading-relaxed", children: explanation })] })] }) }))] }));
|
|
112
113
|
}
|
|
113
114
|
export function FillMissingWordsInGridClient(props) {
|
|
114
115
|
return _jsx(FillMissingWordsInGridClientContent, { ...props });
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { FindWordsInMatrixContent, FindWordsInMatrixCorrectAnswer } from '../../../../shared/types/questions/find-words-in-matrix';
|
|
2
|
+
import type { FindWordsInMatrixContent, FindWordsInMatrixCorrectAnswer, MatrixCellCoord } from '../../../../shared/types/questions/find-words-in-matrix';
|
|
3
|
+
type WordSelection = {
|
|
4
|
+
wordId?: string;
|
|
5
|
+
word?: string;
|
|
6
|
+
categoryId?: string;
|
|
7
|
+
path: MatrixCellCoord[];
|
|
8
|
+
};
|
|
3
9
|
interface FindWordsInMatrixClientProps {
|
|
4
10
|
content: FindWordsInMatrixContent;
|
|
5
11
|
correctAnswer?: FindWordsInMatrixCorrectAnswer;
|
|
6
12
|
showSolution?: boolean;
|
|
13
|
+
userAnswer?: unknown;
|
|
14
|
+
onAnswerChange?: (payload: {
|
|
15
|
+
words: WordSelection[];
|
|
16
|
+
}) => void;
|
|
7
17
|
}
|
|
8
|
-
export declare function FindWordsInMatrixClient({ content, correctAnswer, showSolution, }: FindWordsInMatrixClientProps): React.JSX.Element;
|
|
18
|
+
export declare function FindWordsInMatrixClient({ content, correctAnswer, showSolution, userAnswer, onAnswerChange, }: FindWordsInMatrixClientProps): React.JSX.Element;
|
|
9
19
|
export {};
|