@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
|
@@ -102,7 +102,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
102
102
|
}
|
|
103
103
|
case 'CROSSWORD_PUZZLE': {
|
|
104
104
|
const data = transformCrosswordPuzzle(questions);
|
|
105
|
-
return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
105
|
+
return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, markedColumn: data.markedColumn, hiddenWord: data.hiddenWord, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, caseSensitive: data.caseSensitive }));
|
|
106
106
|
}
|
|
107
107
|
case 'IMAGE_OBJECT_MATCHING': {
|
|
108
108
|
// Reuse coloring renderer — same content structure as READ_AND_COLOR_OBJECTS
|
|
@@ -125,10 +125,6 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
125
125
|
onAnswerChange(regionId, hintItemId);
|
|
126
126
|
}, isReviewMode: isReviewMode }));
|
|
127
127
|
}
|
|
128
|
-
case 'CROSSWORD_PUZZLE': {
|
|
129
|
-
const data = transformCrosswordPuzzle(questions);
|
|
130
|
-
return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
131
|
-
}
|
|
132
128
|
case 'SORT_WORDS_INTO_CATEGORIES': {
|
|
133
129
|
const data = transformSortWordsIntoCategories(questions);
|
|
134
130
|
const gradableCount = data.words.filter((word) => !word.isExample).length;
|
package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js
CHANGED
|
@@ -8,9 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
import { useMemo } from 'react';
|
|
9
9
|
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
10
10
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
11
|
-
|
|
12
|
-
return sentence.trim().split(/\s+/).filter(Boolean);
|
|
13
|
-
}
|
|
11
|
+
import { tokenizeCrossOutQuestion } from '../../../../questions/types/cross-out-word-group/tokenize';
|
|
14
12
|
function normalizeWord(word) {
|
|
15
13
|
return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
|
|
16
14
|
}
|
|
@@ -27,12 +25,20 @@ function formatAnswerLabel(answer) {
|
|
|
27
25
|
return 'câu đúng (không gạch)';
|
|
28
26
|
return `gạch "${answer.crossOut}"${posInfo}`;
|
|
29
27
|
}
|
|
28
|
+
function isCorrectToken(correct, tokenIndex, plain) {
|
|
29
|
+
if (!correct)
|
|
30
|
+
return false;
|
|
31
|
+
if (typeof correct.wordIndex === 'number' && correct.wordIndex >= 0) {
|
|
32
|
+
return correct.wordIndex === tokenIndex;
|
|
33
|
+
}
|
|
34
|
+
return Boolean(correct.crossOut) && normalizeWord(correct.crossOut) === plain && plain !== '';
|
|
35
|
+
}
|
|
30
36
|
function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswer, id, }) {
|
|
31
37
|
const isDisabled = isReviewMode || question.isExample;
|
|
32
38
|
const currentAnswer = question.isExample
|
|
33
39
|
? question.exampleCorrectAnswer
|
|
34
40
|
: answer;
|
|
35
|
-
const tokens = useMemo(() =>
|
|
41
|
+
const tokens = useMemo(() => tokenizeCrossOutQuestion(question.question), [question.question]);
|
|
36
42
|
const selectedWord = currentAnswer?.crossOut || '';
|
|
37
43
|
const selectedIndex = currentAnswer?.wordIndex;
|
|
38
44
|
const replaceWith = currentAnswer?.replaceWith || '';
|
|
@@ -76,11 +82,10 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
|
|
|
76
82
|
});
|
|
77
83
|
}
|
|
78
84
|
else {
|
|
79
|
-
const cleanWord = rawToken.replace(/[^\p{L}\p{N}'-]/gu, '') || rawToken;
|
|
80
85
|
onAnswerChange(question.id, {
|
|
81
86
|
type: mode,
|
|
82
87
|
wordIndex: tokenIndex,
|
|
83
|
-
crossOut:
|
|
88
|
+
crossOut: rawToken,
|
|
84
89
|
...(mode === 'CROSS_OUT_AND_REPLACE' ? { replaceWith } : {}),
|
|
85
90
|
});
|
|
86
91
|
}
|
|
@@ -109,9 +114,9 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
|
|
|
109
114
|
? 'bg-emerald-500'
|
|
110
115
|
: isWrong
|
|
111
116
|
? 'bg-rose-500'
|
|
112
|
-
: 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full border border-amber-300 bg-amber-100/90 px-2 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800", children: "EXAMPLE" })), _jsx("span", { className: `rounded-full px-2.5 py-0.5 text-[11px] font-bold ${mode === 'CROSS_OUT_AND_REPLACE'
|
|
117
|
+
: 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full border border-amber-300 bg-amber-100/90 px-2 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800", children: "EXAMPLE" })), isReviewMode ? (_jsx("span", { className: `rounded-full px-2.5 py-0.5 text-[11px] font-bold ${mode === 'CROSS_OUT_AND_REPLACE'
|
|
113
118
|
? 'bg-violet-100/80 text-violet-700 border border-violet-200/60'
|
|
114
|
-
: 'bg-sky-100/80 text-sky-700 border border-sky-200/60'}`, children: mode === 'CROSS_OUT_AND_REPLACE' ? 'Gạch + Thay thế' : 'Chỉ gạch' }), !question.isExample && selectedWord && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10.5px] font-bold text-emerald-700", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }), "\u0110\u00E3 g\u1EA1ch: \"", selectedWord, "\"", typeof selectedIndex === 'number' && (_jsxs("span", { className: "text-[10px] text-emerald-600 font-normal", children: ["(t\u1EEB #", selectedIndex + 1, ")"] }))] }))] }), mode === 'CROSS_OUT_AND_REPLACE' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-bold text-slate-600 whitespace-nowrap", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: isDisabled, onChange: (e) => setReplaceWith(e.target.value), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 w-40 sm:w-52 rounded-lg border border-slate-200 bg-white px-2.5 text-sm font-semibold text-slate-800 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-slate-50" })] }))] }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2 text-[15.5px] sm:text-[16.5px] font-medium leading-loose text-slate-800 px-1", children: tokens.map((token, tokenIndex) => {
|
|
119
|
+
: 'bg-sky-100/80 text-sky-700 border border-sky-200/60'}`, children: mode === 'CROSS_OUT_AND_REPLACE' ? 'Gạch + Thay thế' : 'Chỉ gạch' })) : null, !question.isExample && selectedWord && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10.5px] font-bold text-emerald-700", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }), "\u0110\u00E3 g\u1EA1ch: \"", selectedWord, "\"", typeof selectedIndex === 'number' && (_jsxs("span", { className: "text-[10px] text-emerald-600 font-normal", children: ["(t\u1EEB #", selectedIndex + 1, ")"] }))] }))] }), mode === 'CROSS_OUT_AND_REPLACE' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-bold text-slate-600 whitespace-nowrap", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: isDisabled, onChange: (e) => setReplaceWith(e.target.value), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 w-40 sm:w-52 rounded-lg border border-slate-200 bg-white px-2.5 text-sm font-semibold text-slate-800 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-slate-50" })] }))] }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2 text-[15.5px] sm:text-[16.5px] font-medium leading-loose text-slate-800 px-1", children: tokens.map((token, tokenIndex) => {
|
|
115
120
|
const plain = normalizeWord(token);
|
|
116
121
|
const isSelected = selectedIndex !== undefined && selectedIndex !== null
|
|
117
122
|
? selectedIndex === tokenIndex
|
|
@@ -119,12 +124,21 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
|
|
|
119
124
|
const isCorrectTargetInReview = isReviewMode &&
|
|
120
125
|
!question.isExample &&
|
|
121
126
|
correctAnswer?.wordIndex === tokenIndex;
|
|
127
|
+
const isSelectedCorrect = isReviewMode &&
|
|
128
|
+
!question.isExample &&
|
|
129
|
+
isSelected &&
|
|
130
|
+
isCorrectToken(correctAnswer, tokenIndex, plain);
|
|
131
|
+
const isSelectedWrong = isReviewMode &&
|
|
132
|
+
!question.isExample &&
|
|
133
|
+
isSelected &&
|
|
134
|
+
!!correctAnswer &&
|
|
135
|
+
!isSelectedCorrect;
|
|
122
136
|
return (_jsx("button", { type: "button", disabled: isDisabled || plain === '', onClick: () => setCrossOut(tokenIndex, token), className: `rounded-lg px-2 py-1 transition-all duration-150 select-none relative ${isSelected
|
|
123
137
|
? question.isExample
|
|
124
138
|
? 'bg-amber-200/90 text-amber-900 font-bold line-through decoration-amber-700 decoration-2 shadow-sm'
|
|
125
|
-
:
|
|
139
|
+
: isSelectedCorrect
|
|
126
140
|
? 'bg-emerald-100 text-emerald-800 font-bold line-through decoration-emerald-600 decoration-2 shadow-sm border border-emerald-300'
|
|
127
|
-
:
|
|
141
|
+
: isSelectedWrong
|
|
128
142
|
? 'bg-rose-100 text-rose-700 font-bold line-through decoration-rose-600 decoration-2 shadow-sm border border-rose-300 scale-[0.98]'
|
|
129
143
|
: 'bg-blue-100 text-blue-700 font-bold line-through decoration-blue-600 decoration-2 shadow-sm border border-blue-200 scale-[0.98]'
|
|
130
144
|
: isCorrectTargetInReview
|
package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js
CHANGED
|
@@ -307,5 +307,5 @@ export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCo
|
|
|
307
307
|
inputRefs.current.delete(key);
|
|
308
308
|
}, type: "text", maxLength: 1, disabled: Boolean(isExample), value: cellValue, onChange: (e) => handleInputChange(e.target.value, r, c), onKeyDown: (e) => handleKeyDown(e, r, c), onFocus: () => handleCellClick(r, c), className: `w-full h-full text-center bg-transparent uppercase font-bold outline-none ${isExample ? 'cursor-default' : 'cursor-pointer'}` })) : (_jsxs("div", { className: "flex flex-col items-center justify-center", children: [_jsx("span", { children: cellValue || '—' }), reviewStatus && !reviewStatus.isCorrect && (_jsx("span", { className: "absolute -bottom-1 -right-1 text-[9px] bg-green-600 text-white font-bold px-1 rounded-full shadow", children: reviewStatus.expected }))] })), isReviewMode && reviewStatus && (_jsx("span", { className: `absolute -top-1 -right-1 rounded-full p-0.5 text-white ${reviewStatus.isCorrect ? 'bg-green-500' : 'bg-red-500'}`, children: reviewStatus.isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5" })) : (_jsx(X, { className: "h-2.5 w-2.5" })) }))] }, key));
|
|
309
309
|
});
|
|
310
|
-
}) }) })] })] })] }));
|
|
310
|
+
}) }) }), (markedColumn || hiddenWord) && (_jsxs("div", { className: "w-full max-w-md rounded-xl border-2 border-amber-200 bg-amber-50 p-4", children: [_jsx("label", { className: "block text-sm font-bold text-amber-900 mb-2", children: "What's the hidden word?" }), !isReviewMode ? (_jsx("input", { type: "text", value: answers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded-lg border border-amber-300 bg-white px-3 py-2 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [answers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: answers['hidden-word'] })), hiddenWord && (_jsxs("div", { className: "mt-1 text-xs", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: hiddenWord })] }))] }))] }))] })] })] }));
|
|
311
311
|
}
|
package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
|
8
8
|
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
9
9
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
10
10
|
import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
|
|
11
|
+
import { FillInBlankAnswerKey } from '../../../../questions/types/fill-in-blank-group/FillInBlankAnswerKey';
|
|
11
12
|
import { extractPlaceholderIndices, isBlankMatch, padStudentAnswers, splitStem, toFillInBlankImageUrls, } from '../../../../questions/types/fill-in-blank-group/blank-utils';
|
|
12
13
|
function emptyAnswer(blankCount) {
|
|
13
14
|
return { answers: Array.from({ length: blankCount }, () => '') };
|
|
@@ -53,7 +54,7 @@ function ItemCard({ question, answer, onAnswerChange, isReviewMode, correctAnswe
|
|
|
53
54
|
: blankWrong
|
|
54
55
|
? 'border-rose-400 bg-rose-50 text-rose-800'
|
|
55
56
|
: 'border-blue-200 bg-blue-50/40 text-blue-900 focus:border-blue-500 focus:bg-white'}` }, partIndex));
|
|
56
|
-
}) })] }), isReviewMode && !question.isExample && expected.length > 0 && (
|
|
57
|
+
}) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsx("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2", children: _jsx(FillInBlankAnswerKey, { answersByBlank: expected, blankIndices: blankIndices }) }))] }));
|
|
57
58
|
}
|
|
58
59
|
export function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints = false, hints = [], questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap, }) {
|
|
59
60
|
return (_jsxs("div", { className: "relative flex h-full flex-col gap-4 overflow-y-auto custom-scrollbar sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 p-3 sm:gap-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), (audioUrl || toFillInBlankImageUrls(imageUrl).length > 0) && (_jsxs("div", { className: "sticky top-0 z-20 flex flex-col gap-3 border-b border-gray-100 bg-white/95 p-3 pb-2 shadow-sm backdrop-blur sm:px-6", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), toFillInBlankImageUrls(imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(imageUrl).map((url, imageIndex) => (_jsx(ResolvedImage, { src: url, alt: `Shared illustration ${imageIndex + 1}`, className: "max-h-[180px] rounded-lg object-contain" }, `group-image-${imageIndex}`))) }))] })), showHints && hints.length > 0 && !isReviewMode && (_jsx("div", { className: "flex flex-wrap gap-2 px-3 sm:px-6", children: hints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:px-6 lg:grid-cols-3", children: questions.map((question) => (_jsx(ItemCard, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap?.[question.id] }, question.id))) })] }));
|
|
@@ -37,14 +37,24 @@ export function SpeakingSpontaneousQaGroupRenderer(props) {
|
|
|
37
37
|
useEffect(() => {
|
|
38
38
|
setSequenceStep(getInitialSequenceStep());
|
|
39
39
|
}, [getInitialSequenceStep, setup.nav.currentIndex]);
|
|
40
|
+
const hasQuestionAudio = Boolean(content?.audioUrl?.trim());
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
if (sequenceStep !== SPONTANEOUS_QA_SEQUENCE_STEP.DELAYING || shouldResumeCompletedState)
|
|
42
43
|
return;
|
|
43
44
|
const timer = setTimeout(() => {
|
|
44
|
-
setSequenceStep(
|
|
45
|
+
setSequenceStep(hasQuestionAudio
|
|
46
|
+
? SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION
|
|
47
|
+
: SPONTANEOUS_QA_SEQUENCE_STEP.ALLOW_RECORDING);
|
|
45
48
|
}, 3000);
|
|
46
49
|
return () => clearTimeout(timer);
|
|
47
|
-
}, [sequenceStep, shouldResumeCompletedState]);
|
|
50
|
+
}, [hasQuestionAudio, sequenceStep, shouldResumeCompletedState]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (sequenceStep === SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION &&
|
|
53
|
+
!hasQuestionAudio &&
|
|
54
|
+
!shouldResumeCompletedState) {
|
|
55
|
+
setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.ALLOW_RECORDING);
|
|
56
|
+
}
|
|
57
|
+
}, [hasQuestionAudio, sequenceStep, shouldResumeCompletedState]);
|
|
48
58
|
const handleTeacherVideoEnded = useCallback(() => {
|
|
49
59
|
setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.DELAYING);
|
|
50
60
|
props.onTeacherIntroPlayed?.();
|
|
@@ -27,8 +27,9 @@ export function useSpontaneousQaGroupRendererSetup({ part, answers, onAnswerChan
|
|
|
27
27
|
flattenIndex: child.flattenIndex,
|
|
28
28
|
expectedAnswers: child.expectedAnswers,
|
|
29
29
|
points: child.points,
|
|
30
|
+
isExample: child.isExample,
|
|
30
31
|
},
|
|
31
|
-
is_example:
|
|
32
|
+
is_example: child.isExample,
|
|
32
33
|
correct_answer: { expectedAnswers: child.expectedAnswers },
|
|
33
34
|
total_points: String(child.points),
|
|
34
35
|
question_type: 'SPONTANEOUS_QA_GROUP',
|
|
@@ -263,6 +263,16 @@ export function transformAnswersForSubmission(answers, parts) {
|
|
|
263
263
|
if (speakingQuestionIds.has(questionId) && typeof answer === 'string') {
|
|
264
264
|
continue;
|
|
265
265
|
}
|
|
266
|
+
if (questionId === 'hidden-word') {
|
|
267
|
+
const parent = findGridParentQuestion(parts);
|
|
268
|
+
if (parent?.questionType === 'CROSSWORD_PUZZLE') {
|
|
269
|
+
if (!gridCellAnswers.has(parent.parentId)) {
|
|
270
|
+
gridCellAnswers.set(parent.parentId, {});
|
|
271
|
+
}
|
|
272
|
+
gridCellAnswers.get(parent.parentId)['hidden-word'] = String(answer);
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
266
276
|
// Handle grid cell keys (from FILL_MISSING_WORDS_IN_GRID)
|
|
267
277
|
if (isGridCellKey(questionId)) {
|
|
268
278
|
const parent = findGridParentQuestion(parts);
|
|
@@ -454,9 +464,14 @@ export function transformAnswersForSubmission(answers, parts) {
|
|
|
454
464
|
words[`down-${c.number}`] = word;
|
|
455
465
|
}
|
|
456
466
|
}
|
|
467
|
+
const hiddenWord = cellAnswers['hidden-word'];
|
|
468
|
+
const cells = Object.fromEntries(Object.entries(cellAnswers).filter(([key]) => key !== 'hidden-word'));
|
|
469
|
+
if (hiddenWord) {
|
|
470
|
+
words['hidden-word'] = hiddenWord;
|
|
471
|
+
}
|
|
457
472
|
result.push({
|
|
458
473
|
questionId: parentId,
|
|
459
|
-
answer: { answers: words, cells
|
|
474
|
+
answer: { answers: words, cells },
|
|
460
475
|
});
|
|
461
476
|
}
|
|
462
477
|
else {
|
|
@@ -63,21 +63,19 @@ export function normalizeCrossOutWordGroupAnswer(raw) {
|
|
|
63
63
|
const rawIndex = obj.wordIndex;
|
|
64
64
|
const wordIndex = typeof rawIndex === 'number' && Number.isInteger(rawIndex) && rawIndex >= 0
|
|
65
65
|
? rawIndex
|
|
66
|
-
:
|
|
67
|
-
? null
|
|
68
|
-
: undefined;
|
|
66
|
+
: null;
|
|
69
67
|
return type === 'CROSS_OUT_AND_REPLACE'
|
|
70
68
|
? {
|
|
71
69
|
type,
|
|
72
|
-
|
|
70
|
+
wordIndex,
|
|
73
71
|
crossOut: typeof obj.crossOut === 'string' ? obj.crossOut : '',
|
|
74
72
|
replaceWith: typeof obj.replaceWith === 'string' ? obj.replaceWith : '',
|
|
75
73
|
}
|
|
76
74
|
: {
|
|
77
75
|
type: 'CROSS_OUT',
|
|
78
|
-
|
|
76
|
+
wordIndex,
|
|
79
77
|
crossOut: typeof obj.crossOut === 'string' ? obj.crossOut : '',
|
|
80
78
|
};
|
|
81
79
|
}
|
|
82
|
-
return { type: 'CROSS_OUT', crossOut: '' };
|
|
80
|
+
return { type: 'CROSS_OUT', wordIndex: null, crossOut: '' };
|
|
83
81
|
}
|
|
@@ -472,14 +472,19 @@ export interface CrosswordClueItem {
|
|
|
472
472
|
startRow: number;
|
|
473
473
|
startCol: number;
|
|
474
474
|
length: number;
|
|
475
|
+
isExample?: boolean;
|
|
475
476
|
}
|
|
476
477
|
export interface CrosswordPuzzlePartData {
|
|
478
|
+
type?: 'STANDARD' | 'HIDDEN_WORD';
|
|
477
479
|
title?: string;
|
|
478
480
|
instruction: string;
|
|
479
481
|
gridSize: {
|
|
480
482
|
rows: number;
|
|
481
483
|
cols: number;
|
|
482
484
|
};
|
|
485
|
+
markedColumn?: number;
|
|
486
|
+
hiddenWord?: string;
|
|
487
|
+
caseSensitive?: boolean;
|
|
483
488
|
clues: {
|
|
484
489
|
across: CrosswordClueItem[];
|
|
485
490
|
down: CrosswordClueItem[];
|
|
@@ -1258,6 +1258,7 @@ export function transformCrosswordPuzzle(questions) {
|
|
|
1258
1258
|
startRow: Number(c.startRow) || 1,
|
|
1259
1259
|
startCol: Number(c.startCol) || 1,
|
|
1260
1260
|
length: Number(c.length) || 1,
|
|
1261
|
+
...(c.isExample === true ? { isExample: true } : {}),
|
|
1261
1262
|
}))
|
|
1262
1263
|
: [];
|
|
1263
1264
|
const down = Array.isArray(content?.clues?.down)
|
|
@@ -1267,15 +1268,20 @@ export function transformCrosswordPuzzle(questions) {
|
|
|
1267
1268
|
startRow: Number(c.startRow) || 1,
|
|
1268
1269
|
startCol: Number(c.startCol) || 1,
|
|
1269
1270
|
length: Number(c.length) || 1,
|
|
1271
|
+
...(c.isExample === true ? { isExample: true } : {}),
|
|
1270
1272
|
}))
|
|
1271
1273
|
: [];
|
|
1272
1274
|
const correctAnswers = correctAnswer?.answers && typeof correctAnswer.answers === 'object'
|
|
1273
1275
|
? correctAnswer.answers
|
|
1274
1276
|
: {};
|
|
1275
1277
|
return {
|
|
1278
|
+
type: content?.type === 'HIDDEN_WORD' ? 'HIDDEN_WORD' : 'STANDARD',
|
|
1276
1279
|
title: content?.title || '',
|
|
1277
1280
|
instruction: question?.instruction || content?.instruction || 'Complete the crossword.',
|
|
1278
1281
|
gridSize: { rows, cols },
|
|
1282
|
+
...(typeof content?.markedColumn === 'number' ? { markedColumn: content.markedColumn } : {}),
|
|
1283
|
+
hiddenWord: correctAnswers['hidden-word'] || '',
|
|
1284
|
+
caseSensitive: correctAnswer?.caseSensitive === true,
|
|
1279
1285
|
clues: { across, down },
|
|
1280
1286
|
correctAnswers,
|
|
1281
1287
|
explanation: question?.explanation,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare function optionLetter(index: number): string;
|
|
3
|
+
export declare function KidChoiceRow({ letter, selected, isReviewMode, isCorrect, disabled, onClick, children, className, }: {
|
|
4
|
+
letter: string;
|
|
5
|
+
selected?: boolean;
|
|
6
|
+
isReviewMode?: boolean;
|
|
7
|
+
isCorrect?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../../../shared/lib/utils';
|
|
5
|
+
export function optionLetter(index) {
|
|
6
|
+
return String.fromCharCode(65 + index);
|
|
7
|
+
}
|
|
8
|
+
export function KidChoiceRow({ letter, selected = false, isReviewMode = false, isCorrect = false, disabled = false, onClick, children, className, }) {
|
|
9
|
+
const showStatus = isReviewMode && (isCorrect || selected);
|
|
10
|
+
const rowTone = isReviewMode
|
|
11
|
+
? isCorrect
|
|
12
|
+
? 'border-green-400 bg-green-50'
|
|
13
|
+
: selected
|
|
14
|
+
? 'border-red-400 bg-red-50'
|
|
15
|
+
: 'border-gray-200 bg-gray-50'
|
|
16
|
+
: selected
|
|
17
|
+
? 'border-blue-500 bg-blue-50 shadow-sm ring-2 ring-blue-100'
|
|
18
|
+
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm';
|
|
19
|
+
const badgeTone = isReviewMode
|
|
20
|
+
? isCorrect
|
|
21
|
+
? 'bg-green-500 text-white'
|
|
22
|
+
: selected
|
|
23
|
+
? 'bg-red-500 text-white'
|
|
24
|
+
: 'bg-gray-200 text-gray-600'
|
|
25
|
+
: selected
|
|
26
|
+
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm'
|
|
27
|
+
: 'bg-gray-100 text-gray-600 group-hover:bg-gray-200';
|
|
28
|
+
return (_jsxs("button", { type: "button", onClick: onClick, disabled: disabled || isReviewMode || !onClick, className: cn('group flex w-full items-center gap-2 rounded-md border px-3 py-1.5 text-left transition-all', rowTone, (disabled || isReviewMode || !onClick) && 'cursor-default', className), children: [_jsx("div", { className: cn('flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-bold transition-all', badgeTone), children: letter }), _jsx("div", { className: cn('min-w-0 flex-1 text-sm font-medium', isReviewMode && isCorrect
|
|
29
|
+
? 'text-green-800'
|
|
30
|
+
: isReviewMode && selected && !isCorrect
|
|
31
|
+
? 'text-red-800'
|
|
32
|
+
: 'text-gray-700'), children: children }), showStatus && (_jsx("div", { className: cn('flex h-5 w-5 shrink-0 items-center justify-center rounded-full', isCorrect ? 'bg-green-500' : 'bg-red-500'), children: isCorrect ? _jsx(Check, { className: "h-3 w-3 text-white" }) : _jsx(X, { className: "h-3 w-3 text-white" }) }))] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../../../../../shared/lib/utils';
|
|
4
|
+
export function KidInstruction({ children, className, }) {
|
|
5
|
+
if (!children) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return (_jsx("div", { className: cn('rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800', className), children: children }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../../../../../shared/lib/utils';
|
|
4
|
+
export function KidPrompt({ children, className, }) {
|
|
5
|
+
if (!children) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return (_jsx("div", { className: cn('rounded-md border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 px-3 py-2 text-sm font-semibold text-gray-800', className), children: children }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare function KidQuestionCard({ icon, title, points, isExample, review, headerExtra, children, className, contentClassName, }: {
|
|
3
|
+
icon?: ReactNode;
|
|
4
|
+
title: string;
|
|
5
|
+
points?: number;
|
|
6
|
+
isExample?: boolean;
|
|
7
|
+
review?: 'correct' | 'wrong' | null;
|
|
8
|
+
headerExtra?: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
contentClassName?: string;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, CircleHelp, Star, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../../../shared/lib/utils';
|
|
5
|
+
export function KidQuestionCard({ icon, title, points, isExample = false, review, headerExtra, children, className, contentClassName, }) {
|
|
6
|
+
return (_jsxs("div", { className: cn('overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "truncate text-sm font-medium text-gray-700", children: title }), isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "C\u00E2u v\u00ED d\u1EE5"] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsxs("span", { className: "text-xs text-gray-500", children: [points, " \u0111i\u1EC3m"] })), review === 'correct' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700", children: [_jsx(Check, { className: "h-3 w-3" }), "\u0110\u00FAng"] })), review === 'wrong' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: [_jsx(X, { className: "h-3 w-3" }), "Sai"] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
|
|
7
|
+
}
|
|
@@ -4,3 +4,7 @@ export { CompactPreviewButton } from './CompactPreviewButton';
|
|
|
4
4
|
export { CompactCreatorHeader } from './CompactCreatorHeader';
|
|
5
5
|
export { CompactFieldCard } from './CompactFieldCard';
|
|
6
6
|
export { CompactExplanationToggle } from './CompactExplanationToggle';
|
|
7
|
+
export { KidInstruction } from './KidInstruction';
|
|
8
|
+
export { KidPrompt } from './KidPrompt';
|
|
9
|
+
export { KidQuestionCard } from './KidQuestionCard';
|
|
10
|
+
export { KidChoiceRow, optionLetter } from './KidChoiceRow';
|
|
@@ -4,3 +4,7 @@ export { CompactPreviewButton } from './CompactPreviewButton';
|
|
|
4
4
|
export { CompactCreatorHeader } from './CompactCreatorHeader';
|
|
5
5
|
export { CompactFieldCard } from './CompactFieldCard';
|
|
6
6
|
export { CompactExplanationToggle } from './CompactExplanationToggle';
|
|
7
|
+
export { KidInstruction } from './KidInstruction';
|
|
8
|
+
export { KidPrompt } from './KidPrompt';
|
|
9
|
+
export { KidQuestionCard } from './KidQuestionCard';
|
|
10
|
+
export { KidChoiceRow, optionLetter } from './KidChoiceRow';
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer } from '../../../../shared/types/questions/crossword-puzzle';
|
|
1
|
+
import type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer, CrosswordSubtype } from '../../../../shared/types/questions/crossword-puzzle';
|
|
2
2
|
export interface CrosswordClueFormItem extends CrosswordClue {
|
|
3
3
|
id: string;
|
|
4
4
|
answer: string;
|
|
5
5
|
direction: 'across' | 'down';
|
|
6
6
|
}
|
|
7
7
|
export interface CrosswordPuzzleFormState {
|
|
8
|
+
type?: CrosswordSubtype;
|
|
8
9
|
title?: string;
|
|
10
|
+
instruction?: string;
|
|
9
11
|
rows: number;
|
|
10
12
|
cols: number;
|
|
13
|
+
markedColumn?: number;
|
|
14
|
+
hiddenWord?: string;
|
|
11
15
|
clues: CrosswordClueFormItem[];
|
|
12
16
|
caseSensitive: boolean;
|
|
13
17
|
points: number;
|
|
@@ -6,6 +6,7 @@ export interface SpontaneousQaGroupQuestionData {
|
|
|
6
6
|
expectedAnswers: string[];
|
|
7
7
|
points: number;
|
|
8
8
|
questionNumber: number;
|
|
9
|
+
isExample?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export interface SpontaneousQaGroupPartData {
|
|
11
12
|
partNumber: number;
|
|
@@ -31,7 +32,23 @@ export interface SpontaneousQaGroupCreatorProps {
|
|
|
31
32
|
getFormData?: () => SpontaneousQaGroupData;
|
|
32
33
|
} | null>;
|
|
33
34
|
}
|
|
35
|
+
export interface SpontaneousQaGroupStudentAnswer {
|
|
36
|
+
audioKeys: Array<string | null>;
|
|
37
|
+
}
|
|
38
|
+
export interface SpontaneousQaGroupGradeDetail {
|
|
39
|
+
itemId: string;
|
|
40
|
+
isCorrect?: boolean;
|
|
41
|
+
userAnswer?: unknown;
|
|
42
|
+
correctAnswer?: unknown;
|
|
43
|
+
partialScore?: number;
|
|
44
|
+
feedback?: string;
|
|
45
|
+
}
|
|
34
46
|
export interface SpontaneousQaGroupClientProps {
|
|
35
47
|
questionData: SpontaneousQaGroupData;
|
|
36
48
|
isReviewMode?: boolean;
|
|
49
|
+
userAnswer?: SpontaneousQaGroupStudentAnswer;
|
|
50
|
+
onItemRecorded?: (flattenIndex: number, blob: Blob) => void;
|
|
51
|
+
uploadingIndex?: number | null;
|
|
52
|
+
gradeDetails?: SpontaneousQaGroupGradeDetail[];
|
|
53
|
+
disabled?: boolean;
|
|
37
54
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Card, CardContent } from '../../../../components/ui/card';
|
|
4
3
|
import { Input } from '../../../../components/ui/input';
|
|
5
|
-
import {
|
|
6
|
-
import { CheckCircle2, XCircle, BookOpen } from 'lucide-react';
|
|
4
|
+
import { CheckCircle2, XCircle, PenLine } from 'lucide-react';
|
|
7
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
6
|
import { toAnswerTheQuestionImageUrls, } from '../../_shared/types/answer-the-question.type';
|
|
7
|
+
import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
9
8
|
import { useState } from 'react';
|
|
10
9
|
function isDisplayableMediaUrl(value) {
|
|
11
10
|
return (value.startsWith('http://')
|
|
@@ -33,7 +32,7 @@ export function AnswerTheQuestionClient({ questionData, onSubmit, isReviewMode =
|
|
|
33
32
|
const [answer, setAnswer] = useState(userAnswer || '');
|
|
34
33
|
const imageUrls = toAnswerTheQuestionImageUrls(questionData.imageUrl);
|
|
35
34
|
if (!questionData.question && imageUrls.length === 0) {
|
|
36
|
-
return (_jsx(
|
|
35
|
+
return (_jsx(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: "Tr\u1EA3 l\u1EDDi c\u00E2u h\u1ECFi", children: _jsx("p", { className: "text-sm text-gray-500", children: "Kh\u00F4ng c\u00F3 d\u1EEF li\u1EC7u c\u00E2u h\u1ECFi" }) }));
|
|
37
36
|
}
|
|
38
37
|
const isExample = questionData.isExample || false;
|
|
39
38
|
const correctAnswer = questionData.correctAnswer || '';
|
|
@@ -43,19 +42,19 @@ export function AnswerTheQuestionClient({ questionData, onSubmit, isReviewMode =
|
|
|
43
42
|
const isCorrect = isReviewMode && userAnswer !== undefined
|
|
44
43
|
? userAnswer.trim().toLowerCase() === correctAnswer.trim().toLowerCase()
|
|
45
44
|
: undefined;
|
|
46
|
-
return (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
return (_jsxs("div", { className: "space-y-3", children: [(questionData.title || questionData.groupContent) && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-gradient-to-r from-indigo-50 to-blue-50 p-4 space-y-2", children: [questionData.title && (_jsx("h3", { className: "text-base font-bold text-indigo-900", children: questionData.title })), questionData.groupContent && (_jsx("p", { className: "text-sm text-indigo-700 leading-relaxed whitespace-pre-wrap", children: questionData.groupContent }))] })), _jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: "Tr\u1EA3 l\u1EDDi c\u00E2u h\u1ECFi", points: questionData.points, isExample: isExample, review: isReviewMode && isCorrect !== undefined ? (isCorrect ? 'correct' : 'wrong') : null, children: [imageUrls.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: imageUrls.map((url, imageIndex) => (_jsx("div", { className: "relative max-w-lg overflow-hidden rounded-md border border-gray-200 bg-gray-50", children: _jsx(MediaPreview, { src: url, alt: `Question image ${imageIndex + 1}` }) }, `atq-image-${imageIndex}`))) })), questionData.question ? (_jsx(KidPrompt, { children: questionData.question })) : null, questionData.suggestedAnswers && questionData.suggestedAnswers.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 mt-2 mb-1", children: questionData.suggestedAnswers.map((suggestion, index) => (_jsx("button", { type: "button", disabled: isReviewMode || isExample, onClick: () => {
|
|
46
|
+
if (!isReviewMode && !isExample) {
|
|
47
|
+
setAnswer(suggestion);
|
|
48
|
+
onSubmit?.(suggestion);
|
|
49
|
+
}
|
|
50
|
+
}, className: `inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium transition-colors ${answer === suggestion
|
|
51
|
+
? 'border-teal-400 bg-teal-50 text-teal-700 ring-1 ring-teal-200'
|
|
52
|
+
: 'border-gray-200 bg-white text-gray-600 hover:border-teal-300 hover:bg-teal-50/50 hover:text-teal-600'} ${isReviewMode || isExample ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`, children: suggestion }, index))) })), _jsxs("div", { className: "flex items-center gap-3 mt-1", children: [questionData.prefixText && (_jsx("span", { className: "text-sm font-medium text-gray-700", children: questionData.prefixText })), _jsx(Input, { value: displayedAnswer, onChange: (e) => {
|
|
53
|
+
setAnswer(e.target.value);
|
|
54
|
+
onSubmit?.(e.target.value);
|
|
55
|
+
}, placeholder: "Vi\u1EBFt \u0111\u00E1p \u00E1n...", disabled: isReviewMode || isExample, className: `max-w-xs border text-sm px-3 py-1.5 rounded-md font-medium ${isReviewMode
|
|
56
|
+
? isCorrect
|
|
57
|
+
? 'border-green-400 bg-green-50 text-green-800'
|
|
58
|
+
: 'border-red-400 bg-red-50 text-red-800'
|
|
59
|
+
: 'border-teal-300 focus:border-teal-500 focus:ring-teal-200'}` }), questionData.suffixText && (_jsx("span", { className: "text-sm font-medium text-gray-700", children: questionData.suffixText })), isReviewMode && isCorrect !== undefined && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(CheckCircle2, { className: "h-5 w-5 text-green-500" })) : (_jsx(XCircle, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && isCorrect === false && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-green-50 border border-green-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: correctAnswer })] })] })), autoFillCorrectAnswers && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-teal-50 border border-teal-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-teal-600" }), _jsxs("span", { className: "text-sm text-teal-700", children: ["\u0110\u00E1p \u00E1n: ", _jsx("strong", { children: correctAnswer })] })] })), explanation && isReviewMode && (_jsxs("div", { className: "rounded-md border border-blue-200 bg-blue-50 px-3 py-2", children: [_jsx("p", { className: "text-sm font-semibold text-blue-800 mb-1", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "text-sm text-blue-700", children: explanation })] }))] })] }));
|
|
61
60
|
}
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, CircleHelp, Lightbulb, Sparkles,
|
|
3
|
+
import { BookOpen, CircleHelp, Lightbulb, Sparkles, Volume2 } from 'lucide-react';
|
|
4
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
5
|
import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
|
|
6
|
+
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
function isAbsoluteMediaUrl(value) {
|
|
7
8
|
return (value.startsWith('http://')
|
|
8
9
|
|| value.startsWith('https://')
|
|
@@ -41,15 +42,15 @@ function MediaPreview({ src, kind = 'image', alt }) {
|
|
|
41
42
|
}
|
|
42
43
|
export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
43
44
|
const items = questionData.items || [];
|
|
44
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
45
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: `Hình ảnh (${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: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
45
46
|
const userValue = userAnswers[itemIndex] || '';
|
|
46
47
|
const expectedAnswers = Array.isArray(item.expectedAnswers)
|
|
47
48
|
? item.expectedAnswers.filter((answer) => answer.trim() !== '')
|
|
48
49
|
: [];
|
|
49
|
-
return (_jsxs(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
return (_jsxs(KidQuestionCard, { icon: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }), title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, headerExtra: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] }), children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), isReviewMode || item.isExample || !onAnswerChange ? (userValue ? (_jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-700", children: userValue })) : null) : (_jsx("textarea", { value: userValue, onChange: (event) => {
|
|
51
|
+
const next = items.map((_, index) => userAnswers[index] || '');
|
|
52
|
+
next[itemIndex] = event.target.value;
|
|
53
|
+
onAnswerChange(next);
|
|
54
|
+
}, placeholder: "Nh\u1EADp c\u00E2u tr\u1EA3 l\u1EDDi...", rows: 2, className: "w-full rounded-md border border-slate-200 px-3 py-1.5 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })), isReviewMode && expectedAnswers.length > 0 ? (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : isReviewMode && !userValue ? (_jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 \u0111\u00E1p \u00E1n" })) : null] }, `${item.questionNumber}-${itemIndex}`));
|
|
54
55
|
}), 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] }))] }));
|
|
55
56
|
}
|