@tinyweb_dev/oe-exam-sdk 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/QuestionRenderer.js +1 -5
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
- package/dist/components/exams/take/utils/answer-transformers.js +16 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +5 -0
- package/dist/components/exams/take/utils/question-transformers.js +6 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +16 -17
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -4
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -32
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +76 -11
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
- package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +76 -7
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
- package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
- package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
- package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
- package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
- package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +20 -8
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
- package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +188 -46
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
- package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +13 -13
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +7 -6
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
- package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
- package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +39 -19
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +20 -19
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +12 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
- package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
- package/dist/components/questions/viewer/QuestionViewer.js +23 -10
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
- package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +1 -1
- package/dist/components/results/review-data.js +8 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +39 -5
- package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
- package/dist/shared/types/questions/answer-the-question-group.js +5 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
- package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,69 +1,211 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
3
4
|
import { Card } from '../../../../components/ui/card';
|
|
4
5
|
import { Badge } from '../../../../components/ui/badge';
|
|
5
|
-
|
|
6
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
const WORD_COLORS = [
|
|
7
|
-
{ bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white'
|
|
8
|
-
{ bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white'
|
|
9
|
-
{ bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white'
|
|
10
|
-
{ bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white'
|
|
11
|
-
{ bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white'
|
|
12
|
-
{ bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white'
|
|
13
|
-
{ bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white'
|
|
14
|
-
{ bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white'
|
|
15
|
-
{ bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white'
|
|
16
|
-
{ bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white'
|
|
8
|
+
{ bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white' },
|
|
9
|
+
{ bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white' },
|
|
10
|
+
{ bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white' },
|
|
11
|
+
{ bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white' },
|
|
12
|
+
{ bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white' },
|
|
13
|
+
{ bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white' },
|
|
14
|
+
{ bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white' },
|
|
15
|
+
{ bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white' },
|
|
16
|
+
{ bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white' },
|
|
17
|
+
{ bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white' },
|
|
17
18
|
];
|
|
18
|
-
|
|
19
|
+
function cleanWord(value) {
|
|
20
|
+
return (value || '').replace(/[\s\-_]/g, '').toUpperCase();
|
|
21
|
+
}
|
|
22
|
+
function getStraightLinePath(start, end) {
|
|
23
|
+
const dr = end.row - start.row;
|
|
24
|
+
const dc = end.col - start.col;
|
|
25
|
+
const stepR = dr === 0 ? 0 : dr > 0 ? 1 : -1;
|
|
26
|
+
const stepC = dc === 0 ? 0 : dc > 0 ? 1 : -1;
|
|
27
|
+
if (dr === 0 && dc !== 0) {
|
|
28
|
+
return Array.from({ length: Math.abs(dc) + 1 }, (_, i) => ({
|
|
29
|
+
row: start.row,
|
|
30
|
+
col: start.col + i * stepC,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
if (dc === 0 && dr !== 0) {
|
|
34
|
+
return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
|
|
35
|
+
row: start.row + i * stepR,
|
|
36
|
+
col: start.col,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
if (Math.abs(dr) === Math.abs(dc) && dr !== 0) {
|
|
40
|
+
return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
|
|
41
|
+
row: start.row + i * stepR,
|
|
42
|
+
col: start.col + i * stepC,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
return dr === 0 && dc === 0 ? [start] : [];
|
|
46
|
+
}
|
|
47
|
+
function readSelections(value) {
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
if (value && typeof value === 'object' && Array.isArray(value.words)) {
|
|
52
|
+
return value.words;
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
function pathLetters(grid, path) {
|
|
57
|
+
return path
|
|
58
|
+
.map((cell) => (grid[cell.row - 1]?.[cell.col - 1] || '').toUpperCase())
|
|
59
|
+
.join('');
|
|
60
|
+
}
|
|
61
|
+
export function FindWordsInMatrixClient({ content, correctAnswer, showSolution = false, userAnswer, onAnswerChange, }) {
|
|
19
62
|
const rows = content.gridSize?.rows || content.grid?.length || 14;
|
|
20
63
|
const cols = content.gridSize?.cols || content.grid?.[0]?.length || 14;
|
|
21
64
|
const grid = content.grid || [];
|
|
22
65
|
const wordBank = content.wordBank || [];
|
|
23
66
|
const isCategorize = content.type === 'CATEGORIZE' && Array.isArray(content.categories) && content.categories.length > 0;
|
|
24
67
|
const categories = content.categories || [];
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
const canSelect = !showSolution && Boolean(onAnswerChange);
|
|
69
|
+
const selections = readSelections(userAnswer);
|
|
70
|
+
const [activeWordId, setActiveWordId] = useState(null);
|
|
71
|
+
const [tappedPath, setTappedPath] = useState([]);
|
|
72
|
+
const playableWords = wordBank.filter((word) => !word.isExample);
|
|
73
|
+
const activeWord = playableWords.find((word) => word.id === activeWordId) ||
|
|
74
|
+
playableWords.find((word) => !selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word))) ||
|
|
75
|
+
null;
|
|
76
|
+
const targetChars = cleanWord(activeWord?.word).split('').filter(Boolean);
|
|
77
|
+
const tappedChars = tappedPath.map((cell) => (grid[cell.row - 1]?.[cell.col - 1] || '').toUpperCase());
|
|
78
|
+
const isReverseMode = tappedChars.length > 1
|
|
79
|
+
&& targetChars.length > 1
|
|
80
|
+
&& tappedChars[0] === targetChars[targetChars.length - 1]
|
|
81
|
+
&& tappedChars[1] === targetChars[targetChars.length - 2];
|
|
82
|
+
const activeCharSequence = isReverseMode ? [...targetChars].reverse() : targetChars;
|
|
83
|
+
const nextExpectedChar = tappedPath.length < activeCharSequence.length
|
|
84
|
+
? activeCharSequence[tappedPath.length]
|
|
85
|
+
: null;
|
|
86
|
+
const cellColorMap = useMemo(() => {
|
|
87
|
+
const map = {};
|
|
88
|
+
if (content.exampleSelection?.path) {
|
|
89
|
+
for (const cell of content.exampleSelection.path) {
|
|
90
|
+
map[`${cell.row}-${cell.col}`] = { colorIdx: 0, word: 'Example' };
|
|
91
|
+
}
|
|
31
92
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
93
|
+
const source = showSolution && correctAnswer?.words ? correctAnswer.words : selections;
|
|
94
|
+
source.forEach((item, index) => {
|
|
95
|
+
const colorIdx = (index + 1) % WORD_COLORS.length;
|
|
96
|
+
item.path?.forEach((cell) => {
|
|
97
|
+
map[`${cell.row}-${cell.col}`] = {
|
|
98
|
+
colorIdx,
|
|
99
|
+
word: item.word || item.wordId || '',
|
|
100
|
+
};
|
|
39
101
|
});
|
|
40
102
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
return map;
|
|
104
|
+
}, [content.exampleSelection, correctAnswer?.words, selections, showSolution]);
|
|
105
|
+
const emit = (next) => {
|
|
106
|
+
onAnswerChange?.({ words: next });
|
|
107
|
+
};
|
|
108
|
+
const completeWord = (path) => {
|
|
109
|
+
if (!activeWord)
|
|
110
|
+
return;
|
|
111
|
+
const target = cleanWord(activeWord.word);
|
|
112
|
+
emit([
|
|
113
|
+
...selections.filter((selection) => selection.wordId !== activeWord.id && cleanWord(selection.word) !== target),
|
|
114
|
+
{
|
|
115
|
+
wordId: activeWord.id,
|
|
116
|
+
word: activeWord.word,
|
|
117
|
+
categoryId: activeWord.categoryId,
|
|
118
|
+
path,
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
setTappedPath([]);
|
|
122
|
+
setActiveWordId(null);
|
|
123
|
+
};
|
|
124
|
+
const handleWordClick = (wordId) => {
|
|
125
|
+
if (!canSelect)
|
|
126
|
+
return;
|
|
127
|
+
const already = selections.find((selection) => selection.wordId === wordId);
|
|
128
|
+
if (already) {
|
|
129
|
+
emit(selections.filter((selection) => selection.wordId !== wordId));
|
|
130
|
+
setActiveWordId(null);
|
|
131
|
+
setTappedPath([]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setActiveWordId(wordId);
|
|
135
|
+
setTappedPath([]);
|
|
136
|
+
};
|
|
137
|
+
const handleCellClick = (row, col) => {
|
|
138
|
+
if (!canSelect || !activeWord || targetChars.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
const char = (grid[row - 1]?.[col - 1] || '').toUpperCase();
|
|
141
|
+
const last = tappedPath[tappedPath.length - 1];
|
|
142
|
+
if (last && last.row === row && last.col === col) {
|
|
143
|
+
setTappedPath((prev) => prev.slice(0, -1));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (tappedPath.length === 0) {
|
|
147
|
+
if (char === targetChars[0] || char === targetChars[targetChars.length - 1]) {
|
|
148
|
+
setTappedPath([{ row, col }]);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const existingIdx = tappedPath.findIndex((cell) => cell.row === row && cell.col === col);
|
|
153
|
+
if (existingIdx >= 0) {
|
|
154
|
+
setTappedPath((prev) => prev.slice(0, existingIdx + 1));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const straightPath = getStraightLinePath(tappedPath[0], { row, col });
|
|
158
|
+
const target = targetChars.join('');
|
|
159
|
+
const letters = pathLetters(grid, straightPath);
|
|
160
|
+
if (straightPath.length === targetChars.length
|
|
161
|
+
&& (letters === target || [...letters].reverse().join('') === target)) {
|
|
162
|
+
completeWord(straightPath);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const prev = tappedPath[tappedPath.length - 1];
|
|
166
|
+
const isAdjacent = Math.abs(row - prev.row) <= 1 && Math.abs(col - prev.col) <= 1;
|
|
167
|
+
if (isAdjacent && char === nextExpectedChar) {
|
|
168
|
+
const nextPath = [...tappedPath, { row, col }];
|
|
169
|
+
if (nextPath.length === activeCharSequence.length) {
|
|
170
|
+
completeWord(nextPath);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
setTappedPath(nextPath);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const renderWordBadge = (word, index) => {
|
|
177
|
+
const isExample = Boolean(word.isExample);
|
|
178
|
+
const isFound = selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word));
|
|
179
|
+
const isActive = activeWord?.id === word.id;
|
|
180
|
+
return (_jsxs("button", { type: "button", disabled: !canSelect || isExample, onClick: () => handleWordClick(word.id), className: `rounded-full border px-3 py-1.5 text-sm font-medium ${isExample
|
|
181
|
+
? 'border-amber-300 bg-amber-50 text-amber-800 line-through'
|
|
182
|
+
: isFound
|
|
183
|
+
? 'border-emerald-400 bg-emerald-50 text-emerald-800'
|
|
184
|
+
: isActive
|
|
185
|
+
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
186
|
+
: 'border-gray-200 bg-white'}`, children: [word.word, isExample ? _jsx("span", { className: "ml-1 font-bold", children: "(Ex)" }) : null] }, word.id || index));
|
|
187
|
+
};
|
|
188
|
+
const questionInstruction = content.instruction?.trim();
|
|
189
|
+
return (_jsxs(KidQuestionCard, { title: content.title || 'Tìm từ trong lưới', children: [questionInstruction ? (_jsx(KidInstruction, { children: questionInstruction })) : null, canSelect ? (_jsx("p", { className: "text-xs font-medium text-slate-600", children: activeWord ? (_jsxs(_Fragment, { children: ["\u0110ang t\u00ECm ", _jsx("strong", { children: activeWord.word }), ' — ', tappedPath.length === 0
|
|
190
|
+
? `bấm chữ cái đầu (${targetChars[0]}) hoặc cuối (${targetChars[targetChars.length - 1]}) trên lưới.`
|
|
191
|
+
: nextExpectedChar
|
|
192
|
+
? `tiếp theo bấm chữ ${nextExpectedChar}, hoặc bấm thẳng ô cuối của từ.`
|
|
193
|
+
: 'bấm ô cuối để hoàn thành từ.'] })) : (_jsx(_Fragment, { children: "Ch\u1ECDn m\u1ED9t t\u1EEB trong danh s\u00E1ch, r\u1ED3i b\u1EA5m t\u1EEBng ch\u1EEF tr\u00EAn l\u01B0\u1EDBi (ho\u1EB7c \u00F4 \u0111\u1EA7u r\u1ED3i \u00F4 cu\u1ED1i)." })) })) : null, isCategorize ? (_jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3", children: categories.map((cat) => {
|
|
194
|
+
const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
|
|
195
|
+
return (_jsxs(Card, { className: "border bg-gray-50/70 p-3.5", children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between border-b pb-1.5 text-sm font-bold", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((word, index) => renderWordBadge(word, index)) })] }, cat.id));
|
|
196
|
+
}) })) : (_jsxs(Card, { className: "border-2 border-dashed bg-gray-50 p-4", children: [_jsxs("div", { className: "mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500", children: ["Word Bank (", wordBank.length, " words)"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => renderWordBadge(word, index)) })] })), _jsx("div", { className: "flex justify-center overflow-x-auto pb-2", children: _jsx("div", { className: "inline-grid gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm", style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
58
197
|
const r = rIdx + 1;
|
|
59
198
|
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
60
199
|
const c = cIdx + 1;
|
|
61
200
|
const char = grid[rIdx]?.[cIdx] || '';
|
|
62
201
|
const highlight = cellColorMap[`${r}-${c}`];
|
|
63
202
|
const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
203
|
+
const isTapped = tappedPath.some((cell) => cell.row === r && cell.col === c);
|
|
204
|
+
return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex h-9 w-9 items-center justify-center rounded-md border text-sm font-bold sm:h-11 sm:w-11 sm:text-base ${colorConfig
|
|
205
|
+
? `${colorConfig.bg} shadow-sm`
|
|
206
|
+
: isTapped
|
|
207
|
+
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
208
|
+
: 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
|
|
67
209
|
});
|
|
68
210
|
}) }) })] }));
|
|
69
211
|
}
|
|
@@ -70,7 +70,9 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
70
70
|
return {
|
|
71
71
|
type,
|
|
72
72
|
title: content.title || '',
|
|
73
|
-
instruction:
|
|
73
|
+
instruction: content.instruction
|
|
74
|
+
|| initialData?.instruction
|
|
75
|
+
|| 'Find these words in the wordsearch box.',
|
|
74
76
|
rows,
|
|
75
77
|
cols,
|
|
76
78
|
grid,
|
|
@@ -94,6 +96,7 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
94
96
|
content: {
|
|
95
97
|
type: next.type,
|
|
96
98
|
title: next.title,
|
|
99
|
+
instruction: next.instruction,
|
|
97
100
|
gridSize: { rows: next.rows, cols: next.cols },
|
|
98
101
|
grid: next.grid,
|
|
99
102
|
...(next.type === 'CATEGORIZE' ? { categories: next.categories } : {}),
|
|
@@ -239,6 +242,7 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
239
242
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(FindWordsInMatrixClient, { content: {
|
|
240
243
|
type: state.type,
|
|
241
244
|
title: state.title,
|
|
245
|
+
instruction: state.instruction,
|
|
242
246
|
gridSize: { rows: state.rows, cols: state.cols },
|
|
243
247
|
grid: state.grid,
|
|
244
248
|
...(state.type === 'CATEGORIZE' ? { categories: state.categories } : {}),
|
|
@@ -275,7 +279,7 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
275
279
|
? 'border-blue-500 bg-blue-50/60 dark:bg-blue-950/30 text-blue-900 dark:text-blue-200 ring-2 ring-blue-400'
|
|
276
280
|
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-blue-100 dark:bg-blue-900 text-blue-600", children: _jsx(ListFilter, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "1. Danh s\u00E1ch t\u1EEB (Word Bank)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm c\u00E1c t\u1EEB c\u00F3 s\u1EB5n trong b\u1EA3ng ch\u1EEF c\u00E1i (VD: Gadgets)." })] })] }), _jsxs("button", { type: "button", onClick: () => updateState((p) => ({ ...p, type: 'CATEGORIZE' })), className: `flex items-center gap-3 p-3 rounded-xl border text-left transition-all ${state.type === 'CATEGORIZE'
|
|
277
281
|
? 'border-emerald-500 bg-emerald-50/60 dark:bg-emerald-950/30 text-emerald-900 dark:text-emerald-200 ring-2 ring-emerald-400'
|
|
278
|
-
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900 text-emerald-600", children: _jsx(Layers, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "2. T\u00ECm t\u1EEB & Ph\u00E2n nh\u00F3m (Categorize)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm t\u1EEB \u1EA9n v\u00E0 x\u1EBFp v\u00E0o c\u00E1c nh\u00F3m ti\u00EAu \u0111\u1EC1 (VD: Plants / Animals)." })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4 pt-2 border-t", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsx(Label, { className: "font-semibold", children: "Ti\u00EAu \u0111\u1EC1 b\u00E0i t\u1EADp" }), _jsx(Input, { value: state.title || '', onChange: (e) => updateState((p) => ({ ...p, title: e.target.value })), placeholder: state.type === 'CATEGORIZE' ? 'VD: Plants and Animals Word Search' : 'VD: Technology Gadgets Word Search', className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { className: "font-semibold", children: "K\u00EDch th\u01B0\u1EDBc ma tr\u1EADn (H\u00E0ng x C\u1ED9t)" }), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx(Input, { type: "number", min: 4, max: 25, value: state.rows, onChange: (e) => handleResize(Number(e.target.value), state.cols), className: "w-20 text-center font-bold" }), _jsx("span", { className: "font-bold text-gray-400", children: "\u00D7" }), _jsx(Input, { type: "number", min: 4, max: 25, value: state.cols, onChange: (e) => handleResize(state.rows, Number(e.target.value)), className: "w-20 text-center font-bold" })] })] })] })] }), state.type === 'CATEGORIZE' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white p-4 space-y-3 shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-bold text-sm text-emerald-900 dark:text-emerald-300 flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-4 w-4 text-emerald-600" }), "C\u00E1c nh\u00F3m danh m\u1EE5c (Categories / Headings)"] }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E1c c\u1ED9t ti\u00EAu \u0111\u1EC1 \u0111\u1EC3 h\u1ECDc sinh ph\u00E2n lo\u1EA1i t\u1EEB v\u00E0o (t\u1ED1i thi\u1EC3u 2 nh\u00F3m)." })] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: handleAddCategory, className: "gap-1 text-xs", children: [_jsx(FolderPlus, { className: "h-3.5 w-3.5" }), " Th\u00EAm nh\u00F3m"] })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3", children: state.categories.map((cat, idx) => (_jsxs("div", { className: "flex items-center gap-2 p-2.5 bg-white dark:bg-gray-900 rounded-lg border shadow-xs", children: [_jsx(Badge, { variant: "secondary", className: "font-mono text-xs", children: idx + 1 }), _jsx(Input, { value: cat.name, onChange: (e) => {
|
|
282
|
+
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900 text-emerald-600", children: _jsx(Layers, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "2. T\u00ECm t\u1EEB & Ph\u00E2n nh\u00F3m (Categorize)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm t\u1EEB \u1EA9n v\u00E0 x\u1EBFp v\u00E0o c\u00E1c nh\u00F3m ti\u00EAu \u0111\u1EC1 (VD: Plants / Animals)." })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4 pt-2 border-t", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsx(Label, { className: "font-semibold", children: "Ti\u00EAu \u0111\u1EC1 b\u00E0i t\u1EADp" }), _jsx(Input, { value: state.title || '', onChange: (e) => updateState((p) => ({ ...p, title: e.target.value })), placeholder: state.type === 'CATEGORIZE' ? 'VD: Plants and Animals Word Search' : 'VD: Technology Gadgets Word Search', className: "mt-1" })] }), _jsxs("div", { className: "md:col-span-3", children: [_jsx(Label, { className: "font-semibold", children: "H\u01B0\u1EDBng d\u1EABn (Instruction)" }), _jsx(Input, { value: state.instruction || '', onChange: (e) => updateState((p) => ({ ...p, instruction: e.target.value })), placeholder: "\u2605 Find nine food items in the word search.", className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { className: "font-semibold", children: "K\u00EDch th\u01B0\u1EDBc ma tr\u1EADn (H\u00E0ng x C\u1ED9t)" }), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx(Input, { type: "number", min: 4, max: 25, value: state.rows, onChange: (e) => handleResize(Number(e.target.value), state.cols), className: "w-20 text-center font-bold" }), _jsx("span", { className: "font-bold text-gray-400", children: "\u00D7" }), _jsx(Input, { type: "number", min: 4, max: 25, value: state.cols, onChange: (e) => handleResize(state.rows, Number(e.target.value)), className: "w-20 text-center font-bold" })] })] })] })] }), state.type === 'CATEGORIZE' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white p-4 space-y-3 shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-bold text-sm text-emerald-900 dark:text-emerald-300 flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-4 w-4 text-emerald-600" }), "C\u00E1c nh\u00F3m danh m\u1EE5c (Categories / Headings)"] }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E1c c\u1ED9t ti\u00EAu \u0111\u1EC1 \u0111\u1EC3 h\u1ECDc sinh ph\u00E2n lo\u1EA1i t\u1EEB v\u00E0o (t\u1ED1i thi\u1EC3u 2 nh\u00F3m)." })] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: handleAddCategory, className: "gap-1 text-xs", children: [_jsx(FolderPlus, { className: "h-3.5 w-3.5" }), " Th\u00EAm nh\u00F3m"] })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3", children: state.categories.map((cat, idx) => (_jsxs("div", { className: "flex items-center gap-2 p-2.5 bg-white dark:bg-gray-900 rounded-lg border shadow-xs", children: [_jsx(Badge, { variant: "secondary", className: "font-mono text-xs", children: idx + 1 }), _jsx(Input, { value: cat.name, onChange: (e) => {
|
|
279
283
|
const val = e.target.value;
|
|
280
284
|
updateState((p) => ({
|
|
281
285
|
...p,
|
|
@@ -4,11 +4,15 @@ export const transformFindWordsInMatrix = (question) => {
|
|
|
4
4
|
let content;
|
|
5
5
|
let correctAnswer;
|
|
6
6
|
const modeType = contentData?.type || (Array.isArray(contentData?.categories) && contentData.categories.length > 0 ? 'CATEGORIZE' : 'WORD_BANK');
|
|
7
|
+
const instruction = typeof contentData?.instruction === 'string' && contentData.instruction.trim()
|
|
8
|
+
? contentData.instruction.trim()
|
|
9
|
+
: undefined;
|
|
7
10
|
if (contentData?.gridSize && Array.isArray(contentData?.grid) && Array.isArray(contentData?.wordBank)) {
|
|
8
11
|
// Already in API shape
|
|
9
12
|
content = {
|
|
10
13
|
type: modeType,
|
|
11
14
|
title: contentData.title || '',
|
|
15
|
+
...(instruction ? { instruction } : {}),
|
|
12
16
|
gridSize: {
|
|
13
17
|
rows: Number(contentData.gridSize.rows) || 14,
|
|
14
18
|
cols: Number(contentData.gridSize.cols) || 14,
|
|
@@ -61,6 +65,7 @@ export const transformFindWordsInMatrix = (question) => {
|
|
|
61
65
|
content = {
|
|
62
66
|
type: modeType,
|
|
63
67
|
title: contentData.title || '',
|
|
68
|
+
...(instruction ? { instruction } : {}),
|
|
64
69
|
gridSize: { rows, cols },
|
|
65
70
|
grid,
|
|
66
71
|
...(modeType === 'CATEGORIZE' ? { categories } : {}),
|
|
@@ -5,6 +5,7 @@ import { cn } from '../../../../shared/lib/utils';
|
|
|
5
5
|
import { Input } from '../../../../components/ui/input';
|
|
6
6
|
import { Lightbulb } from 'lucide-react';
|
|
7
7
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
9
|
export function LabelThePictureClient({ questionData, userAnswer = {}, isReviewMode = false, explanation, onSubmit, }) {
|
|
9
10
|
const { imageUrl, wordBank, labels } = questionData;
|
|
10
11
|
// Initialize answers with pre-filled values
|
|
@@ -68,7 +69,7 @@ export function LabelThePictureClient({ questionData, userAnswer = {}, isReviewM
|
|
|
68
69
|
});
|
|
69
70
|
// Sort labels by number
|
|
70
71
|
const sortedLabels = [...labels].sort((a, b) => a.number - b.number);
|
|
71
|
-
return (_jsxs(
|
|
72
|
+
return (_jsxs(KidQuestionCard, { title: "Label the Picture", children: [_jsx(KidInstruction, { children: "Nh\u00ECn tranh v\u00E0 \u0111i\u1EC1n t\u1EEB v\u00E0o c\u00E1c v\u1ECB tr\u00ED t\u01B0\u01A1ng \u1EE9ng" }), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-red-400" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-green-400" }), _jsx("span", { className: "ml-3 text-sm font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex justify-center bg-gradient-to-br from-slate-50 to-gray-100 p-4", children: _jsxs("div", { className: "relative max-h-[400px] w-full max-w-2xl overflow-hidden rounded-lg", children: [isLoadingUrl && (_jsx("div", { className: "flex h-64 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })), !isLoadingUrl && (previewUrl || imageUrl) ? (
|
|
72
73
|
/* eslint-disable-next-line @next/next/no-img-element */
|
|
73
74
|
_jsx("img", { src: previewUrl || imageUrl, alt: "Label the picture", className: "h-full w-full object-contain", onError: (e) => {
|
|
74
75
|
console.error('Failed to load image:', imageUrl, 'previewUrl:', previewUrl);
|
|
@@ -82,8 +83,8 @@ export function LabelThePictureClient({ questionData, userAnswer = {}, isReviewM
|
|
|
82
83
|
return (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-lg border-2 px-3 py-1.5 text-sm font-semibold transition-all', isUsedAsPreFilled
|
|
83
84
|
? 'border-blue-300 bg-blue-50 text-blue-700'
|
|
84
85
|
: isUsedByUser
|
|
85
|
-
? 'border-
|
|
86
|
-
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:shadow-sm'), children: [word, isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-blue-200 px-1 py-0.5 text-xs", children: "s\u1EB5n" })), isUsedByUser && !isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-
|
|
86
|
+
? 'border-blue-500 bg-blue-50 text-blue-800 ring-2 ring-blue-100'
|
|
87
|
+
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:shadow-sm'), children: [word, isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-blue-200 px-1 py-0.5 text-xs", children: "s\u1EB5n" })), isUsedByUser && !isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-blue-200 px-1 py-0.5 text-xs", children: "\u2713" }))] }, index));
|
|
87
88
|
}) }) })] })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("svg", { className: "h-5 w-5 text-blue-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) }), _jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o c\u00E1c v\u1ECB tr\u00ED" })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: sortedLabels.map((label) => {
|
|
88
89
|
const userAnswerValue = answers[label.id] || '';
|
|
89
90
|
const correctAnswer = label.correctAnswer.trim().toLowerCase();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Play, Pause, Volume2, ImageIcon } from 'lucide-react';
|
|
5
5
|
import { Button } from '../../../../components/ui/button';
|
|
6
6
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
7
7
|
import { cn } from '../../../../shared/lib/utils';
|
|
8
|
+
import { KidChoiceRow, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
|
|
8
9
|
// ─── Sub-component: Audio player ─────────────────────────────────────────────
|
|
9
10
|
function QuestionAudioPlayer({ audioUrl }) {
|
|
10
11
|
const { previewUrl } = usePresignedFileUrl(audioUrl);
|
|
@@ -47,32 +48,12 @@ export function ListenAndChooseFromAnswerGroupClient({ questionData, onSubmit, i
|
|
|
47
48
|
const hasAnswered = !!selectedAnswer;
|
|
48
49
|
const selectedOption = questionData.options.find((o) => o.id === selectedAnswer);
|
|
49
50
|
const isAnswerCorrect = isReviewMode && selectedAnswer === resolvedCorrectAnswer;
|
|
50
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
? 'border-red-400 bg-red-50'
|
|
59
|
-
: 'border-gray-200 bg-gray-50'
|
|
60
|
-
: isSelected
|
|
61
|
-
? 'border-blue-500 bg-blue-50 shadow-md ring-2 ring-blue-100'
|
|
62
|
-
: 'border-gray-200 bg-white hover:border-gray-400 hover:shadow-sm cursor-pointer'), children: [_jsx("div", { className: cn('flex h-7 w-7 items-center justify-center rounded-lg text-sm font-bold shadow-sm', isReviewMode
|
|
63
|
-
? isCorrect
|
|
64
|
-
? 'bg-green-500 text-white'
|
|
65
|
-
: isWrong
|
|
66
|
-
? 'bg-red-500 text-white'
|
|
67
|
-
: 'bg-gray-200 text-gray-600'
|
|
68
|
-
: isSelected
|
|
69
|
-
? 'bg-blue-500 text-white'
|
|
70
|
-
: 'bg-gray-100 text-gray-600'), children: String.fromCharCode(65 + optIndex) }), _jsx(OptionImage, { imageUrl: option.imageUrl }), option.label && (_jsx("p", { className: cn('text-xs text-center font-medium', isReviewMode
|
|
71
|
-
? isCorrect ? 'text-green-700' : isWrong ? 'text-red-700' : 'text-gray-600'
|
|
72
|
-
: isSelected ? 'text-blue-700' : 'text-gray-600'), children: option.label })), isReviewMode && (isCorrect || isWrong) && (_jsx("div", { className: cn('absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full shadow-sm', isCorrect ? 'bg-green-500' : 'bg-red-500'), children: isCorrect ? (_jsx(Check, { className: "h-3.5 w-3.5 text-white" })) : (_jsx(X, { className: "h-3.5 w-3.5 text-white" })) })), !isReviewMode && isSelected && (_jsx("div", { className: "absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 shadow-sm", children: _jsx(Check, { className: "h-3.5 w-3.5 text-white" }) }))] }, option.id));
|
|
73
|
-
}) })] }), hasAnswered && selectedOption && (_jsxs("div", { className: cn('flex items-center gap-2 rounded-lg border p-3', isReviewMode
|
|
74
|
-
? isAnswerCorrect
|
|
75
|
-
? 'border-green-200 bg-green-50'
|
|
76
|
-
: 'border-red-200 bg-red-50'
|
|
77
|
-
: 'border-blue-200 bg-blue-50'), children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["\u0110\u00E3 ch\u1ECDn: ", _jsx("strong", { children: selectedOption.label || selectedOption.id })] }), isReviewMode && (_jsx("span", { className: cn('text-xs font-semibold', isAnswerCorrect ? 'text-green-600' : 'text-red-600'), children: isAnswerCorrect ? '✓ Chính xác' : '✗ Không chính xác' }))] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "H\u00E3y ch\u1ECDn m\u1ED9t \u0111\u00E1p \u00E1n" })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-lg border border-indigo-200 bg-indigo-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "mt-1 text-sm text-indigo-700", children: explanation })] }))] })] })] }));
|
|
51
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { icon: _jsx(Volume2, { className: "h-3.5 w-3.5" }), title: "Nghe v\u00E0 ch\u1ECDn trong nh\u00F3m \u0111\u00E1p \u00E1n", review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [questionData.audioUrl && (_jsx(QuestionAudioPlayer, { audioUrl: questionData.audioUrl })), _jsx("div", { className: "space-y-1.5", children: questionData.options.map((option, optIndex) => {
|
|
52
|
+
const { isSelected, isCorrect } = getOptionStatus(option.id);
|
|
53
|
+
return (_jsx(KidChoiceRow, { letter: optionLetter(optIndex), selected: isSelected, isReviewMode: isReviewMode, isCorrect: isCorrect, onClick: () => handleOptionSelect(option.id), children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(OptionImage, { imageUrl: option.imageUrl }), option.label ? _jsx("span", { children: option.label }) : null] }) }, option.id));
|
|
54
|
+
}) }), hasAnswered && selectedOption && (_jsxs("div", { className: cn('flex items-center gap-2 rounded-lg border p-3', isReviewMode
|
|
55
|
+
? isAnswerCorrect
|
|
56
|
+
? 'border-green-200 bg-green-50'
|
|
57
|
+
: 'border-red-200 bg-red-50'
|
|
58
|
+
: 'border-blue-200 bg-blue-50'), children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["\u0110\u00E3 ch\u1ECDn: ", _jsx("strong", { children: selectedOption.label || selectedOption.id })] }), isReviewMode && (_jsx("span", { className: cn('text-xs font-semibold', isAnswerCorrect ? 'text-green-600' : 'text-red-600'), children: isAnswerCorrect ? '✓ Chính xác' : '✗ Không chính xác' }))] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "H\u00E3y ch\u1ECDn m\u1ED9t \u0111\u00E1p \u00E1n" })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-lg border border-indigo-200 bg-indigo-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "mt-1 text-sm text-indigo-700", children: explanation })] }))] })] }));
|
|
78
59
|
}
|
package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Mic, Volume2, CheckCircle2, Play, Pause, Info } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
6
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
import { ArrowIndicatorSVG } from './ArrowIndicator';
|
|
7
8
|
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
8
9
|
function QuestionAudioPlayer({ audioUrl }) {
|
|
@@ -13,10 +14,10 @@ function QuestionAudioPlayer({ audioUrl }) {
|
|
|
13
14
|
// ─── Main Client Component ───────────────────────────────────────────────────
|
|
14
15
|
export function ListenAndSpeakAnswerClient({ questionData, isReviewMode = false, }) {
|
|
15
16
|
const { previewUrl: backgroundUrl } = usePresignedFileUrl(questionData.backgroundImageUrl || '');
|
|
16
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Nghe v\u00E0 tr\u1EA3 l\u1EDDi", icon: _jsx(Mic, { className: "h-3.5 w-3.5" }), children: [questionData.audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: questionData.audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), backgroundUrl && questionData.arrowNode && (_jsxs("div", { className: "relative w-full max-w-3xl mx-auto rounded-xl overflow-hidden shadow-sm border border-gray-200 bg-gray-50", children: [_jsx("img", { src: backgroundUrl, alt: "Background", className: "w-full object-contain pointer-events-none" }), _jsx("div", { className: "absolute", style: {
|
|
18
|
+
left: `${questionData.arrowNode.x}%`,
|
|
19
|
+
top: `${questionData.arrowNode.y}%`,
|
|
20
|
+
width: `${questionData.arrowNode.width}%`,
|
|
21
|
+
height: `${questionData.arrowNode.height}%`,
|
|
22
|
+
}, children: _jsx("div", { className: "flex h-full w-full items-center justify-center overflow-hidden p-[6%]", children: _jsx(ArrowIndicatorSVG, { idSuffix: "-client", animated: true, className: "w-full h-full" }) }) })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh s\u1EBD nh\u1EA5n n\u00FAt ghi \u00E2m v\u00E0 n\u00F3i c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y" })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: questionData.explanation })] }))] })] }));
|
|
22
23
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Mic, Volume2, CheckCircle2, Play, Pause, ImageIcon, Info, GitCompareArrows } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
5
6
|
import { useCompareImagesGroup } from '../../_shared/hooks/useCompareImagesGroup';
|
|
6
7
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
7
8
|
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
@@ -28,5 +29,5 @@ export function ListenAndSpeakCompareImagesClient({ questionData, isReviewMode =
|
|
|
28
29
|
// fall back to store values (exam-taking mode populates the store)
|
|
29
30
|
const candidateImageUrl = questionData.candidateImageUrl || storeCandidateImageUrl;
|
|
30
31
|
const examinerImageUrl = questionData.examinerImageUrl || storeExaminerImageUrl;
|
|
31
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
32
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Nghe v\u00E0 n\u00F3i (So s\u00E1nh h\u00ECnh \u1EA3nh)", icon: _jsx(GitCompareArrows, { className: "h-3.5 w-3.5" }), children: [audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), (candidateImageUrl || examinerImageUrl) && (_jsxs("div", { className: "space-y-4 bg-gradient-to-br from-amber-100 to-orange-100 p-5 rounded-2xl border border-amber-200 shadow-sm", children: [candidateImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-amber-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-amber-300/50", children: "Candidate's Copy" }) }), _jsx("div", { className: "w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200", children: _jsx(CompareImagePreview, { imageUrl: candidateImageUrl, label: "Candidate's Copy" }) })] })), examinerImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-orange-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-orange-300/50", children: "Examiner's Copy" }) }), _jsx("div", { className: "w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200", children: _jsx(CompareImagePreview, { imageUrl: examinerImageUrl, label: "Examiner's Copy" }) })] }))] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh s\u1EBD nh\u1EA5n n\u00FAt ghi \u00E2m v\u00E0 n\u00F3i c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y" })] }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: explanation })] }))] })] }));
|
|
32
33
|
}
|