@tinyweb_dev/oe-exam-sdk 0.1.31 → 0.1.32
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 +6 -2
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts +3 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +285 -0
- 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 +30 -0
- package/dist/components/exams/take/utils/answer-transformers.js +35 -5
- package/dist/components/exams/take/utils/question-transformers.d.ts +23 -1
- package/dist/components/exams/take/utils/question-transformers.js +36 -0
- package/dist/components/questions/_shared/config/question-types.config.js +6 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +16 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -1
- 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.d.ts +11 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +69 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.d.ts +8 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +213 -0
- package/dist/components/questions/types/crossword-puzzle/index.d.ts +4 -0
- package/dist/components/questions/types/crossword-puzzle/index.js +4 -0
- package/dist/components/questions/types/crossword-puzzle/register.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/register.js +29 -0
- package/dist/components/questions/types/crossword-puzzle/transform.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/transform.js +75 -0
- package/dist/components/questions/viewer/QuestionViewer.js +7 -4
- package/dist/components/results/renderers/review-question-body-movers.js +7 -2
- package/dist/components/results/review-data.js +8 -4
- package/dist/shared/constants/question-skills.js +2 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +54 -0
- package/dist/shared/lib/utils/question-transform-validation.js +12 -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/crossword-puzzle.d.ts +50 -0
- package/dist/shared/types/questions/crossword-puzzle.js +10 -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, } 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, } 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, } 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, } 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, }) {
|
|
@@ -112,6 +112,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
112
112
|
const data = transformGridFill(questions);
|
|
113
113
|
return (_jsx(MoversGridFillRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, grid: data.grid, rows: data.rows, columns: data.columns, cellAnswers: {}, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, disabledCells: data.disabledCells }));
|
|
114
114
|
}
|
|
115
|
+
case 'CROSSWORD_PUZZLE': {
|
|
116
|
+
const data = transformCrosswordPuzzle(questions);
|
|
117
|
+
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 }));
|
|
118
|
+
}
|
|
115
119
|
case 'IMAGE_OBJECT_MATCHING': {
|
|
116
120
|
// Reuse coloring renderer — same content structure as READ_AND_COLOR_OBJECTS
|
|
117
121
|
const data = transformColorObjects(questions);
|
package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import React from 'react';
|
|
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;
|
package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useMemo, useRef, useEffect } from 'react';
|
|
4
|
+
import { Check, X } from 'lucide-react';
|
|
5
|
+
import CambridgeYlePartBanner from '../../../../themes/cambridge-yle/CambridgeYlePartBanner';
|
|
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, }) {
|
|
8
|
+
const rows = gridSize?.rows || 12;
|
|
9
|
+
const cols = gridSize?.cols || 12;
|
|
10
|
+
const acrossClues = useMemo(() => clues?.across || [], [clues?.across]);
|
|
11
|
+
const downClues = useMemo(() => clues?.down || [], [clues?.down]);
|
|
12
|
+
const [activeDirection, setActiveDirection] = useState('across');
|
|
13
|
+
const [activeClueNumber, setActiveClueNumber] = useState(() => {
|
|
14
|
+
return acrossClues[0]?.number || downClues[0]?.number || 1;
|
|
15
|
+
});
|
|
16
|
+
const [selectedCell, setSelectedCell] = useState(null);
|
|
17
|
+
const inputRefs = useRef(new Map());
|
|
18
|
+
const { cellMap, clueNumberMap, wordCellsMap } = useMemo(() => {
|
|
19
|
+
const cMap = new Map();
|
|
20
|
+
const numMap = new Map();
|
|
21
|
+
const wMap = new Map();
|
|
22
|
+
for (const c of acrossClues) {
|
|
23
|
+
numMap.set(`${c.startRow}-${c.startCol}`, c.number);
|
|
24
|
+
const cells = [];
|
|
25
|
+
for (let i = 0; i < c.length; i++) {
|
|
26
|
+
const key = `${c.startRow}-${c.startCol + i}`;
|
|
27
|
+
cells.push({ r: c.startRow, c: c.startCol + i });
|
|
28
|
+
const existing = cMap.get(key) || {};
|
|
29
|
+
cMap.set(key, { ...existing, across: c });
|
|
30
|
+
}
|
|
31
|
+
wMap.set(`across-${c.number}`, cells);
|
|
32
|
+
}
|
|
33
|
+
for (const c of downClues) {
|
|
34
|
+
if (!numMap.has(`${c.startRow}-${c.startCol}`)) {
|
|
35
|
+
numMap.set(`${c.startRow}-${c.startCol}`, c.number);
|
|
36
|
+
}
|
|
37
|
+
const cells = [];
|
|
38
|
+
for (let i = 0; i < c.length; i++) {
|
|
39
|
+
const key = `${c.startRow + i}-${c.startCol}`;
|
|
40
|
+
cells.push({ r: c.startRow + i, c: c.startCol });
|
|
41
|
+
const existing = cMap.get(key) || {};
|
|
42
|
+
cMap.set(key, { ...existing, down: c });
|
|
43
|
+
}
|
|
44
|
+
wMap.set(`down-${c.number}`, cells);
|
|
45
|
+
}
|
|
46
|
+
return { cellMap: cMap, clueNumberMap: numMap, wordCellsMap: wMap };
|
|
47
|
+
}, [acrossClues, downClues]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!selectedCell) {
|
|
50
|
+
const firstClue = acrossClues[0] || downClues[0];
|
|
51
|
+
if (firstClue) {
|
|
52
|
+
setSelectedCell({ r: firstClue.startRow, c: firstClue.startCol });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, [acrossClues, downClues, selectedCell]);
|
|
56
|
+
const activeWordCells = useMemo(() => {
|
|
57
|
+
return wordCellsMap.get(`${activeDirection}-${activeClueNumber}`) || [];
|
|
58
|
+
}, [wordCellsMap, activeDirection, activeClueNumber]);
|
|
59
|
+
const handleSelectClue = (number, direction) => {
|
|
60
|
+
setActiveDirection(direction);
|
|
61
|
+
setActiveClueNumber(number);
|
|
62
|
+
const clueList = direction === 'across' ? acrossClues : downClues;
|
|
63
|
+
const clue = clueList.find((c) => c.number === number);
|
|
64
|
+
if (clue) {
|
|
65
|
+
setSelectedCell({ r: clue.startRow, c: clue.startCol });
|
|
66
|
+
const key = `${clue.startRow}-${clue.startCol}`;
|
|
67
|
+
inputRefs.current.get(key)?.focus();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const handleCellClick = (r, c) => {
|
|
71
|
+
const key = `${r}-${c}`;
|
|
72
|
+
const cellInfo = cellMap.get(key);
|
|
73
|
+
if (!cellInfo)
|
|
74
|
+
return;
|
|
75
|
+
if (selectedCell?.r === r && selectedCell?.c === c && cellInfo.across && cellInfo.down) {
|
|
76
|
+
const newDir = activeDirection === 'across' ? 'down' : 'across';
|
|
77
|
+
setActiveDirection(newDir);
|
|
78
|
+
const newClue = newDir === 'across' ? cellInfo.across : cellInfo.down;
|
|
79
|
+
if (newClue)
|
|
80
|
+
setActiveClueNumber(newClue.number);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
if (activeDirection === 'across' && cellInfo.across) {
|
|
84
|
+
setActiveClueNumber(cellInfo.across.number);
|
|
85
|
+
}
|
|
86
|
+
else if (activeDirection === 'down' && cellInfo.down) {
|
|
87
|
+
setActiveClueNumber(cellInfo.down.number);
|
|
88
|
+
}
|
|
89
|
+
else if (cellInfo.across) {
|
|
90
|
+
setActiveDirection('across');
|
|
91
|
+
setActiveClueNumber(cellInfo.across.number);
|
|
92
|
+
}
|
|
93
|
+
else if (cellInfo.down) {
|
|
94
|
+
setActiveDirection('down');
|
|
95
|
+
setActiveClueNumber(cellInfo.down.number);
|
|
96
|
+
}
|
|
97
|
+
setSelectedCell({ r, c });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const moveToNextCell = (r, c) => {
|
|
101
|
+
const cells = activeWordCells;
|
|
102
|
+
const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
|
|
103
|
+
if (currIdx >= 0 && currIdx < cells.length - 1) {
|
|
104
|
+
const next = cells[currIdx + 1];
|
|
105
|
+
setSelectedCell(next);
|
|
106
|
+
inputRefs.current.get(`${next.r}-${next.c}`)?.focus();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const moveToPrevCell = (r, c) => {
|
|
110
|
+
const cells = activeWordCells;
|
|
111
|
+
const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
|
|
112
|
+
if (currIdx > 0) {
|
|
113
|
+
const prev = cells[currIdx - 1];
|
|
114
|
+
setSelectedCell(prev);
|
|
115
|
+
inputRefs.current.get(`${prev.r}-${prev.c}`)?.focus();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const handleKeyDown = (e, r, c) => {
|
|
119
|
+
if (isReviewMode)
|
|
120
|
+
return;
|
|
121
|
+
if (e.key === 'Backspace') {
|
|
122
|
+
const key = `${r}-${c}`;
|
|
123
|
+
if (!answers[key]) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
moveToPrevCell(r, c);
|
|
126
|
+
const cells = activeWordCells;
|
|
127
|
+
const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
|
|
128
|
+
if (currIdx > 0) {
|
|
129
|
+
const prev = cells[currIdx - 1];
|
|
130
|
+
onAnswerChange(`${prev.r}-${prev.c}`, '');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
onAnswerChange(key, '');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (e.key === 'ArrowRight') {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
if (activeDirection !== 'across') {
|
|
140
|
+
setActiveDirection('across');
|
|
141
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
142
|
+
if (info?.across)
|
|
143
|
+
setActiveClueNumber(info.across.number);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
moveToNextCell(r, c);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (e.key === 'ArrowLeft') {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
if (activeDirection !== 'across') {
|
|
152
|
+
setActiveDirection('across');
|
|
153
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
154
|
+
if (info?.across)
|
|
155
|
+
setActiveClueNumber(info.across.number);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
moveToPrevCell(r, c);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (e.key === 'ArrowDown') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
if (activeDirection !== 'down') {
|
|
164
|
+
setActiveDirection('down');
|
|
165
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
166
|
+
if (info?.down)
|
|
167
|
+
setActiveClueNumber(info.down.number);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
moveToNextCell(r, c);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (e.key === 'ArrowUp') {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
if (activeDirection !== 'down') {
|
|
176
|
+
setActiveDirection('down');
|
|
177
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
178
|
+
if (info?.down)
|
|
179
|
+
setActiveClueNumber(info.down.number);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
moveToPrevCell(r, c);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const handleInputChange = (val, r, c) => {
|
|
187
|
+
if (isReviewMode)
|
|
188
|
+
return;
|
|
189
|
+
const cleanVal = val.slice(-1).toUpperCase();
|
|
190
|
+
const key = `${r}-${c}`;
|
|
191
|
+
onAnswerChange(key, cleanVal);
|
|
192
|
+
if (cleanVal) {
|
|
193
|
+
moveToNextCell(r, c);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const getCellReviewStatus = (r, c) => {
|
|
197
|
+
if (!isReviewMode)
|
|
198
|
+
return null;
|
|
199
|
+
const key = `${r}-${c}`;
|
|
200
|
+
const userLetter = (answers[key] || '').trim();
|
|
201
|
+
const cellInfo = cellMap.get(key);
|
|
202
|
+
if (!cellInfo)
|
|
203
|
+
return null;
|
|
204
|
+
let expectedLetter = '';
|
|
205
|
+
if (cellInfo.across && correctAnswers[`across-${cellInfo.across.number}`]) {
|
|
206
|
+
const word = correctAnswers[`across-${cellInfo.across.number}`];
|
|
207
|
+
const offset = c - cellInfo.across.startCol;
|
|
208
|
+
expectedLetter = word[offset] || '';
|
|
209
|
+
}
|
|
210
|
+
else if (cellInfo.down && correctAnswers[`down-${cellInfo.down.number}`]) {
|
|
211
|
+
const word = correctAnswers[`down-${cellInfo.down.number}`];
|
|
212
|
+
const offset = r - cellInfo.down.startRow;
|
|
213
|
+
expectedLetter = word[offset] || '';
|
|
214
|
+
}
|
|
215
|
+
if (!expectedLetter)
|
|
216
|
+
return null;
|
|
217
|
+
const isMatch = caseSensitive
|
|
218
|
+
? userLetter === expectedLetter
|
|
219
|
+
: userLetter.toLowerCase() === expectedLetter.toLowerCase();
|
|
220
|
+
return { isCorrect: isMatch, expected: expectedLetter, user: userLetter };
|
|
221
|
+
};
|
|
222
|
+
return (_jsxs("div", { className: "flex h-full flex-col gap-4 overflow-y-auto p-3 sm:gap-5 sm:p-4 md:gap-6 md:p-6", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), title && (_jsx("h3", { className: "text-center text-lg font-bold text-gray-800 tracking-wide", children: title })), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("div", { className: "lg:col-span-5 flex flex-col gap-4", children: [_jsxs("div", { className: "rounded-xl border-2 border-blue-200 bg-blue-50/40 p-4 shadow-sm", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-3 flex items-center justify-between", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-bold uppercase", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsxs("span", { className: "text-xs text-blue-600 font-medium", children: ["(", acrossClues.length, " c\u00E2u)"] })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-72 overflow-y-auto pr-1", children: acrossClues.map((c) => {
|
|
223
|
+
const isActive = activeDirection === 'across' && activeClueNumber === c.number;
|
|
224
|
+
const answerKey = `across-${c.number}`;
|
|
225
|
+
const expectedWord = correctAnswers[answerKey];
|
|
226
|
+
let userWord = '';
|
|
227
|
+
for (let i = 0; i < c.length; i++) {
|
|
228
|
+
userWord += (answers[`${c.startRow}-${c.startCol + i}`] || '').trim();
|
|
229
|
+
}
|
|
230
|
+
const isFilled = userWord.length === c.length;
|
|
231
|
+
return (_jsxs("button", { type: "button", onClick: () => handleSelectClue(c.number, 'across'), className: `flex items-start gap-2.5 p-2.5 rounded-lg border text-left transition ${isActive
|
|
232
|
+
? 'border-blue-600 bg-blue-100/90 shadow-sm ring-1 ring-blue-500'
|
|
233
|
+
: 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/60'}`, children: [_jsx("span", { className: `flex items-center justify-center h-6 w-6 rounded-md text-xs font-bold ${isActive ? 'bg-blue-600 text-white' : 'bg-blue-100 text-blue-800'}`, children: c.number }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: c.clue }), isReviewMode && expectedWord && (_jsxs("div", { className: "text-xs mt-1 flex items-center gap-2", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "font-bold text-green-700", children: expectedWord }), userWord && userWord !== expectedWord && (_jsx("span", { className: "line-through text-red-500 font-semibold", children: userWord }))] }))] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("span", { className: "text-xs text-gray-400 font-medium", children: ["(", c.length, ")"] }), isFilled && !isReviewMode && (_jsx("span", { className: "h-2 w-2 rounded-full bg-blue-500" }))] })] }, `across-${c.number}`));
|
|
234
|
+
}) })] }), _jsxs("div", { className: "rounded-xl border-2 border-purple-200 bg-purple-50/40 p-4 shadow-sm", children: [_jsxs("h4", { className: "font-bold text-purple-900 mb-3 flex items-center justify-between", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-purple-600 text-white text-xs font-bold uppercase", children: "Down" }), _jsx("span", { children: "H\u00E0ng d\u1ECDc" })] }), _jsxs("span", { className: "text-xs text-purple-600 font-medium", children: ["(", downClues.length, " c\u00E2u)"] })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-72 overflow-y-auto pr-1", children: downClues.map((c) => {
|
|
235
|
+
const isActive = activeDirection === 'down' && activeClueNumber === c.number;
|
|
236
|
+
const answerKey = `down-${c.number}`;
|
|
237
|
+
const expectedWord = correctAnswers[answerKey];
|
|
238
|
+
let userWord = '';
|
|
239
|
+
for (let i = 0; i < c.length; i++) {
|
|
240
|
+
userWord += (answers[`${c.startRow + i}-${c.startCol}`] || '').trim();
|
|
241
|
+
}
|
|
242
|
+
const isFilled = userWord.length === c.length;
|
|
243
|
+
return (_jsxs("button", { type: "button", onClick: () => handleSelectClue(c.number, 'down'), className: `flex items-start gap-2.5 p-2.5 rounded-lg border text-left transition ${isActive
|
|
244
|
+
? 'border-purple-600 bg-purple-100/90 shadow-sm ring-1 ring-purple-500'
|
|
245
|
+
: 'border-gray-200 bg-white hover:border-purple-300 hover:bg-purple-50/60'}`, children: [_jsx("span", { className: `flex items-center justify-center h-6 w-6 rounded-md text-xs font-bold ${isActive ? 'bg-purple-600 text-white' : 'bg-purple-100 text-purple-800'}`, children: c.number }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: c.clue }), isReviewMode && expectedWord && (_jsxs("div", { className: "text-xs mt-1 flex items-center gap-2", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "font-bold text-green-700", children: expectedWord }), userWord && userWord !== expectedWord && (_jsx("span", { className: "line-through text-red-500 font-semibold", children: userWord }))] }))] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("span", { className: "text-xs text-gray-400 font-medium", children: ["(", c.length, ")"] }), isFilled && !isReviewMode && (_jsx("span", { className: "h-2 w-2 rounded-full bg-purple-500" }))] })] }, `down-${c.number}`));
|
|
246
|
+
}) })] })] }), _jsxs("div", { className: "lg:col-span-7 flex flex-col items-center gap-4 bg-gray-50/80 p-4 sm:p-6 rounded-2xl border-2 border-gray-200 shadow-sm", children: [!isReviewMode && (_jsxs("div", { className: "flex items-center gap-3 text-xs font-semibold text-gray-600 bg-white px-3 py-1.5 rounded-full border border-gray-200 shadow-sm", children: [_jsx("span", { children: "\u0110ang ch\u1ECDn:" }), _jsx("span", { className: "flex items-center gap-1 text-blue-700", children: activeDirection === 'across' ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-bold", children: ["Across ", activeClueNumber] }), _jsx("span", { className: "text-gray-400 font-normal", children: "(Ngang)" })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-bold", children: ["Down ", activeClueNumber] }), _jsx("span", { className: "text-gray-400 font-normal", children: "(D\u1ECDc)" })] })) }), _jsx("span", { className: "text-gray-300", children: "|" }), _jsx("span", { className: "text-gray-500 font-normal", children: "Click \u0111\u00FAp \u00F4 giao nhau \u0111\u1EC3 \u0111\u1ED5i chi\u1EC1u" })] })), _jsx("div", { className: "overflow-x-auto max-w-full p-2", children: _jsx("div", { className: "inline-grid gap-1 sm:gap-1.5", style: {
|
|
247
|
+
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
248
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
249
|
+
const r = rIdx + 1;
|
|
250
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
251
|
+
const c = cIdx + 1;
|
|
252
|
+
const key = `${r}-${c}`;
|
|
253
|
+
const cellInfo = cellMap.get(key);
|
|
254
|
+
const clueNum = clueNumberMap.get(key);
|
|
255
|
+
if (!cellInfo) {
|
|
256
|
+
return (_jsx("div", { className: "h-8 w-8 sm:h-10 sm:w-10 md:h-11 md:w-11 bg-transparent" }, key));
|
|
257
|
+
}
|
|
258
|
+
const isSelected = selectedCell?.r === r && selectedCell?.c === c;
|
|
259
|
+
const isWordActive = activeWordCells.some((cell) => cell.r === r && cell.c === c);
|
|
260
|
+
const reviewStatus = getCellReviewStatus(r, c);
|
|
261
|
+
const cellValue = answers[key] || '';
|
|
262
|
+
let cellStyle = 'border-gray-400 bg-white text-gray-800';
|
|
263
|
+
if (isReviewMode && reviewStatus) {
|
|
264
|
+
if (reviewStatus.isCorrect) {
|
|
265
|
+
cellStyle = 'border-green-500 bg-green-100 text-green-700 font-bold';
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
cellStyle = 'border-red-500 bg-red-100 text-red-700 font-bold';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (isSelected) {
|
|
272
|
+
cellStyle = 'border-blue-600 bg-blue-100 ring-2 ring-blue-500 text-blue-900 font-bold';
|
|
273
|
+
}
|
|
274
|
+
else if (isWordActive) {
|
|
275
|
+
cellStyle = 'border-blue-400 bg-blue-50 text-blue-900';
|
|
276
|
+
}
|
|
277
|
+
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
|
+
if (el)
|
|
279
|
+
inputRefs.current.set(key, el);
|
|
280
|
+
else
|
|
281
|
+
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: "w-full h-full text-center bg-transparent uppercase font-bold outline-none 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
|
+
});
|
|
284
|
+
}) }) })] })] })] }));
|
|
285
|
+
}
|
|
@@ -18,4 +18,5 @@ export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRender
|
|
|
18
18
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
19
19
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
20
20
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
21
|
+
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
21
22
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -22,4 +22,5 @@ export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRender
|
|
|
22
22
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
23
23
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
24
24
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
25
|
+
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
25
26
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -287,3 +287,33 @@ export interface MoversGridFillRendererProps {
|
|
|
287
287
|
caseSensitive?: boolean;
|
|
288
288
|
disabledCells?: string[];
|
|
289
289
|
}
|
|
290
|
+
export interface CrosswordClueItem {
|
|
291
|
+
number: number;
|
|
292
|
+
clue: string;
|
|
293
|
+
startRow: number;
|
|
294
|
+
startCol: number;
|
|
295
|
+
length: number;
|
|
296
|
+
isExample?: boolean;
|
|
297
|
+
}
|
|
298
|
+
export interface MoversCrosswordPuzzleRendererProps {
|
|
299
|
+
partNumber: number;
|
|
300
|
+
partName?: string;
|
|
301
|
+
questionCount: number;
|
|
302
|
+
instruction: string;
|
|
303
|
+
title?: string;
|
|
304
|
+
gridSize: {
|
|
305
|
+
rows: number;
|
|
306
|
+
cols: number;
|
|
307
|
+
};
|
|
308
|
+
markedColumn?: number;
|
|
309
|
+
hiddenWord?: string;
|
|
310
|
+
clues: {
|
|
311
|
+
across: CrosswordClueItem[];
|
|
312
|
+
down: CrosswordClueItem[];
|
|
313
|
+
};
|
|
314
|
+
answers: Record<string, string>;
|
|
315
|
+
correctAnswers?: Record<string, string>;
|
|
316
|
+
onAnswerChange: (key: string, value: string) => void;
|
|
317
|
+
isReviewMode?: boolean;
|
|
318
|
+
caseSensitive?: boolean;
|
|
319
|
+
}
|
|
@@ -106,7 +106,7 @@ function findParentQuestion(subQuestionId, parts) {
|
|
|
106
106
|
*/
|
|
107
107
|
function findGridParentQuestion(parts) {
|
|
108
108
|
for (const part of parts) {
|
|
109
|
-
if (part.questionType === 'FILL_MISSING_WORDS_IN_GRID') {
|
|
109
|
+
if (part.questionType === 'FILL_MISSING_WORDS_IN_GRID' || part.questionType === 'CROSSWORD_PUZZLE') {
|
|
110
110
|
if (part.questions.length > 0) {
|
|
111
111
|
return {
|
|
112
112
|
parentId: part.questions[0].id,
|
|
@@ -417,10 +417,40 @@ export function transformAnswersForSubmission(answers, parts) {
|
|
|
417
417
|
}
|
|
418
418
|
// Add grid cell answers (grouped under parent question)
|
|
419
419
|
for (const [parentId, cellAnswers] of gridCellAnswers) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
420
|
+
const parentPart = parts.find((p) => p.questions.some((q) => q.id === parentId));
|
|
421
|
+
if (parentPart?.questionType === 'CROSSWORD_PUZZLE') {
|
|
422
|
+
const q = parentPart.questions.find((x) => x.id === parentId);
|
|
423
|
+
const content = q?.content;
|
|
424
|
+
const words = {};
|
|
425
|
+
if (content?.clues) {
|
|
426
|
+
const across = content.clues.across || [];
|
|
427
|
+
const down = content.clues.down || [];
|
|
428
|
+
for (const c of across) {
|
|
429
|
+
let word = '';
|
|
430
|
+
for (let i = 0; i < (c.length || 1); i++) {
|
|
431
|
+
word += (cellAnswers[`${c.startRow}-${c.startCol + i}`] || '').trim();
|
|
432
|
+
}
|
|
433
|
+
words[`across-${c.number}`] = word;
|
|
434
|
+
}
|
|
435
|
+
for (const c of down) {
|
|
436
|
+
let word = '';
|
|
437
|
+
for (let i = 0; i < (c.length || 1); i++) {
|
|
438
|
+
word += (cellAnswers[`${c.startRow + i}-${c.startCol}`] || '').trim();
|
|
439
|
+
}
|
|
440
|
+
words[`down-${c.number}`] = word;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
result.push({
|
|
444
|
+
questionId: parentId,
|
|
445
|
+
answer: { answers: words, cells: cellAnswers },
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
result.push({
|
|
450
|
+
questionId: parentId,
|
|
451
|
+
answer: { answers: cellAnswers },
|
|
452
|
+
});
|
|
453
|
+
}
|
|
424
454
|
}
|
|
425
455
|
// Add label answers (grouped under parent question)
|
|
426
456
|
for (const [parentId, labels] of labelAnswers) {
|
|
@@ -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' | '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' | '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;
|
|
@@ -415,3 +415,25 @@ export interface WordOrderAndMatchGroupPartData {
|
|
|
415
415
|
explanation?: string;
|
|
416
416
|
}
|
|
417
417
|
export declare function transformWordOrderAndMatchGroup(questions: ApiQuestion[]): WordOrderAndMatchGroupPartData;
|
|
418
|
+
export interface CrosswordClueItem {
|
|
419
|
+
number: number;
|
|
420
|
+
clue: string;
|
|
421
|
+
startRow: number;
|
|
422
|
+
startCol: number;
|
|
423
|
+
length: number;
|
|
424
|
+
}
|
|
425
|
+
export interface CrosswordPuzzlePartData {
|
|
426
|
+
title?: string;
|
|
427
|
+
instruction: string;
|
|
428
|
+
gridSize: {
|
|
429
|
+
rows: number;
|
|
430
|
+
cols: number;
|
|
431
|
+
};
|
|
432
|
+
clues: {
|
|
433
|
+
across: CrosswordClueItem[];
|
|
434
|
+
down: CrosswordClueItem[];
|
|
435
|
+
};
|
|
436
|
+
correctAnswers: Record<string, string>;
|
|
437
|
+
explanation?: string;
|
|
438
|
+
}
|
|
439
|
+
export declare function transformCrosswordPuzzle(questions: ApiQuestion[]): CrosswordPuzzlePartData;
|
|
@@ -1168,3 +1168,39 @@ export function transformWordOrderAndMatchGroup(questions) {
|
|
|
1168
1168
|
explanation: parent?.explanation,
|
|
1169
1169
|
};
|
|
1170
1170
|
}
|
|
1171
|
+
export function transformCrosswordPuzzle(questions) {
|
|
1172
|
+
const question = questions[0];
|
|
1173
|
+
const content = (question?.content || {});
|
|
1174
|
+
const correctAnswer = (question?.correct_answer || question?.correctAnswer || {});
|
|
1175
|
+
const rows = content?.gridSize?.rows || content?.rows || 12;
|
|
1176
|
+
const cols = content?.gridSize?.cols || content?.cols || 12;
|
|
1177
|
+
const across = Array.isArray(content?.clues?.across)
|
|
1178
|
+
? content.clues.across.map((c) => ({
|
|
1179
|
+
number: Number(c.number),
|
|
1180
|
+
clue: String(c.clue || '').trim(),
|
|
1181
|
+
startRow: Number(c.startRow) || 1,
|
|
1182
|
+
startCol: Number(c.startCol) || 1,
|
|
1183
|
+
length: Number(c.length) || 1,
|
|
1184
|
+
}))
|
|
1185
|
+
: [];
|
|
1186
|
+
const down = Array.isArray(content?.clues?.down)
|
|
1187
|
+
? content.clues.down.map((c) => ({
|
|
1188
|
+
number: Number(c.number),
|
|
1189
|
+
clue: String(c.clue || '').trim(),
|
|
1190
|
+
startRow: Number(c.startRow) || 1,
|
|
1191
|
+
startCol: Number(c.startCol) || 1,
|
|
1192
|
+
length: Number(c.length) || 1,
|
|
1193
|
+
}))
|
|
1194
|
+
: [];
|
|
1195
|
+
const correctAnswers = correctAnswer?.answers && typeof correctAnswer.answers === 'object'
|
|
1196
|
+
? correctAnswer.answers
|
|
1197
|
+
: {};
|
|
1198
|
+
return {
|
|
1199
|
+
title: content?.title || '',
|
|
1200
|
+
instruction: question?.instruction || content?.instruction || 'Complete the crossword.',
|
|
1201
|
+
gridSize: { rows, cols },
|
|
1202
|
+
clues: { across, down },
|
|
1203
|
+
correctAnswers,
|
|
1204
|
+
explanation: question?.explanation,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
@@ -317,6 +317,11 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
317
317
|
requiresContent: true,
|
|
318
318
|
hasDedicatedComponent: true,
|
|
319
319
|
},
|
|
320
|
+
CROSSWORD_PUZZLE: {
|
|
321
|
+
label: 'Giải ô chữ (Crossword)',
|
|
322
|
+
requiresContent: true,
|
|
323
|
+
hasDedicatedComponent: true,
|
|
324
|
+
},
|
|
320
325
|
};
|
|
321
326
|
export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
322
327
|
'FILL_IN_BLANK',
|
|
@@ -362,6 +367,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
362
367
|
'GN_SPEAKING_INTERVIEW',
|
|
363
368
|
'SPEAKING_CUE_CARD',
|
|
364
369
|
'WORD_FILL_STRUCTURED_FORM',
|
|
370
|
+
'CROSSWORD_PUZZLE',
|
|
365
371
|
];
|
|
366
372
|
export function getQuestionTypeLabel(type) {
|
|
367
373
|
return QUESTION_TYPE_CONFIG[type]?.label || type;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer } from '../../../../shared/types/questions/crossword-puzzle';
|
|
2
|
+
export interface CrosswordClueFormItem extends CrosswordClue {
|
|
3
|
+
id: string;
|
|
4
|
+
answer: string;
|
|
5
|
+
direction: 'across' | 'down';
|
|
6
|
+
}
|
|
7
|
+
export interface CrosswordPuzzleFormState {
|
|
8
|
+
title?: string;
|
|
9
|
+
rows: number;
|
|
10
|
+
cols: number;
|
|
11
|
+
clues: CrosswordClueFormItem[];
|
|
12
|
+
caseSensitive: boolean;
|
|
13
|
+
points: number;
|
|
14
|
+
explanation?: string;
|
|
15
|
+
}
|
|
16
|
+
export type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -34,7 +34,7 @@ const PASSAGE_BASED_TYPES = [
|
|
|
34
34
|
'READ_PASSAGE_AND_COMPLETE_STORY',
|
|
35
35
|
];
|
|
36
36
|
// Grid-based question types
|
|
37
|
-
const GRID_BASED_TYPES = ['FILL_MISSING_WORDS_IN_GRID'];
|
|
37
|
+
const GRID_BASED_TYPES = ['FILL_MISSING_WORDS_IN_GRID', 'CROSSWORD_PUZZLE'];
|
|
38
38
|
// Get display icon for question type
|
|
39
39
|
const getQuestionTypeIcon = (type) => {
|
|
40
40
|
if (IMAGE_BASED_TYPES.includes(type)) {
|
|
@@ -43,6 +43,7 @@ import { gnSpeakingInterviewRegistration } from '../types/gn-speaking-interview/
|
|
|
43
43
|
import { speakingCueCardRegistration } from '../types/speaking-cue-card/register';
|
|
44
44
|
import { wordFillStructuredFormRegistration } from '../types/word-fill-structured-form/register';
|
|
45
45
|
import { wordOrderAndMatchGroupRegistration } from '../types/word-order-and-match-group/register';
|
|
46
|
+
import { crosswordPuzzleRegistration } from '../types/crossword-puzzle/register';
|
|
46
47
|
// ─── Registry map ───────────────────────────────────────────────────────────
|
|
47
48
|
export const questionTypeRegistry = {
|
|
48
49
|
CHOOSE_THE_CORRECT_ANSWER: chooseTheCorrectAnswerRegistration,
|
|
@@ -89,4 +90,5 @@ export const questionTypeRegistry = {
|
|
|
89
90
|
GN_SPEAKING_INTERVIEW: gnSpeakingInterviewRegistration,
|
|
90
91
|
SPEAKING_CUE_CARD: speakingCueCardRegistration,
|
|
91
92
|
WORD_FILL_STRUCTURED_FORM: wordFillStructuredFormRegistration,
|
|
93
|
+
CROSSWORD_PUZZLE: crosswordPuzzleRegistration,
|
|
92
94
|
};
|
|
@@ -14,7 +14,8 @@ export declare enum QuestionBankEditorType {
|
|
|
14
14
|
WRITE_A_SHORT_LETTER = "WRITE_A_SHORT_LETTER",
|
|
15
15
|
WRITE_CORRECT_VERB_FORM = "WRITE_CORRECT_VERB_FORM",
|
|
16
16
|
FILL_IN_BLANK = "FILL_IN_BLANK",
|
|
17
|
-
CHOOSE_CORRECT_ADJECTIVE = "CHOOSE_CORRECT_ADJECTIVE"
|
|
17
|
+
CHOOSE_CORRECT_ADJECTIVE = "CHOOSE_CORRECT_ADJECTIVE",
|
|
18
|
+
CROSSWORD_PUZZLE = "CROSSWORD_PUZZLE"
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Renders the appropriate Creator for a question-bank item.
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { t } from '../../../shared/lib/i18n';
|
|
5
5
|
import { renderChooseCorrectAdjectiveEditor, renderChooseTheCorrectAnswerEditor, renderFillInBlankEditor, renderFillMissingWordsInGridEditor, renderImageObjectMatchingEditor, renderLabelThePictureEditor, renderLookPictureChooseCorrectAnswerEditor, renderLookPictureFillBlankChooseAnswerEditor, renderLookPictureFillWordHintEditor, renderReadAndColorObjectsEditor, renderReadPassageAndAnswerQuestionsEditor, renderWriteAShortLetterEditor, renderWriteCorrectVerbFormEditor, UnsupportedEditorFallback, } from './editor-adapters';
|
|
6
|
+
import { CrosswordPuzzleCreator } from '../types/crossword-puzzle/CrosswordPuzzleCreator';
|
|
6
7
|
/** Marketing question types supported by the question-bank editor. */
|
|
7
8
|
export var QuestionBankEditorType;
|
|
8
9
|
(function (QuestionBankEditorType) {
|
|
@@ -19,6 +20,7 @@ export var QuestionBankEditorType;
|
|
|
19
20
|
QuestionBankEditorType["WRITE_CORRECT_VERB_FORM"] = "WRITE_CORRECT_VERB_FORM";
|
|
20
21
|
QuestionBankEditorType["FILL_IN_BLANK"] = "FILL_IN_BLANK";
|
|
21
22
|
QuestionBankEditorType["CHOOSE_CORRECT_ADJECTIVE"] = "CHOOSE_CORRECT_ADJECTIVE";
|
|
23
|
+
QuestionBankEditorType["CROSSWORD_PUZZLE"] = "CROSSWORD_PUZZLE";
|
|
22
24
|
})(QuestionBankEditorType || (QuestionBankEditorType = {}));
|
|
23
25
|
/**
|
|
24
26
|
* Renders the appropriate Creator for a question-bank item.
|
|
@@ -69,6 +71,20 @@ export function QuestionEditorRenderer({ questionType, content, correctAnswer, e
|
|
|
69
71
|
return renderFillInBlankEditor(adapterProps);
|
|
70
72
|
case QuestionBankEditorType.CHOOSE_CORRECT_ADJECTIVE:
|
|
71
73
|
return renderChooseCorrectAdjectiveEditor(adapterProps);
|
|
74
|
+
case QuestionBankEditorType.CROSSWORD_PUZZLE:
|
|
75
|
+
return (_jsx(CrosswordPuzzleCreator, { initialData: {
|
|
76
|
+
content,
|
|
77
|
+
answer: correctAnswer,
|
|
78
|
+
explanation,
|
|
79
|
+
points,
|
|
80
|
+
}, onChange: (data) => {
|
|
81
|
+
onChange?.({
|
|
82
|
+
content: data.content,
|
|
83
|
+
answer: data.answer,
|
|
84
|
+
explanation: data.explanation || (explanation ?? undefined),
|
|
85
|
+
points: data.points ?? points,
|
|
86
|
+
});
|
|
87
|
+
} }));
|
|
72
88
|
default:
|
|
73
89
|
return (_jsx(UnsupportedEditorFallback, { questionType: questionType, content: content, correctAnswer: correctAnswer, explanation: explanation }));
|
|
74
90
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer } from '../../../../shared/types/questions/crossword-puzzle';
|
|
3
|
+
interface CrosswordPuzzleClientProps {
|
|
4
|
+
content: CrosswordPuzzleContent;
|
|
5
|
+
correctAnswer?: CrosswordPuzzleCorrectAnswer;
|
|
6
|
+
isReviewMode?: boolean;
|
|
7
|
+
userAnswers?: Record<string, string>;
|
|
8
|
+
onAnswerChange?: (key: string, value: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
11
|
+
export {};
|