@tinyweb_dev/oe-exam-sdk 0.2.1 → 0.2.3

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.
Files changed (39) hide show
  1. package/dist/components/CueCardEditor.d.ts +1 -7
  2. package/dist/components/CueCardEditor.js +1 -58
  3. package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
  4. package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.d.ts +4 -0
  5. package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.js +389 -0
  6. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  7. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  8. package/dist/components/exams/take/types.d.ts +50 -0
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +41 -1
  10. package/dist/components/exams/take/utils/question-transformers.js +24 -0
  11. package/dist/components/questions/_shared/config/question-types.config.js +6 -0
  12. package/dist/components/questions/_shared/types/find-words-in-matrix.type.d.ts +18 -0
  13. package/dist/components/questions/_shared/types/find-words-in-matrix.type.js +1 -0
  14. package/dist/components/questions/creator/question-type-registry.js +2 -0
  15. package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
  16. package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
  17. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +3 -3
  18. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +9 -0
  19. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +69 -0
  20. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.d.ts +8 -0
  21. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +302 -0
  22. package/dist/components/questions/types/find-words-in-matrix/index.d.ts +4 -0
  23. package/dist/components/questions/types/find-words-in-matrix/index.js +4 -0
  24. package/dist/components/questions/types/find-words-in-matrix/register.d.ts +2 -0
  25. package/dist/components/questions/types/find-words-in-matrix/register.js +29 -0
  26. package/dist/components/questions/types/find-words-in-matrix/transform.d.ts +2 -0
  27. package/dist/components/questions/types/find-words-in-matrix/transform.js +89 -0
  28. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -1
  29. package/dist/components/shared/CueCardEditor.d.ts +7 -0
  30. package/dist/components/shared/CueCardEditor.js +44 -0
  31. package/dist/shared/constants/question-skills.js +2 -0
  32. package/dist/shared/lib/utils/question-reverse-transform.js +63 -0
  33. package/dist/shared/lib/utils/question-transform.js +2 -0
  34. package/dist/shared/types/common.types.d.ts +1 -1
  35. package/dist/shared/types/questions/find-words-in-matrix.d.ts +71 -0
  36. package/dist/shared/types/questions/find-words-in-matrix.js +8 -0
  37. package/dist/shared/types/questions/index.d.ts +1 -0
  38. package/dist/shared/types/questions/index.js +2 -0
  39. package/package.json +1 -1
@@ -1,7 +1 @@
1
- import React from 'react';
2
- export interface CueCardEditorProps {
3
- value?: string;
4
- onChange: (html: string) => void;
5
- label?: React.ReactNode;
6
- }
7
- export declare function CueCardEditor({ value, onChange, label, }: CueCardEditorProps): React.JSX.Element;
1
+ export * from './shared/CueCardEditor';
@@ -1,58 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useState } from 'react';
4
- import { useEditor, EditorContent } from '@tiptap/react';
5
- import StarterKit from '@tiptap/starter-kit';
6
- const DEFAULT_CONTENT = `
7
- <p><strong>Describe a topic you know well.</strong></p>
8
- <p>You should say:</p>
9
- <ul>
10
- <li>What it is</li>
11
- <li>When you first learned about it</li>
12
- <li>Why you find it interesting</li>
13
- </ul>
14
- <p><em>and explain how it has impacted your life.</em></p>
15
- `;
16
- export function CueCardEditor({ value, onChange, label, }) {
17
- const [mounted, setMounted] = useState(false);
18
- const editor = useEditor({
19
- immediatelyRender: false,
20
- extensions: [StarterKit],
21
- content: value || DEFAULT_CONTENT,
22
- onUpdate: ({ editor }) => {
23
- onChange(editor.getHTML());
24
- },
25
- editorProps: {
26
- attributes: {
27
- class: 'tiptap-editor focus:outline-none min-h-[250px] p-4 bg-white border border-gray-200 rounded-b-md',
28
- },
29
- },
30
- });
31
- useEffect(() => {
32
- setMounted(true);
33
- }, []);
34
- // Sync value if changed from outside (e.g., initial load)
35
- useEffect(() => {
36
- if (editor && value !== undefined && value !== editor.getHTML()) {
37
- // Small timeout to prevent update loops
38
- setTimeout(() => editor.commands.setContent(value), 0);
39
- }
40
- }, [value, editor]);
41
- if (!mounted)
42
- return null;
43
- return (_jsxs("div", { className: "w-full flex flex-col gap-4", children: [label && _jsx("div", { children: label }), _jsxs("div", { className: "flex flex-col border border-gray-300 rounded-md shadow-sm", children: [_jsxs("div", { className: "flex flex-wrap gap-2 p-2 relative bg-gray-50 border-b border-gray-300 rounded-t-md", children: [_jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleBold().run(), disabled: !editor?.can().chain().focus().toggleBold().run(), className: `px-3 py-1 rounded text-sm font-bold ${editor?.isActive('bold')
44
- ? 'bg-blue-500 text-white'
45
- : 'bg-white border text-gray-700 hover:bg-gray-100'}`, children: "B" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleItalic().run(), disabled: !editor?.can().chain().focus().toggleItalic().run(), className: `px-3 py-1 rounded text-sm italic ${editor?.isActive('italic')
46
- ? 'bg-blue-500 text-white'
47
- : 'bg-white border text-gray-700 hover:bg-gray-100'}`, children: "I" }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1 self-center" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleHeading({ level: 1 }).run(), className: `px-3 py-1 rounded text-sm font-bold ${editor?.isActive('heading', { level: 1 })
48
- ? 'bg-blue-500 text-white'
49
- : 'bg-white border text-gray-700 hover:bg-gray-100'}`, children: "H1" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(), className: `px-3 py-1 rounded text-sm font-bold ${editor?.isActive('heading', { level: 2 })
50
- ? 'bg-blue-500 text-white'
51
- : 'bg-white border text-gray-700 hover:bg-gray-100'}`, children: "H2" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleHeading({ level: 3 }).run(), className: `px-3 py-1 rounded text-sm font-bold ${editor?.isActive('heading', { level: 3 })
52
- ? 'bg-blue-500 text-white'
53
- : 'bg-white border text-gray-700 hover:bg-gray-100'}`, children: "H3" }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1 self-center" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleBulletList().run(), className: `px-3 py-1 rounded text-sm ${editor?.isActive('bulletList')
54
- ? 'bg-blue-500 text-white'
55
- : 'bg-white border text-gray-700 hover:bg-gray-100'}`, children: "\u2022 List" }), _jsx("button", { type: "button", onClick: () => editor?.chain().focus().toggleOrderedList().run(), className: `px-3 py-1 rounded text-sm ${editor?.isActive('orderedList')
56
- ? 'bg-blue-500 text-white'
57
- : 'bg-white border text-gray-700 hover:bg-gray-100'}`, children: "1. List" })] }), _jsx(EditorContent, { editor: editor })] })] }));
58
- }
1
+ export * from './shared/CueCardEditor';
@@ -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 }));
@@ -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;
@@ -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
+ }
@@ -5,7 +5,7 @@
5
5
  * Each transformer handles specific question type data structure.
6
6
  */
7
7
  import type { FillBlankQuestion, ChooseBestAnswerQuestion, PictureChooseSubQuestion, PictureFillBlankSubQuestion, ReadingPassageQuestion, LabelThePictureLabel, FillWordHintSubQuestion, ColoringHintItem, ColoringRegion } from '../types';
8
- export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
8
+ export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
9
9
  export interface ApiQuestion {
10
10
  id: string;
11
11
  content: any;
@@ -437,3 +437,43 @@ export interface CrosswordPuzzlePartData {
437
437
  explanation?: string;
438
438
  }
439
439
  export declare function transformCrosswordPuzzle(questions: ApiQuestion[]): CrosswordPuzzlePartData;
440
+ export interface FindWordsInMatrixPartData {
441
+ type?: 'WORD_BANK' | 'CATEGORIZE';
442
+ title?: string;
443
+ instruction: string;
444
+ gridSize: {
445
+ rows: number;
446
+ cols: number;
447
+ };
448
+ grid: string[][];
449
+ categories?: Array<{
450
+ id: string;
451
+ name: string;
452
+ color?: string;
453
+ }>;
454
+ wordBank: Array<{
455
+ id: string;
456
+ word: string;
457
+ isExample?: boolean;
458
+ categoryId?: string;
459
+ }>;
460
+ exampleSelection?: {
461
+ wordId: string;
462
+ categoryId?: string;
463
+ path: Array<{
464
+ row: number;
465
+ col: number;
466
+ }>;
467
+ };
468
+ correctWords: Array<{
469
+ wordId: string;
470
+ word?: string;
471
+ categoryId?: string;
472
+ path: Array<{
473
+ row: number;
474
+ col: number;
475
+ }>;
476
+ }>;
477
+ explanation?: string;
478
+ }
479
+ export declare function transformFindWordsInMatrix(questions: ApiQuestion[]): FindWordsInMatrixPartData;