@tinyweb_dev/oe-exam-sdk 1.0.9 → 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/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/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/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
- package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
- package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
- package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
- package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
- package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
- package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
- package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +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.js +21 -23
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
- package/dist/components/questions/viewer/QuestionViewer.js +2 -2
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/results/renderers/review-question-body-movers.js +1 -1
- package/dist/components/results/review-data.js +8 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +31 -5
- 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,10 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
5
|
+
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
6
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
7
|
function normalizeWord(word) {
|
|
9
8
|
return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
|
|
10
9
|
}
|
|
@@ -21,6 +20,14 @@ function formatAnswer(answer) {
|
|
|
21
20
|
return 'câu đúng (không gạch)';
|
|
22
21
|
return `gạch "${answer.crossOut}"${posInfo}`;
|
|
23
22
|
}
|
|
23
|
+
function isCorrectToken(correct, tokenIndex, plain) {
|
|
24
|
+
if (!correct)
|
|
25
|
+
return false;
|
|
26
|
+
if (typeof correct.wordIndex === 'number' && correct.wordIndex >= 0) {
|
|
27
|
+
return correct.wordIndex === tokenIndex;
|
|
28
|
+
}
|
|
29
|
+
return Boolean(correct.crossOut) && normalizeWord(correct.crossOut) === plain && plain !== '';
|
|
30
|
+
}
|
|
24
31
|
export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
25
32
|
const items = questionData.items || [];
|
|
26
33
|
const emit = (itemIndex, nextItem) => {
|
|
@@ -32,47 +39,58 @@ export function CrossOutWordGroupClient({ questionData, isReviewMode = false, us
|
|
|
32
39
|
next[itemIndex] = nextItem;
|
|
33
40
|
onAnswerChange(next);
|
|
34
41
|
};
|
|
35
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
42
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => {
|
|
36
43
|
const isReplace = item.type === 'CROSS_OUT_AND_REPLACE';
|
|
37
44
|
const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
|
|
38
45
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
39
|
-
const tokens =
|
|
46
|
+
const tokens = tokenizeCrossOutQuestion(item.question || '');
|
|
40
47
|
const selectedIndex = userValue?.wordIndex;
|
|
41
48
|
const selectedWord = userValue?.crossOut || '';
|
|
42
49
|
const replaceWith = userValue?.replaceWith || '';
|
|
43
50
|
const correct = item.correctAnswer;
|
|
44
|
-
return (_jsxs(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, headerExtra: isReviewMode ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
|
|
52
|
+
? 'bg-violet-100 text-violet-700'
|
|
53
|
+
: 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })) : null, children: [isReplace ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-semibold text-slate-600", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: !canSelect, onChange: (event) => emit(itemIndex, {
|
|
54
|
+
type: item.type,
|
|
55
|
+
wordIndex: selectedIndex ?? null,
|
|
56
|
+
crossOut: selectedWord,
|
|
57
|
+
replaceWith: event.target.value,
|
|
58
|
+
}), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 min-w-[10rem] rounded-lg border border-slate-200 px-2.5 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 disabled:bg-slate-50" })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-sm font-medium leading-relaxed text-slate-800", children: tokens.map((token, tokenIndex) => {
|
|
59
|
+
const plain = normalizeWord(token);
|
|
60
|
+
const isSelected = typeof selectedIndex === 'number'
|
|
61
|
+
? selectedIndex === tokenIndex
|
|
62
|
+
: Boolean(selectedWord) && normalizeWord(selectedWord) === plain && plain !== '';
|
|
63
|
+
const isCorrectTarget = isReviewMode && !item.isExample && correct?.wordIndex === tokenIndex;
|
|
64
|
+
const isSelectedCorrect = isReviewMode &&
|
|
65
|
+
!item.isExample &&
|
|
66
|
+
isSelected &&
|
|
67
|
+
isCorrectToken(correct, tokenIndex, plain);
|
|
68
|
+
const isSelectedWrong = isReviewMode &&
|
|
69
|
+
!item.isExample &&
|
|
70
|
+
isSelected &&
|
|
71
|
+
Boolean(correct) &&
|
|
72
|
+
!isSelectedCorrect;
|
|
73
|
+
return (_jsx("button", { type: "button", disabled: !canSelect || plain === '', onClick: () => {
|
|
74
|
+
const already = typeof selectedIndex === 'number'
|
|
54
75
|
? selectedIndex === tokenIndex
|
|
55
|
-
:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...(isReplace ? { replaceWith } : {}),
|
|
68
|
-
});
|
|
69
|
-
}, className: cn('rounded-lg px-2 py-1 transition-colors', isSelected
|
|
76
|
+
: selectedWord === token;
|
|
77
|
+
emit(itemIndex, {
|
|
78
|
+
type: item.type,
|
|
79
|
+
wordIndex: already ? null : tokenIndex,
|
|
80
|
+
crossOut: already ? '' : token,
|
|
81
|
+
...(isReplace ? { replaceWith } : {}),
|
|
82
|
+
});
|
|
83
|
+
}, className: cn('rounded-lg px-2 py-1 transition-colors', isSelectedWrong
|
|
84
|
+
? 'border border-rose-200 bg-rose-100 font-bold text-rose-700 line-through decoration-rose-600'
|
|
85
|
+
: isSelectedCorrect
|
|
86
|
+
? 'border border-emerald-300 bg-emerald-100 font-bold text-emerald-800 line-through decoration-emerald-600'
|
|
87
|
+
: isSelected
|
|
70
88
|
? 'border border-blue-200 bg-blue-100 font-bold text-blue-700 line-through decoration-blue-600'
|
|
71
89
|
: isCorrectTarget
|
|
72
90
|
? 'border-2 border-dashed border-emerald-500 bg-emerald-50 font-bold text-emerald-800'
|
|
73
91
|
: canSelect
|
|
74
92
|
? 'cursor-pointer hover:bg-blue-50 hover:text-blue-700'
|
|
75
93
|
: 'cursor-default'), children: token }, `${item.questionNumber}-token-${tokenIndex}`));
|
|
76
|
-
|
|
94
|
+
}) }), isReviewMode ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("span", { className: "font-medium", children: "\u0110\u00E1p \u00E1n: " }), formatAnswer(item.correctAnswer)] })) : null] }, `${item.questionNumber}-${itemIndex}`));
|
|
77
95
|
}), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
|
78
96
|
}
|
|
@@ -13,6 +13,8 @@ import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
|
13
13
|
import { HelpCircle, Plus, Star, Trash2 } from 'lucide-react';
|
|
14
14
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
15
15
|
import { CrossOutWordGroupClient } from './CrossOutWordGroupClient';
|
|
16
|
+
import { createCrossOutAnswer, createEmptyCrossOutItem, DEFAULT_CROSS_OUT_INSTRUCTION, } from './map-cross-out-word-group-data';
|
|
17
|
+
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
16
18
|
function parsePointsValue(raw, allowZero = false) {
|
|
17
19
|
if (raw === '')
|
|
18
20
|
return allowZero ? 0 : 1;
|
|
@@ -22,43 +24,9 @@ function parsePointsValue(raw, allowZero = false) {
|
|
|
22
24
|
const min = allowZero ? 0 : 1;
|
|
23
25
|
return Math.max(min, parsed);
|
|
24
26
|
}
|
|
25
|
-
const DEFAULT_INSTRUCTION = 'Cross out the extra or incorrect word. If the word is incorrect, write the correct word. If the line is correct, leave it blank.';
|
|
26
|
-
function tokenize(sentence) {
|
|
27
|
-
return sentence.trim().split(/\s+/).filter(Boolean);
|
|
28
|
-
}
|
|
29
|
-
function createAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
|
|
30
|
-
const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
|
|
31
|
-
? wordIndex
|
|
32
|
-
: wordIndex === null
|
|
33
|
-
? null
|
|
34
|
-
: undefined;
|
|
35
|
-
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
36
|
-
return {
|
|
37
|
-
type,
|
|
38
|
-
...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
|
|
39
|
-
crossOut,
|
|
40
|
-
replaceWith,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
type: 'CROSS_OUT',
|
|
45
|
-
...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
|
|
46
|
-
crossOut,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function createEmptyItem(questionNumber) {
|
|
50
|
-
return {
|
|
51
|
-
type: 'CROSS_OUT',
|
|
52
|
-
question: '',
|
|
53
|
-
correctAnswer: createAnswer('CROSS_OUT'),
|
|
54
|
-
isExample: false,
|
|
55
|
-
points: 1,
|
|
56
|
-
questionNumber,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
27
|
function normalizeItems(rawItems) {
|
|
60
28
|
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
61
|
-
return [
|
|
29
|
+
return [createEmptyCrossOutItem(1)];
|
|
62
30
|
}
|
|
63
31
|
return rawItems.map((item, index) => {
|
|
64
32
|
const type = item.type === 'CROSS_OUT_AND_REPLACE' ||
|
|
@@ -66,7 +34,7 @@ function normalizeItems(rawItems) {
|
|
|
66
34
|
? 'CROSS_OUT_AND_REPLACE'
|
|
67
35
|
: 'CROSS_OUT';
|
|
68
36
|
const rawIndex = item.correctAnswer?.wordIndex;
|
|
69
|
-
const correctAnswer =
|
|
37
|
+
const correctAnswer = createCrossOutAnswer(type, item.correctAnswer?.crossOut || '', item.correctAnswer?.replaceWith || '', rawIndex);
|
|
70
38
|
return {
|
|
71
39
|
type,
|
|
72
40
|
question: item.question || '',
|
|
@@ -81,7 +49,7 @@ function sumPoints(items) {
|
|
|
81
49
|
return items.reduce((total, item) => item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1), 0);
|
|
82
50
|
}
|
|
83
51
|
export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
84
|
-
const [instruction, setInstruction] = useState(initialData?.instruction ||
|
|
52
|
+
const [instruction, setInstruction] = useState(initialData?.instruction || DEFAULT_CROSS_OUT_INSTRUCTION);
|
|
85
53
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
86
54
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
87
55
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
@@ -121,7 +89,20 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
121
89
|
if (!item.question.trim()) {
|
|
122
90
|
nextErrors.push(`Câu ${index + 1}: Nội dung câu không được để trống.`);
|
|
123
91
|
}
|
|
92
|
+
const wordIndex = item.correctAnswer.wordIndex;
|
|
93
|
+
if (wordIndex === undefined) {
|
|
94
|
+
nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch hoặc để trống nếu câu đúng (wordIndex).`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof wordIndex === 'number') {
|
|
97
|
+
const tokens = tokenizeCrossOutQuestion(item.question);
|
|
98
|
+
if (tokens[wordIndex] !== item.correctAnswer.crossOut) {
|
|
99
|
+
nextErrors.push(`Câu ${index + 1}: Từ gạch phải khớp token tại wordIndex.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
124
102
|
if (item.type === 'CROSS_OUT_AND_REPLACE') {
|
|
103
|
+
if (typeof wordIndex !== 'number') {
|
|
104
|
+
nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch (wordIndex).`);
|
|
105
|
+
}
|
|
125
106
|
if (!item.correctAnswer.crossOut.trim()) {
|
|
126
107
|
nextErrors.push(`Câu ${index + 1}: Cần chọn hoặc nhập từ cần gạch (crossOut).`);
|
|
127
108
|
}
|
|
@@ -160,7 +141,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
160
141
|
next[index] = {
|
|
161
142
|
...current,
|
|
162
143
|
type,
|
|
163
|
-
correctAnswer:
|
|
144
|
+
correctAnswer: createCrossOutAnswer(type, current.correctAnswer.crossOut, current.correctAnswer.replaceWith || '', current.correctAnswer.wordIndex),
|
|
164
145
|
};
|
|
165
146
|
return next;
|
|
166
147
|
});
|
|
@@ -174,7 +155,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
174
155
|
const type = current.type;
|
|
175
156
|
next[index] = {
|
|
176
157
|
...current,
|
|
177
|
-
correctAnswer:
|
|
158
|
+
correctAnswer: createCrossOutAnswer(type, patch.crossOut !== undefined ? patch.crossOut : current.correctAnswer.crossOut, patch.replaceWith !== undefined
|
|
178
159
|
? patch.replaceWith
|
|
179
160
|
: current.correctAnswer.replaceWith || '', patch.wordIndex !== undefined ? patch.wordIndex : current.correctAnswer.wordIndex),
|
|
180
161
|
};
|
|
@@ -194,15 +175,14 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
194
175
|
});
|
|
195
176
|
}
|
|
196
177
|
else {
|
|
197
|
-
const cleanWord = token.replace(/[^\p{L}\p{N}'-]/gu, '') || token;
|
|
198
178
|
updateItemAnswer(itemIndex, {
|
|
199
179
|
wordIndex: tokenIndex,
|
|
200
|
-
crossOut:
|
|
180
|
+
crossOut: token,
|
|
201
181
|
});
|
|
202
182
|
}
|
|
203
183
|
};
|
|
204
184
|
const addItem = () => {
|
|
205
|
-
setItems((prev) => [...prev,
|
|
185
|
+
setItems((prev) => [...prev, createEmptyCrossOutItem(prev.length + 1)]);
|
|
206
186
|
};
|
|
207
187
|
const removeItem = (index) => {
|
|
208
188
|
setItems((prev) => {
|
|
@@ -217,8 +197,8 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
217
197
|
if (isClientMode) {
|
|
218
198
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(CrossOutWordGroupClient, { questionData: currentPayload })] }));
|
|
219
199
|
}
|
|
220
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder:
|
|
221
|
-
const tokens =
|
|
200
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: DEFAULT_CROSS_OUT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] }) }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["Danh s\u00E1ch c\u00E2u (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u00E2u"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => {
|
|
201
|
+
const tokens = tokenizeCrossOutQuestion(item.question);
|
|
222
202
|
const selectedWordIndex = item.correctAnswer.wordIndex;
|
|
223
203
|
return (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700", children: item.questionNumber || index + 1 }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", index + 1] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("div", { className: "w-52", children: _jsxs(Select, { value: item.type, onValueChange: (value) => updateItemType(index, value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Lo\u1EA1i c\u00E2u" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "CROSS_OUT", children: "Ch\u1EC9 g\u1EA1ch (CROSS_OUT)" }), _jsx(SelectItem, { value: "CROSS_OUT_AND_REPLACE", children: "G\u1EA1ch + thay (CROSS_OUT_AND_REPLACE)" })] })] }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, {
|
|
224
204
|
isExample: checked,
|
package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js
CHANGED
|
@@ -14,20 +14,18 @@ function normalizeMode(type) {
|
|
|
14
14
|
export function createCrossOutAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
|
|
15
15
|
const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
|
|
16
16
|
? wordIndex
|
|
17
|
-
:
|
|
18
|
-
? null
|
|
19
|
-
: undefined;
|
|
17
|
+
: null;
|
|
20
18
|
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
21
19
|
return {
|
|
22
20
|
type,
|
|
23
|
-
|
|
21
|
+
wordIndex: parsedIndex,
|
|
24
22
|
crossOut,
|
|
25
23
|
replaceWith,
|
|
26
24
|
};
|
|
27
25
|
}
|
|
28
26
|
return {
|
|
29
27
|
type: 'CROSS_OUT',
|
|
30
|
-
|
|
28
|
+
wordIndex: parsedIndex,
|
|
31
29
|
crossOut,
|
|
32
30
|
};
|
|
33
31
|
}
|
|
@@ -47,10 +45,12 @@ export function mapCrossOutWordGroupItem(item, index, fallbackAnswer) {
|
|
|
47
45
|
const correctAnswerRaw = asRecord(record.correctAnswer);
|
|
48
46
|
const answerObj = isRecord(correctAnswerRaw.answer)
|
|
49
47
|
? correctAnswerRaw.answer
|
|
50
|
-
:
|
|
51
|
-
?
|
|
52
|
-
:
|
|
53
|
-
|
|
48
|
+
: 'crossOut' in correctAnswerRaw || 'wordIndex' in correctAnswerRaw
|
|
49
|
+
? correctAnswerRaw
|
|
50
|
+
: isRecord(fallbackAnswer)
|
|
51
|
+
? fallbackAnswer
|
|
52
|
+
: {};
|
|
53
|
+
const type = normalizeMode(content.type || record.type || answerObj.type);
|
|
54
54
|
const isExample = record.isExample === true;
|
|
55
55
|
const rawIndex = typeof answerObj.wordIndex === 'number'
|
|
56
56
|
? answerObj.wordIndex
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function normalizeCrossOutQuestionText(question) {
|
|
2
|
+
return question
|
|
3
|
+
.replace(/\\n/g, ' ')
|
|
4
|
+
.replace(/\r\n|\r|\n/g, ' ')
|
|
5
|
+
.replace(/\s+/g, ' ')
|
|
6
|
+
.trim();
|
|
7
|
+
}
|
|
8
|
+
export function tokenizeCrossOutQuestion(question) {
|
|
9
|
+
const normalized = normalizeCrossOutQuestionText(question);
|
|
10
|
+
return normalized ? normalized.split(' ').filter(Boolean) : [];
|
|
11
|
+
}
|
|
@@ -16,12 +16,10 @@ function normalizeAnswer(answer, fallbackType) {
|
|
|
16
16
|
const rawIndex = answer?.wordIndex;
|
|
17
17
|
const wordIndex = typeof rawIndex === 'number' && Number.isInteger(rawIndex) && rawIndex >= 0
|
|
18
18
|
? rawIndex
|
|
19
|
-
:
|
|
20
|
-
? null
|
|
21
|
-
: undefined;
|
|
19
|
+
: null;
|
|
22
20
|
const result = {
|
|
23
21
|
type,
|
|
24
|
-
|
|
22
|
+
wordIndex,
|
|
25
23
|
crossOut: typeof answer?.crossOut === 'string' ? answer.crossOut.trim() : '',
|
|
26
24
|
};
|
|
27
25
|
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
@@ -5,7 +5,8 @@ interface CrosswordPuzzleClientProps {
|
|
|
5
5
|
correctAnswer?: CrosswordPuzzleCorrectAnswer;
|
|
6
6
|
isReviewMode?: boolean;
|
|
7
7
|
userAnswers?: Record<string, string>;
|
|
8
|
+
questionNumber?: number;
|
|
8
9
|
onAnswerChange?: (key: string, value: string) => void;
|
|
9
10
|
}
|
|
10
|
-
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, onAnswerChange, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
11
|
+
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, questionNumber, onAnswerChange, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
11
12
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, onAnswerChange, }) {
|
|
4
|
+
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, questionNumber, onAnswerChange, }) {
|
|
5
5
|
const rows = content?.gridSize?.rows || 12;
|
|
6
6
|
const cols = content?.gridSize?.cols || 12;
|
|
7
|
+
const instructionText = (content?.instruction || content?.title || '').trim();
|
|
8
|
+
const displayNumber = questionNumber ?? 1;
|
|
7
9
|
const acrossClues = useMemo(() => content?.clues?.across || [], [content?.clues?.across]);
|
|
8
10
|
const downClues = useMemo(() => content?.clues?.down || [], [content?.clues?.down]);
|
|
9
11
|
// Build grid cell mapping
|
|
@@ -30,14 +32,14 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
30
32
|
}
|
|
31
33
|
return { cellMap: cMap, clueNumberMap: numMap };
|
|
32
34
|
}, [acrossClues, downClues]);
|
|
33
|
-
return (_jsxs("div", { className: "flex flex-col
|
|
35
|
+
return (_jsxs("div", { className: "flex flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [_jsxs("span", { className: "font-bold", children: [displayNumber, "."] }), instructionText ? ` ${instructionText}` : ''] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("div", { className: "lg:col-span-5 flex flex-col gap-4", children: [_jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-semibold", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: acrossClues.map((c) => {
|
|
34
36
|
const answer = correctAnswer?.answers?.[`across-${c.number}`];
|
|
35
37
|
const userAns = userAnswers[`across-${c.number}`];
|
|
36
|
-
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-blue-100", children: [_jsxs("span", { className: "font-bold text-blue-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [
|
|
38
|
+
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-blue-100", children: [_jsxs("span", { className: "font-bold text-blue-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`across-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-blue-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-blue-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `across-${c.number}`));
|
|
37
39
|
}) })] }), _jsxs("div", { className: "rounded-lg border border-purple-200 bg-purple-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-purple-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-purple-600 text-white text-xs font-semibold", children: "Down" }), _jsx("span", { children: "H\u00E0ng d\u1ECDc" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: downClues.map((c) => {
|
|
38
40
|
const answer = correctAnswer?.answers?.[`down-${c.number}`];
|
|
39
41
|
const userAns = userAnswers[`down-${c.number}`];
|
|
40
|
-
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-purple-100", children: [_jsxs("span", { className: "font-bold text-purple-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [
|
|
42
|
+
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-purple-100", children: [_jsxs("span", { className: "font-bold text-purple-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`down-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-purple-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-purple-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `down-${c.number}`));
|
|
41
43
|
}) })] })] }), _jsx("div", { className: "lg:col-span-7 flex justify-center overflow-x-auto p-4 bg-gray-50 rounded-xl border border-gray-200", children: _jsx("div", { className: "inline-grid gap-1", style: {
|
|
42
44
|
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
43
45
|
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
@@ -90,5 +92,5 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
90
92
|
}
|
|
91
93
|
return (_jsxs("div", { className: `relative flex items-center justify-center rounded border-2 font-bold shadow-sm h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 text-sm sm:text-base md:text-lg ${cellBg} ${textColor}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
|
|
92
94
|
});
|
|
93
|
-
}) }) })] })] }));
|
|
95
|
+
}) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
|
|
94
96
|
}
|