@tinyweb_dev/oe-exam-sdk 0.2.0 → 0.2.2
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 +10 -2
- package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.d.ts +4 -0
- package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.js +389 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +33 -7
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/types.d.ts +50 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +41 -1
- package/dist/components/exams/take/utils/question-transformers.js +24 -0
- package/dist/components/questions/_shared/config/question-types.config.js +6 -0
- package/dist/components/questions/_shared/types/find-words-in-matrix.type.d.ts +18 -0
- package/dist/components/questions/_shared/types/find-words-in-matrix.type.js +1 -0
- package/dist/components/questions/creator/question-type-registry.js +2 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
- package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +10 -6
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +9 -0
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +69 -0
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.d.ts +8 -0
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +302 -0
- package/dist/components/questions/types/find-words-in-matrix/index.d.ts +4 -0
- package/dist/components/questions/types/find-words-in-matrix/index.js +4 -0
- package/dist/components/questions/types/find-words-in-matrix/register.d.ts +2 -0
- package/dist/components/questions/types/find-words-in-matrix/register.js +29 -0
- package/dist/components/questions/types/find-words-in-matrix/transform.d.ts +2 -0
- package/dist/components/questions/types/find-words-in-matrix/transform.js +89 -0
- package/dist/shared/constants/question-skills.js +2 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +63 -0
- package/dist/shared/lib/utils/question-transform.js +2 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +71 -0
- package/dist/shared/types/questions/find-words-in-matrix.js +8 -0
- package/dist/shared/types/questions/index.d.ts +1 -0
- package/dist/shared/types/questions/index.js +2 -0
- package/package.json +1 -1
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* 2. Transforms data to renderer-specific props
|
|
9
9
|
* 3. Renders the correct component for each question type
|
|
10
10
|
*/
|
|
11
|
-
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, } from './question-renderers';
|
|
11
|
+
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
|
|
12
12
|
import { SpeakingQuestionRenderer } from './speaking/renderers';
|
|
13
|
-
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, } from '../utils/question-transformers';
|
|
13
|
+
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
|
|
14
14
|
import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
|
|
15
15
|
import { normalizeColoringAssignments } from '../utils/coloring-assignments';
|
|
16
16
|
export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
|
|
@@ -137,6 +137,14 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
137
137
|
onAnswerChange(regionId, hintItemId);
|
|
138
138
|
}, isReviewMode: isReviewMode }));
|
|
139
139
|
}
|
|
140
|
+
case 'CROSSWORD_PUZZLE': {
|
|
141
|
+
const data = transformCrosswordPuzzle(questions);
|
|
142
|
+
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 }));
|
|
143
|
+
}
|
|
144
|
+
case 'FIND_WORDS_IN_MATRIX': {
|
|
145
|
+
const data = transformFindWordsInMatrix(questions);
|
|
146
|
+
return (_jsx(FindWordsInMatrixRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, type: data.type, title: data.title, gridSize: data.gridSize, grid: data.grid, categories: data.categories, wordBank: data.wordBank, exampleSelection: data.exampleSelection, correctWords: data.correctWords, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, questionId: questions[0]?.id || 'find-words' }));
|
|
147
|
+
}
|
|
140
148
|
case 'ANSWER_THE_QUESTION': {
|
|
141
149
|
const data = transformAnswerTheQuestion(questions);
|
|
142
150
|
return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, audioUrl: part.audioUrl, title: data.title, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { FindWordsInMatrixRendererProps } from '../../types';
|
|
3
|
+
export declare function FindWordsInMatrixRenderer({ partNumber, partName, questionCount, instruction, type, title, gridSize, grid, categories, wordBank, exampleSelection, selectedWords: propSelectedWords, correctWords, answers, onAnswerChange, isReviewMode, questionId, }: FindWordsInMatrixRendererProps): React.JSX.Element;
|
|
4
|
+
export default FindWordsInMatrixRenderer;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
4
|
+
import { Badge } from '../../../../../components/ui/badge';
|
|
5
|
+
import { Card } from '../../../../../components/ui/card';
|
|
6
|
+
import { Button } from '../../../../../components/ui/button';
|
|
7
|
+
import { RotateCcw, Layers, Sparkles, Eye, Undo2, } from 'lucide-react';
|
|
8
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
9
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
10
|
+
// Distinct pastel color palette for word highlights
|
|
11
|
+
const HIGHLIGHT_PALETTE = [
|
|
12
|
+
{ bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white', color: '#f59e0b', ring: 'ring-amber-400' },
|
|
13
|
+
{ bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white', color: '#10b981', ring: 'ring-emerald-400' },
|
|
14
|
+
{ bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white', color: '#0ea5e9', ring: 'ring-sky-400' },
|
|
15
|
+
{ bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white', color: '#a855f7', ring: 'ring-purple-400' },
|
|
16
|
+
{ bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white', color: '#f43f5e', ring: 'ring-rose-400' },
|
|
17
|
+
{ bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white', color: '#6366f1', ring: 'ring-indigo-400' },
|
|
18
|
+
{ bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white', color: '#14b8a6', ring: 'ring-teal-400' },
|
|
19
|
+
{ bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white', color: '#eab308', ring: 'ring-yellow-400' },
|
|
20
|
+
{ bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white', color: '#ec4899', ring: 'ring-pink-400' },
|
|
21
|
+
{ bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white', color: '#06b6d4', ring: 'ring-cyan-400' },
|
|
22
|
+
];
|
|
23
|
+
// Calculate straight line path between start and end (8 directions)
|
|
24
|
+
function getStraightLinePath(start, end) {
|
|
25
|
+
const dr = end.row - start.row;
|
|
26
|
+
const dc = end.col - start.col;
|
|
27
|
+
const stepR = dr === 0 ? 0 : dr > 0 ? 1 : -1;
|
|
28
|
+
const stepC = dc === 0 ? 0 : dc > 0 ? 1 : -1;
|
|
29
|
+
if (dr === 0 && dc !== 0) {
|
|
30
|
+
const len = Math.abs(dc);
|
|
31
|
+
return Array.from({ length: len + 1 }, (_, i) => ({
|
|
32
|
+
row: start.row,
|
|
33
|
+
col: start.col + i * stepC,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
if (dc === 0 && dr !== 0) {
|
|
37
|
+
const len = Math.abs(dr);
|
|
38
|
+
return Array.from({ length: len + 1 }, (_, i) => ({
|
|
39
|
+
row: start.row + i * stepR,
|
|
40
|
+
col: start.col,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
if (Math.abs(dr) === Math.abs(dc) && dr !== 0) {
|
|
44
|
+
const len = Math.abs(dr);
|
|
45
|
+
return Array.from({ length: len + 1 }, (_, i) => ({
|
|
46
|
+
row: start.row + i * stepR,
|
|
47
|
+
col: start.col + i * stepC,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
if (dr === 0 && dc === 0) {
|
|
51
|
+
return [start];
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
// Helper to compare paths
|
|
56
|
+
function arePathsMatching(p1, p2) {
|
|
57
|
+
if (!Array.isArray(p1) || !Array.isArray(p2) || p1.length !== p2.length)
|
|
58
|
+
return false;
|
|
59
|
+
const forward = p1.every((c, i) => c.row === p2[i]?.row && c.col === p2[i]?.col);
|
|
60
|
+
if (forward)
|
|
61
|
+
return true;
|
|
62
|
+
const rev2 = [...p2].reverse();
|
|
63
|
+
return p1.every((c, i) => c.row === rev2[i]?.row && c.col === rev2[i]?.col);
|
|
64
|
+
}
|
|
65
|
+
// Clean string for spelling match (remove spaces, symbols)
|
|
66
|
+
function cleanWord(str) {
|
|
67
|
+
return (str || '').replace(/[\s\-_]/g, '').toUpperCase();
|
|
68
|
+
}
|
|
69
|
+
export function FindWordsInMatrixRenderer({ partNumber, partName, questionCount, instruction, type = 'WORD_BANK', title, gridSize, grid, categories = [], wordBank = [], exampleSelection, selectedWords: propSelectedWords, correctWords = [], answers, onAnswerChange, isReviewMode = false, questionId = 'find-words', }) {
|
|
70
|
+
const rows = gridSize?.rows || grid?.length || 14;
|
|
71
|
+
const cols = gridSize?.cols || grid?.[0]?.length || 14;
|
|
72
|
+
const isCategorize = type === 'CATEGORIZE' && categories.length > 0;
|
|
73
|
+
// Category lookup map
|
|
74
|
+
const categoryMap = useMemo(() => {
|
|
75
|
+
const map = new Map();
|
|
76
|
+
categories.forEach((c) => map.set(c.id, c.name));
|
|
77
|
+
return map;
|
|
78
|
+
}, [categories]);
|
|
79
|
+
// Non-example words list
|
|
80
|
+
const playableWords = useMemo(() => wordBank.filter((w) => !w.isExample), [wordBank]);
|
|
81
|
+
// Parse student answer from answers object
|
|
82
|
+
const currentAnswer = answers[questionId];
|
|
83
|
+
const initialStudentWords = Array.isArray(propSelectedWords)
|
|
84
|
+
? propSelectedWords
|
|
85
|
+
: Array.isArray(currentAnswer?.words)
|
|
86
|
+
? currentAnswer.words
|
|
87
|
+
: Array.isArray(currentAnswer)
|
|
88
|
+
? currentAnswer
|
|
89
|
+
: [];
|
|
90
|
+
const [studentSelections, setStudentSelections] = useState(initialStudentWords);
|
|
91
|
+
// Active target word selected from Word Bank
|
|
92
|
+
const [activeWordId, setActiveWordId] = useState(null);
|
|
93
|
+
// Sequence of tapped cells on the grid for current active word
|
|
94
|
+
const [tappedPath, setTappedPath] = useState([]);
|
|
95
|
+
// Sync state if answers change externally
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (Array.isArray(currentAnswer?.words)) {
|
|
98
|
+
setStudentSelections(currentAnswer.words);
|
|
99
|
+
}
|
|
100
|
+
}, [currentAnswer]);
|
|
101
|
+
// Determine which word is currently selected/active
|
|
102
|
+
const activeTargetWord = useMemo(() => {
|
|
103
|
+
if (activeWordId) {
|
|
104
|
+
return wordBank.find((w) => w.id === activeWordId && !w.isExample) || null;
|
|
105
|
+
}
|
|
106
|
+
// Auto-select first unfound playable word
|
|
107
|
+
const firstUnfound = playableWords.find((w) => !studentSelections.some((s) => s.wordId === w.id || cleanWord(s.word) === cleanWord(w.word)));
|
|
108
|
+
return firstUnfound || null;
|
|
109
|
+
}, [activeWordId, wordBank, playableWords, studentSelections]);
|
|
110
|
+
// Clean characters of target word
|
|
111
|
+
const targetChars = useMemo(() => {
|
|
112
|
+
if (!activeTargetWord)
|
|
113
|
+
return [];
|
|
114
|
+
return cleanWord(activeTargetWord.word).split('');
|
|
115
|
+
}, [activeTargetWord]);
|
|
116
|
+
// Characters tapped so far
|
|
117
|
+
const tappedChars = useMemo(() => {
|
|
118
|
+
return tappedPath.map((c) => (grid[c.row - 1]?.[c.col - 1] || '').toUpperCase());
|
|
119
|
+
}, [tappedPath, grid]);
|
|
120
|
+
// Next expected character (supports forward or reverse direction after 1st char)
|
|
121
|
+
const isReverseMode = useMemo(() => {
|
|
122
|
+
if (tappedChars.length <= 1 || targetChars.length <= 1)
|
|
123
|
+
return false;
|
|
124
|
+
const revChars = [...targetChars].reverse();
|
|
125
|
+
return tappedChars[0] === revChars[0] && tappedChars[1] === revChars[1];
|
|
126
|
+
}, [tappedChars, targetChars]);
|
|
127
|
+
const activeCharSequence = useMemo(() => {
|
|
128
|
+
return isReverseMode ? [...targetChars].reverse() : targetChars;
|
|
129
|
+
}, [isReverseMode, targetChars]);
|
|
130
|
+
const nextExpectedChar = useMemo(() => {
|
|
131
|
+
if (tappedPath.length >= activeCharSequence.length)
|
|
132
|
+
return null;
|
|
133
|
+
return activeCharSequence[tappedPath.length] || null;
|
|
134
|
+
}, [tappedPath.length, activeCharSequence]);
|
|
135
|
+
const emitAnswer = useCallback((newSelections) => {
|
|
136
|
+
setStudentSelections(newSelections);
|
|
137
|
+
onAnswerChange(questionId, { words: newSelections });
|
|
138
|
+
}, [onAnswerChange, questionId]);
|
|
139
|
+
// Complete word found
|
|
140
|
+
const handleWordFound = useCallback((wordItem, path) => {
|
|
141
|
+
const newSelection = {
|
|
142
|
+
wordId: wordItem.id,
|
|
143
|
+
word: wordItem.word,
|
|
144
|
+
categoryId: wordItem.categoryId,
|
|
145
|
+
path,
|
|
146
|
+
};
|
|
147
|
+
const updated = [
|
|
148
|
+
...studentSelections.filter((s) => s.wordId !== wordItem.id && cleanWord(s.word) !== cleanWord(wordItem.word)),
|
|
149
|
+
newSelection,
|
|
150
|
+
];
|
|
151
|
+
emitAnswer(updated);
|
|
152
|
+
setTappedPath([]);
|
|
153
|
+
// Auto switch to next unfound word
|
|
154
|
+
const nextRemaining = playableWords.find((w) => w.id !== wordItem.id && !updated.some((s) => s.wordId === w.id || cleanWord(s.word) === cleanWord(w.word)));
|
|
155
|
+
if (nextRemaining) {
|
|
156
|
+
setActiveWordId(nextRemaining.id);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
setActiveWordId(null);
|
|
160
|
+
}
|
|
161
|
+
}, [studentSelections, emitAnswer, playableWords]);
|
|
162
|
+
// Tap a cell on the grid
|
|
163
|
+
const handleCellTap = (r, c) => {
|
|
164
|
+
if (isReviewMode || !activeTargetWord || targetChars.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
const char = (grid[r - 1]?.[c - 1] || '').toUpperCase();
|
|
167
|
+
// 1. If clicking the last tapped cell -> undo that cell
|
|
168
|
+
if (tappedPath.length > 0) {
|
|
169
|
+
const last = tappedPath[tappedPath.length - 1];
|
|
170
|
+
if (last.row === r && last.col === c) {
|
|
171
|
+
setTappedPath((prev) => prev.slice(0, -1));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 2. If no cells tapped yet -> start of word
|
|
176
|
+
if (tappedPath.length === 0) {
|
|
177
|
+
const isStartChar = char === targetChars[0] || char === targetChars[targetChars.length - 1];
|
|
178
|
+
if (isStartChar) {
|
|
179
|
+
setTappedPath([{ row: r, col: c }]);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// 3. If cell is already tapped in the current path -> jump to that step
|
|
184
|
+
const existingIdx = tappedPath.findIndex((p) => p.row === r && p.col === c);
|
|
185
|
+
if (existingIdx >= 0) {
|
|
186
|
+
setTappedPath((prev) => prev.slice(0, existingIdx + 1));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// 4. Check if shortcut: Two-Tap straight line from start cell to this end cell
|
|
190
|
+
const startCell = tappedPath[0];
|
|
191
|
+
const straightPath = getStraightLinePath(startCell, { row: r, col: c });
|
|
192
|
+
if (straightPath.length === targetChars.length) {
|
|
193
|
+
const straightLetters = straightPath.map((p) => (grid[p.row - 1]?.[p.col - 1] || '').toUpperCase()).join('');
|
|
194
|
+
const targetStr = targetChars.join('');
|
|
195
|
+
const revTargetStr = [...targetChars].reverse().join('');
|
|
196
|
+
if (straightLetters === targetStr || straightLetters === revTargetStr) {
|
|
197
|
+
// Complete via two-tap straight line shortcut!
|
|
198
|
+
handleWordFound(activeTargetWord, straightPath);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// 5. Letter-by-letter tapping: check adjacency to last cell
|
|
203
|
+
const prev = tappedPath[tappedPath.length - 1];
|
|
204
|
+
const isAdjacent = Math.abs(r - prev.row) <= 1 && Math.abs(c - prev.col) <= 1;
|
|
205
|
+
if (isAdjacent && char === nextExpectedChar) {
|
|
206
|
+
const nextPath = [...tappedPath, { row: r, col: c }];
|
|
207
|
+
if (nextPath.length === activeCharSequence.length) {
|
|
208
|
+
// Word completed letter-by-letter!
|
|
209
|
+
handleWordFound(activeTargetWord, nextPath);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
setTappedPath(nextPath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
// Switch active target word
|
|
217
|
+
const handleSelectWordBankItem = (wordItem) => {
|
|
218
|
+
if (isReviewMode || wordItem.isExample)
|
|
219
|
+
return;
|
|
220
|
+
setActiveWordId(wordItem.id);
|
|
221
|
+
setTappedPath([]);
|
|
222
|
+
};
|
|
223
|
+
// Remove a found word
|
|
224
|
+
const handleRemoveSelection = (wordId, wordText) => {
|
|
225
|
+
if (isReviewMode)
|
|
226
|
+
return;
|
|
227
|
+
const updated = studentSelections.filter((s) => s.wordId !== wordId && (!wordText || cleanWord(s.word) !== cleanWord(wordText)));
|
|
228
|
+
emitAnswer(updated);
|
|
229
|
+
if (wordId)
|
|
230
|
+
setActiveWordId(wordId);
|
|
231
|
+
setTappedPath([]);
|
|
232
|
+
};
|
|
233
|
+
// Evaluation for Review Mode
|
|
234
|
+
const reviewEvaluation = useMemo(() => {
|
|
235
|
+
if (!isReviewMode)
|
|
236
|
+
return null;
|
|
237
|
+
// Evaluate each student selection
|
|
238
|
+
const evaluatedSelections = studentSelections.map((sel) => {
|
|
239
|
+
const matchingCorrect = correctWords.find((cw) => {
|
|
240
|
+
const idMatches = cw.wordId && sel.wordId && cw.wordId === sel.wordId;
|
|
241
|
+
const textMatches = cw.word && sel.word && cleanWord(cw.word) === cleanWord(sel.word);
|
|
242
|
+
const pathMatches = arePathsMatching(cw.path, sel.path);
|
|
243
|
+
return (idMatches || textMatches) && pathMatches;
|
|
244
|
+
});
|
|
245
|
+
if (!matchingCorrect) {
|
|
246
|
+
return { ...sel, status: 'INCORRECT', correctCategoryName: undefined };
|
|
247
|
+
}
|
|
248
|
+
if (isCategorize && matchingCorrect.categoryId && sel.categoryId !== matchingCorrect.categoryId) {
|
|
249
|
+
return {
|
|
250
|
+
...sel,
|
|
251
|
+
status: 'WRONG_CATEGORY',
|
|
252
|
+
correctCategoryName: categoryMap.get(matchingCorrect.categoryId) || matchingCorrect.categoryId,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return { ...sel, status: 'CORRECT', correctCategoryName: undefined };
|
|
256
|
+
});
|
|
257
|
+
// Determine missed words
|
|
258
|
+
const missedWords = correctWords.filter((cw) => {
|
|
259
|
+
return !studentSelections.some((sel) => {
|
|
260
|
+
const idMatches = cw.wordId && sel.wordId && cw.wordId === sel.wordId;
|
|
261
|
+
const textMatches = cw.word && sel.word && cleanWord(cw.word) === cleanWord(sel.word);
|
|
262
|
+
const pathMatches = arePathsMatching(cw.path, sel.path);
|
|
263
|
+
return (idMatches || textMatches) && pathMatches;
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
return { evaluatedSelections, missedWords };
|
|
267
|
+
}, [isReviewMode, studentSelections, correctWords, isCategorize, categoryMap]);
|
|
268
|
+
const cellMap = {};
|
|
269
|
+
// 1. Example word (always orange highlight)
|
|
270
|
+
if (exampleSelection?.path) {
|
|
271
|
+
exampleSelection.path.forEach((c) => {
|
|
272
|
+
cellMap[`${c.row}-${c.col}`] = {
|
|
273
|
+
colorConfig: HIGHLIGHT_PALETTE[0],
|
|
274
|
+
label: 'Example',
|
|
275
|
+
isExample: true,
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// 2. Confirmed student selections
|
|
280
|
+
studentSelections.forEach((sel, selIdx) => {
|
|
281
|
+
const colorIdx = (selIdx + 1) % HIGHLIGHT_PALETTE.length;
|
|
282
|
+
const colorConfig = HIGHLIGHT_PALETTE[colorIdx];
|
|
283
|
+
sel.path.forEach((c) => {
|
|
284
|
+
cellMap[`${c.row}-${c.col}`] = {
|
|
285
|
+
colorConfig,
|
|
286
|
+
label: sel.word || `Word #${selIdx + 1}`,
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
// 3. Active tapped path (glowing blue with step numbers)
|
|
291
|
+
tappedPath.forEach((c, idx) => {
|
|
292
|
+
cellMap[`${c.row}-${c.col}`] = {
|
|
293
|
+
colorConfig: {
|
|
294
|
+
bg: 'bg-blue-500 text-white border-blue-600 shadow-md ring-4 ring-blue-300 dark:ring-blue-800 scale-105',
|
|
295
|
+
badge: 'bg-blue-600 text-white',
|
|
296
|
+
color: '#2563eb',
|
|
297
|
+
ring: 'ring-blue-400',
|
|
298
|
+
},
|
|
299
|
+
label: `Chữ thứ ${idx + 1}`,
|
|
300
|
+
isCurrentTapping: true,
|
|
301
|
+
stepNumber: idx + 1,
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
// 4. In Review Mode: Highlight missed words with dotted solution indicator
|
|
305
|
+
if (isReviewMode && reviewEvaluation?.missedWords) {
|
|
306
|
+
reviewEvaluation.missedWords.forEach((mw) => {
|
|
307
|
+
mw.path.forEach((c) => {
|
|
308
|
+
if (!cellMap[`${c.row}-${c.col}`]) {
|
|
309
|
+
cellMap[`${c.row}-${c.col}`] = {
|
|
310
|
+
colorConfig: {
|
|
311
|
+
bg: 'bg-amber-50 text-amber-900 border-amber-400 border-dashed',
|
|
312
|
+
badge: 'bg-amber-500 text-white',
|
|
313
|
+
color: '#d97706',
|
|
314
|
+
ring: 'ring-amber-300',
|
|
315
|
+
},
|
|
316
|
+
label: `Đáp án: ${mw.word || mw.wordId}`,
|
|
317
|
+
isReviewMissed: true,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const foundCount = studentSelections.length;
|
|
324
|
+
return (_jsxs("div", { className: "space-y-6 select-none", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), isCategorize ? (_jsxs(Card, { className: "p-4 sm:p-5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 shadow-sm rounded-2xl", children: [_jsxs("div", { className: "flex items-center justify-between mb-4 border-b border-gray-100 dark:border-gray-800 pb-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Layers, { className: "h-4 w-4 text-emerald-600" }), _jsxs("span", { className: "text-sm font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wider", children: ["Headings (", categories.length, " nh\u00F3m t\u1EEB)"] }), _jsxs(Badge, { variant: "secondary", className: "text-xs font-semibold", children: [foundCount, " / ", playableWords.length, " t\u1EEB"] })] }), !isReviewMode && studentSelections.length > 0 && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
325
|
+
emitAnswer([]);
|
|
326
|
+
setTappedPath([]);
|
|
327
|
+
}, className: "text-xs text-gray-400 hover:text-red-500 gap-1 h-7", children: [_jsx(RotateCcw, { className: "h-3 w-3" }), " L\u00E0m l\u1EA1i t\u1EA5t c\u1EA3"] }))] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4", children: categories.map((cat) => {
|
|
328
|
+
const wordsInCat = wordBank.filter((w) => w.categoryId === cat.id);
|
|
329
|
+
return (_jsxs("div", { className: "p-3.5 rounded-xl bg-gray-50/80 dark:bg-gray-800/40 border border-gray-200 dark:border-gray-700 space-y-2.5", children: [_jsxs("div", { className: "font-bold text-sm text-gray-800 dark:text-gray-200 border-b border-gray-200 dark:border-gray-700 pb-1 flex items-center justify-between", children: [_jsx("span", { children: cat.name }), _jsxs(Badge, { variant: "outline", className: "text-xs font-semibold", children: [wordsInCat.filter((w) => studentSelections.some((s) => s.wordId === w.id) || w.isExample).length, " / ", wordsInCat.length] })] }), _jsx("div", { className: "min-h-[60px] flex flex-wrap gap-2 content-start", children: wordsInCat.map((wb, idx) => {
|
|
330
|
+
const isExample = Boolean(wb.isExample);
|
|
331
|
+
const isFound = studentSelections.some((s) => s.wordId === wb.id || cleanWord(s.word) === cleanWord(wb.word));
|
|
332
|
+
const isActive = activeTargetWord?.id === wb.id && !isFound && !isExample;
|
|
333
|
+
return (_jsxs("div", { onClick: () => !isExample && !isFound && handleSelectWordBankItem(wb), className: `px-3 py-1.5 rounded-xl text-sm font-semibold transition-all duration-150 border flex items-center gap-1.5 ${isExample
|
|
334
|
+
? 'bg-amber-50 text-amber-800 border-amber-300 italic opacity-80 cursor-default line-through'
|
|
335
|
+
: isFound
|
|
336
|
+
? 'bg-emerald-50 text-emerald-800 border-emerald-300 line-through shadow-xs cursor-pointer'
|
|
337
|
+
: isActive
|
|
338
|
+
? 'bg-blue-50 text-blue-900 border-blue-500 ring-2 ring-blue-400 font-extrabold scale-105 shadow-sm cursor-pointer'
|
|
339
|
+
: 'bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border-gray-200 dark:border-gray-700 hover:border-blue-300 cursor-pointer'}`, children: [_jsx("span", { children: wb.word }), isExample && _jsx("span", { className: "text-xs text-amber-600 font-bold no-underline", children: "(Ex)" }), isFound && !isExample && (_jsx("button", { type: "button", onClick: (e) => {
|
|
340
|
+
e.stopPropagation();
|
|
341
|
+
handleRemoveSelection(wb.id, wb.word);
|
|
342
|
+
}, className: "text-gray-400 hover:text-red-500 text-xs ml-1", title: "B\u1EA5m \u0111\u1EC3 x\u00F3a t\u1EEB n\u00E0y", children: "\u2715" }))] }, wb.id || idx));
|
|
343
|
+
}) })] }, cat.id));
|
|
344
|
+
}) })] })) : (
|
|
345
|
+
/* WORD_BANK MODE */
|
|
346
|
+
_jsxs(Card, { className: "p-4 sm:p-5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 shadow-sm rounded-2xl", children: [_jsxs("div", { className: "flex items-center justify-between mb-3 border-b border-gray-100 dark:border-gray-800 pb-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wider", children: "Word Bank (Danh s\u00E1ch t\u1EEB c\u1EA7n t\u00ECm)" }), _jsxs(Badge, { variant: "secondary", className: "text-xs font-semibold", children: [foundCount, " / ", playableWords.length, " t\u1EEB"] })] }), !isReviewMode && studentSelections.length > 0 && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
347
|
+
emitAnswer([]);
|
|
348
|
+
setTappedPath([]);
|
|
349
|
+
}, className: "text-xs text-gray-400 hover:text-red-500 gap-1 h-7", children: [_jsx(RotateCcw, { className: "h-3 w-3" }), " L\u00E0m l\u1EA1i t\u1EA5t c\u1EA3"] }))] }), _jsx("div", { className: "flex flex-wrap gap-2.5", children: wordBank.map((wb, idx) => {
|
|
350
|
+
const isExample = Boolean(wb.isExample);
|
|
351
|
+
const isFound = studentSelections.some((s) => s.wordId === wb.id || cleanWord(s.word) === cleanWord(wb.word));
|
|
352
|
+
const isActive = activeTargetWord?.id === wb.id && !isFound && !isExample;
|
|
353
|
+
return (_jsxs("div", { onClick: () => !isExample && !isFound && handleSelectWordBankItem(wb), className: `relative px-3.5 py-2 rounded-xl text-sm sm:text-base font-semibold transition-all duration-200 border flex items-center gap-1.5 ${isExample
|
|
354
|
+
? 'bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-200 border-amber-300 dark:border-amber-700 line-through opacity-80 cursor-default'
|
|
355
|
+
: isFound
|
|
356
|
+
? 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-200 border-emerald-300 dark:border-emerald-700 line-through shadow-xs cursor-pointer'
|
|
357
|
+
: isActive
|
|
358
|
+
? 'bg-blue-50 dark:bg-blue-950/60 text-blue-900 dark:text-blue-100 border-blue-500 ring-3 ring-blue-400/50 font-extrabold scale-105 shadow-md cursor-pointer'
|
|
359
|
+
: 'bg-gray-50 dark:bg-gray-800 text-gray-800 dark:text-gray-200 border-gray-200 dark:border-gray-700 hover:border-blue-400 cursor-pointer shadow-2xs'}`, children: [_jsx("span", { children: wb.word }), isExample && (_jsx("span", { className: "text-xs text-amber-600 font-bold no-underline", children: "(Example)" })), isFound && !isExample && (_jsx("span", { className: "text-xs text-emerald-600 font-bold no-underline", children: "\u2713" })), isFound && !isExample && !isReviewMode && (_jsx("button", { type: "button", onClick: (e) => {
|
|
360
|
+
e.stopPropagation();
|
|
361
|
+
handleRemoveSelection(wb.id, wb.word);
|
|
362
|
+
}, className: "ml-1 text-gray-400 hover:text-red-500 text-xs", title: "B\u1EA5m \u0111\u1EC3 x\u00F3a t\u1EEB n\u00E0y", children: "\u2715" })), isActive && (_jsx("span", { className: "ml-1 text-[11px] bg-blue-600 text-white px-1.5 py-0.5 rounded-md font-bold uppercase tracking-wider animate-pulse", children: "\u0110ang t\u00ECm" }))] }, wb.id || idx));
|
|
363
|
+
}) })] })), !isReviewMode && activeTargetWord && (_jsxs(Card, { className: "p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/40 dark:to-indigo-950/40 border-2 border-blue-300 dark:border-blue-700 rounded-2xl shadow-sm space-y-3", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Sparkles, { className: "h-5 w-5 text-blue-600 dark:text-blue-400 animate-bounce" }), _jsx("span", { className: "text-xs sm:text-sm font-extrabold text-blue-900 dark:text-blue-100 uppercase tracking-wider", children: "M\u1EE5c ti\u00EAu hi\u1EC7n t\u1EA1i:" }), _jsxs("span", { className: "text-base sm:text-lg font-black text-blue-700 dark:text-blue-300", children: ["\"", activeTargetWord.word, "\""] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [tappedPath.length > 0 && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setTappedPath((prev) => prev.slice(0, -1)), className: "h-8 text-xs font-bold gap-1 bg-white hover:bg-rose-50 hover:text-rose-600 border-gray-300", children: [_jsx(Undo2, { className: "h-3.5 w-3.5" }), " X\u00F3a ch\u1EEF v\u1EEBa b\u1EA5m (\u232B)"] })), tappedPath.length > 0 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setTappedPath([]), className: "h-8 text-xs text-gray-500 hover:text-gray-800", children: "L\u00E0m l\u1EA1i" }))] })] }), _jsx("div", { className: "flex flex-wrap items-center gap-2 pt-1", children: activeCharSequence.map((targetChar, idx) => {
|
|
364
|
+
const isFilled = idx < tappedPath.length;
|
|
365
|
+
const isNext = idx === tappedPath.length;
|
|
366
|
+
return (_jsx("div", { className: `h-10 w-10 sm:h-12 sm:w-12 rounded-xl flex items-center justify-center font-black text-lg sm:text-xl transition-all duration-200 border ${isFilled
|
|
367
|
+
? 'bg-blue-600 text-white border-blue-700 shadow-md scale-105'
|
|
368
|
+
: isNext
|
|
369
|
+
? 'bg-white dark:bg-gray-800 text-blue-600 border-blue-500 border-2 ring-4 ring-blue-200 dark:ring-blue-900 animate-pulse'
|
|
370
|
+
: 'bg-white/80 dark:bg-gray-800/80 text-gray-400 border-gray-200 dark:border-gray-700'}`, children: isFilled ? tappedChars[idx] || targetChar : targetChar }, idx));
|
|
371
|
+
}) }), _jsx("p", { className: "text-xs text-blue-700/80 dark:text-blue-300/80 font-medium", children: "\uD83D\uDC49 Ch\u1EA1m l\u1EA7n l\u01B0\u1EE3t t\u1EEBng ch\u1EEF c\u00E1i tr\u00EAn b\u1EA3ng ch\u1EEF c\u00E1i b\u00EAn d\u01B0\u1EDBi. (Ho\u1EB7c ch\u1EA1m ch\u1EEF c\u00E1i \u0111\u1EA7u \u2794 ch\u1EA1m ch\u1EEF c\u00E1i cu\u1ED1i \u0111\u1EC3 khoanh nhanh)." })] })), _jsx("div", { className: "flex flex-col items-center justify-center p-2 sm:p-4 bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-800", children: _jsx("div", { className: "overflow-x-auto max-w-full p-2 flex justify-center", children: _jsx("div", { className: "inline-grid gap-1.5 sm:gap-2 p-3 sm:p-4 bg-amber-50/30 dark:bg-gray-950/40 rounded-2xl border border-amber-200/60 dark:border-gray-800 touch-none", style: {
|
|
372
|
+
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
373
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
374
|
+
const r = rIdx + 1;
|
|
375
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
376
|
+
const c = cIdx + 1;
|
|
377
|
+
const char = grid[rIdx]?.[cIdx] || '';
|
|
378
|
+
const highlight = cellMap[`${r}-${c}`];
|
|
379
|
+
return (_jsxs("button", { type: "button", onClick: () => handleCellTap(r, c), className: `relative h-9 w-9 sm:h-11 sm:w-11 md:h-12 md:w-12 flex items-center justify-center font-black text-base sm:text-lg select-none rounded-xl sm:rounded-2xl transition-all duration-150 cursor-pointer border ${highlight
|
|
380
|
+
? `${highlight.colorConfig.bg} font-extrabold shadow-sm scale-95 border-2`
|
|
381
|
+
: 'border-transparent bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 hover:bg-blue-100/70 hover:border-blue-300 dark:hover:bg-gray-700 shadow-2xs'}`, title: highlight ? `${highlight.label} (${r},${c})` : `(${r},${c})`, children: [_jsx("span", { children: char || '·' }), highlight?.stepNumber && (_jsx("span", { className: "absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full bg-white text-blue-600 text-[10px] font-black flex items-center justify-center shadow-xs border border-blue-400", children: highlight.stepNumber }))] }, `${r}-${c}`));
|
|
382
|
+
});
|
|
383
|
+
}) }) }) }), isReviewMode && correctWords.length > 0 && (_jsxs(Card, { className: "p-4 bg-gray-50/80 dark:bg-gray-800/40 border border-gray-200 dark:border-gray-700 space-y-3 rounded-2xl", children: [_jsxs("div", { className: "flex items-center gap-2 font-bold text-sm text-gray-800 dark:text-gray-200 border-b pb-2", children: [_jsx(Eye, { className: "h-4 w-4 text-blue-600" }), _jsxs("span", { children: ["\u0110\u00E1p \u00E1n chi ti\u1EBFt (", correctWords.length, " t\u1EEB)"] })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2.5", children: correctWords.map((cw, idx) => {
|
|
384
|
+
const catName = cw.categoryId ? categoryMap.get(cw.categoryId) : undefined;
|
|
385
|
+
const pathStr = cw.path?.map((p) => `[${p.row},${p.col}]`).join(' → ');
|
|
386
|
+
return (_jsxs("div", { className: "p-2.5 rounded-lg bg-white dark:bg-gray-900 border text-xs space-y-1 shadow-2xs", children: [_jsxs("div", { className: "flex items-center justify-between font-bold", children: [_jsx("span", { className: "text-emerald-700 dark:text-emerald-300 uppercase", children: cw.word || cw.wordId }), catName && (_jsx(Badge, { variant: "outline", className: "text-[10px] bg-blue-50 text-blue-800 border-blue-200", children: catName }))] }), _jsx("div", { className: "text-[11px] text-gray-400 font-mono truncate", title: pathStr, children: pathStr })] }, cw.wordId || idx));
|
|
387
|
+
}) })] }))] }));
|
|
388
|
+
}
|
|
389
|
+
export default FindWordsInMatrixRenderer;
|
package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { MoversCrosswordPuzzleRendererProps } from '../../types';
|
|
3
|
-
export declare function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, clues, answers, correctAnswers, onAnswerChange, isReviewMode, caseSensitive, }: MoversCrosswordPuzzleRendererProps): React.JSX.Element;
|
|
3
|
+
export declare function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, markedColumn, hiddenWord, clues, answers, correctAnswers, onAnswerChange, isReviewMode, caseSensitive, }: MoversCrosswordPuzzleRendererProps): React.JSX.Element;
|
package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useState, useMemo, useRef, useEffect } from 'react';
|
|
|
4
4
|
import { Check, X } from 'lucide-react';
|
|
5
5
|
import CambridgeYlePartBanner from '../../../../themes/cambridge-yle/CambridgeYlePartBanner';
|
|
6
6
|
import CambridgeYleInstructionBanner from '../../../../themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
7
|
-
export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, clues, answers = {}, correctAnswers = {}, onAnswerChange, isReviewMode = false, caseSensitive = false, }) {
|
|
7
|
+
export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, markedColumn, hiddenWord, clues, answers = {}, correctAnswers = {}, onAnswerChange, isReviewMode = false, caseSensitive = false, }) {
|
|
8
8
|
const rows = gridSize?.rows || 12;
|
|
9
9
|
const cols = gridSize?.cols || 12;
|
|
10
10
|
const acrossClues = useMemo(() => clues?.across || [], [clues?.across]);
|
|
@@ -258,9 +258,31 @@ export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCo
|
|
|
258
258
|
const isSelected = selectedCell?.r === r && selectedCell?.c === c;
|
|
259
259
|
const isWordActive = activeWordCells.some((cell) => cell.r === r && cell.c === c);
|
|
260
260
|
const reviewStatus = getCellReviewStatus(r, c);
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
const isMarked = markedColumn && c === markedColumn;
|
|
262
|
+
const isExample = cellInfo.across?.isExample || cellInfo.down?.isExample;
|
|
263
|
+
// Example letter resolution
|
|
264
|
+
let exampleLetter = '';
|
|
265
|
+
if (isExample) {
|
|
266
|
+
if (cellInfo.across?.isExample && correctAnswers[`across-${cellInfo.across.number}`]) {
|
|
267
|
+
const word = correctAnswers[`across-${cellInfo.across.number}`];
|
|
268
|
+
exampleLetter = word[c - cellInfo.across.startCol] || '';
|
|
269
|
+
}
|
|
270
|
+
else if (cellInfo.down?.isExample && correctAnswers[`down-${cellInfo.down.number}`]) {
|
|
271
|
+
const word = correctAnswers[`down-${cellInfo.down.number}`];
|
|
272
|
+
exampleLetter = word[r - cellInfo.down.startRow] || '';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const cellValue = isExample ? exampleLetter : (answers[key] || '');
|
|
276
|
+
// Dynamic styles
|
|
277
|
+
let cellStyle = isMarked
|
|
278
|
+
? 'border-gray-400 bg-gray-400 text-gray-950 shadow-sm ring-1 ring-gray-400/50'
|
|
279
|
+
: 'border-gray-300 bg-gray-100 text-gray-800';
|
|
280
|
+
if (isExample) {
|
|
281
|
+
cellStyle = isMarked
|
|
282
|
+
? 'border-orange-400 bg-gray-400 text-orange-600 font-bold italic shadow-sm ring-1 ring-gray-400/50'
|
|
283
|
+
: 'border-gray-300 bg-gray-100 text-orange-600 font-bold italic';
|
|
284
|
+
}
|
|
285
|
+
else if (isReviewMode && reviewStatus) {
|
|
264
286
|
if (reviewStatus.isCorrect) {
|
|
265
287
|
cellStyle = 'border-green-500 bg-green-100 text-green-700 font-bold';
|
|
266
288
|
}
|
|
@@ -269,17 +291,21 @@ export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCo
|
|
|
269
291
|
}
|
|
270
292
|
}
|
|
271
293
|
else if (isSelected) {
|
|
272
|
-
cellStyle =
|
|
294
|
+
cellStyle = isMarked
|
|
295
|
+
? 'border-blue-600 bg-blue-200 ring-2 ring-blue-600 text-blue-950 font-bold'
|
|
296
|
+
: 'border-blue-600 bg-blue-100 ring-2 ring-blue-500 text-blue-900 font-bold';
|
|
273
297
|
}
|
|
274
298
|
else if (isWordActive) {
|
|
275
|
-
cellStyle =
|
|
299
|
+
cellStyle = isMarked
|
|
300
|
+
? 'border-blue-500 bg-slate-300 text-blue-950 font-bold ring-1 ring-blue-400'
|
|
301
|
+
: 'border-blue-400 bg-blue-50 text-blue-900';
|
|
276
302
|
}
|
|
277
303
|
return (_jsxs("div", { onClick: () => handleCellClick(r, c), className: `relative flex items-center justify-center rounded-lg border-2 font-bold shadow-sm h-8 w-8 sm:h-10 sm:w-10 md:h-11 md:w-11 text-base sm:text-lg transition-all select-none cursor-pointer ${cellStyle}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] sm:text-[10px] font-bold text-gray-500 leading-none", children: clueNum })), !isReviewMode ? (_jsx("input", { ref: (el) => {
|
|
278
304
|
if (el)
|
|
279
305
|
inputRefs.current.set(key, el);
|
|
280
306
|
else
|
|
281
307
|
inputRefs.current.delete(key);
|
|
282
|
-
}, type: "text", maxLength: 1, value: cellValue, onChange: (e) => handleInputChange(e.target.value, r, c), onKeyDown: (e) => handleKeyDown(e, r, c), onFocus: () => handleCellClick(r, c), className:
|
|
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));
|
|
283
309
|
});
|
|
284
310
|
}) }) })] })] })] }));
|
|
285
311
|
}
|
|
@@ -19,4 +19,5 @@ export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswer
|
|
|
19
19
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
20
20
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
21
21
|
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
22
|
+
export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
|
|
22
23
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -23,4 +23,5 @@ export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswer
|
|
|
23
23
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
24
24
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
25
25
|
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
26
|
+
export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
|
|
26
27
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -317,3 +317,53 @@ export interface MoversCrosswordPuzzleRendererProps {
|
|
|
317
317
|
isReviewMode?: boolean;
|
|
318
318
|
caseSensitive?: boolean;
|
|
319
319
|
}
|
|
320
|
+
export type FindWordsInMatrixType = 'WORD_BANK' | 'CATEGORIZE';
|
|
321
|
+
export interface FindWordsInMatrixCategoryItem {
|
|
322
|
+
id: string;
|
|
323
|
+
name: string;
|
|
324
|
+
color?: string;
|
|
325
|
+
}
|
|
326
|
+
export interface FindWordsInMatrixWordBankItem {
|
|
327
|
+
id: string;
|
|
328
|
+
word: string;
|
|
329
|
+
isExample?: boolean;
|
|
330
|
+
categoryId?: string;
|
|
331
|
+
}
|
|
332
|
+
export interface FindWordsInMatrixSelectionItem {
|
|
333
|
+
wordId?: string;
|
|
334
|
+
word?: string;
|
|
335
|
+
categoryId?: string;
|
|
336
|
+
path: Array<{
|
|
337
|
+
row: number;
|
|
338
|
+
col: number;
|
|
339
|
+
}>;
|
|
340
|
+
}
|
|
341
|
+
export interface FindWordsInMatrixRendererProps {
|
|
342
|
+
partNumber: number;
|
|
343
|
+
partName?: string;
|
|
344
|
+
questionCount: number;
|
|
345
|
+
instruction: string;
|
|
346
|
+
type?: FindWordsInMatrixType;
|
|
347
|
+
title?: string;
|
|
348
|
+
gridSize: {
|
|
349
|
+
rows: number;
|
|
350
|
+
cols: number;
|
|
351
|
+
};
|
|
352
|
+
grid: string[][];
|
|
353
|
+
categories?: FindWordsInMatrixCategoryItem[];
|
|
354
|
+
wordBank: FindWordsInMatrixWordBankItem[];
|
|
355
|
+
exampleSelection?: {
|
|
356
|
+
wordId: string;
|
|
357
|
+
categoryId?: string;
|
|
358
|
+
path: Array<{
|
|
359
|
+
row: number;
|
|
360
|
+
col: number;
|
|
361
|
+
}>;
|
|
362
|
+
};
|
|
363
|
+
selectedWords?: FindWordsInMatrixSelectionItem[];
|
|
364
|
+
correctWords?: FindWordsInMatrixSelectionItem[];
|
|
365
|
+
answers: Record<string, any>;
|
|
366
|
+
onAnswerChange: (questionId: string, value: any) => void;
|
|
367
|
+
isReviewMode?: boolean;
|
|
368
|
+
questionId?: string;
|
|
369
|
+
}
|