@tinyweb_dev/oe-exam-sdk 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.d.ts +4 -0
- package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.js +389 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +33 -7
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/types.d.ts +50 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +41 -1
- package/dist/components/exams/take/utils/question-transformers.js +24 -0
- package/dist/components/questions/_shared/config/question-types.config.js +6 -0
- package/dist/components/questions/_shared/types/find-words-in-matrix.type.d.ts +18 -0
- package/dist/components/questions/_shared/types/find-words-in-matrix.type.js +1 -0
- package/dist/components/questions/creator/question-type-registry.js +2 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
- package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +10 -6
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +9 -0
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +69 -0
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.d.ts +8 -0
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +302 -0
- package/dist/components/questions/types/find-words-in-matrix/index.d.ts +4 -0
- package/dist/components/questions/types/find-words-in-matrix/index.js +4 -0
- package/dist/components/questions/types/find-words-in-matrix/register.d.ts +2 -0
- package/dist/components/questions/types/find-words-in-matrix/register.js +29 -0
- package/dist/components/questions/types/find-words-in-matrix/transform.d.ts +2 -0
- package/dist/components/questions/types/find-words-in-matrix/transform.js +89 -0
- package/dist/shared/constants/question-skills.js +2 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +63 -0
- package/dist/shared/lib/utils/question-transform.js +2 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +71 -0
- package/dist/shared/types/questions/find-words-in-matrix.js +8 -0
- package/dist/shared/types/questions/index.d.ts +1 -0
- package/dist/shared/types/questions/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { Input } from '../../../../components/ui/input';
|
|
6
|
+
import { Label } from '../../../../components/ui/label';
|
|
7
|
+
import { Card } from '../../../../components/ui/card';
|
|
8
|
+
import { Badge } from '../../../../components/ui/badge';
|
|
9
|
+
import { Checkbox } from '../../../../components/ui/checkbox';
|
|
10
|
+
import { Textarea } from '../../../../components/ui/textarea';
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../../components/ui/select';
|
|
12
|
+
import { Plus, Trash2, Sparkles, Clipboard, MousePointer, RotateCcw, Layers, ListFilter, FolderPlus, } from 'lucide-react';
|
|
13
|
+
const PALETTE = [
|
|
14
|
+
'#f59e0b', '#10b981', '#0ea5e9', '#8b5cf6', '#f43f5e',
|
|
15
|
+
'#6366f1', '#14b8a6', '#eab308', '#ec4899', '#06b6d4',
|
|
16
|
+
];
|
|
17
|
+
export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChangesChange, }) {
|
|
18
|
+
// Parse initial state
|
|
19
|
+
const parseInitial = () => {
|
|
20
|
+
const content = initialData?.content || {};
|
|
21
|
+
const answer = initialData?.answer || {};
|
|
22
|
+
const type = content.type || (Array.isArray(content.categories) && content.categories.length > 0 ? 'CATEGORIZE' : 'WORD_BANK');
|
|
23
|
+
const rows = Number(content.gridSize?.rows) || (Array.isArray(content.grid) ? content.grid.length : 14);
|
|
24
|
+
const cols = Number(content.gridSize?.cols) || (Array.isArray(content.grid?.[0]) ? content.grid[0].length : 14);
|
|
25
|
+
let grid = [];
|
|
26
|
+
if (Array.isArray(content.grid) && content.grid.length > 0) {
|
|
27
|
+
grid = content.grid.map((row) => Array.isArray(row) ? [...row].map((c) => String(c || '').toUpperCase()) : Array(cols).fill(''));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
grid = Array.from({ length: rows }, () => Array(cols).fill(''));
|
|
31
|
+
}
|
|
32
|
+
const categories = Array.isArray(content.categories) && content.categories.length > 0
|
|
33
|
+
? content.categories
|
|
34
|
+
: [
|
|
35
|
+
{ id: 'cat_1', name: 'Plants' },
|
|
36
|
+
{ id: 'cat_2', name: 'Animals' },
|
|
37
|
+
];
|
|
38
|
+
const words = [];
|
|
39
|
+
if (Array.isArray(content.wordBank)) {
|
|
40
|
+
const correctMap = new Map();
|
|
41
|
+
if (Array.isArray(answer.words)) {
|
|
42
|
+
answer.words.forEach((cw) => {
|
|
43
|
+
if (cw.wordId && Array.isArray(cw.path)) {
|
|
44
|
+
correctMap.set(cw.wordId, cw);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
content.wordBank.forEach((wb) => {
|
|
49
|
+
const isEx = Boolean(wb.isExample);
|
|
50
|
+
const correctInfo = correctMap.get(wb.id);
|
|
51
|
+
let path = correctInfo?.path;
|
|
52
|
+
let categoryId = wb.categoryId || correctInfo?.categoryId;
|
|
53
|
+
if (isEx && content.exampleSelection?.wordId === wb.id) {
|
|
54
|
+
path = content.exampleSelection.path;
|
|
55
|
+
if (content.exampleSelection.categoryId) {
|
|
56
|
+
categoryId = content.exampleSelection.categoryId;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
words.push({
|
|
60
|
+
id: wb.id || `w_${Math.random().toString(36).substring(2, 9)}`,
|
|
61
|
+
word: wb.word || '',
|
|
62
|
+
isExample: isEx,
|
|
63
|
+
...(categoryId ? { categoryId } : {}),
|
|
64
|
+
path: path || [],
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
type,
|
|
70
|
+
title: content.title || '',
|
|
71
|
+
instruction: initialData?.instruction || 'Find these words in the wordsearch box.',
|
|
72
|
+
rows,
|
|
73
|
+
cols,
|
|
74
|
+
grid,
|
|
75
|
+
categories,
|
|
76
|
+
words,
|
|
77
|
+
points: Number(initialData?.points) || 1,
|
|
78
|
+
explanation: initialData?.explanation || '',
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const [state, setState] = useState(parseInitial);
|
|
82
|
+
const [selectedWordIdx, setSelectedWordIdx] = useState(null);
|
|
83
|
+
const [isPickingPath, setIsPickingPath] = useState(false);
|
|
84
|
+
const [pasteModalOpen, setPasteModalOpen] = useState(false);
|
|
85
|
+
const [pasteText, setPasteText] = useState('');
|
|
86
|
+
const updateState = useCallback((updater) => {
|
|
87
|
+
setState((prev) => {
|
|
88
|
+
const next = updater(prev);
|
|
89
|
+
if (onChange) {
|
|
90
|
+
onChange({
|
|
91
|
+
content: {
|
|
92
|
+
type: next.type,
|
|
93
|
+
title: next.title,
|
|
94
|
+
gridSize: { rows: next.rows, cols: next.cols },
|
|
95
|
+
grid: next.grid,
|
|
96
|
+
...(next.type === 'CATEGORIZE' ? { categories: next.categories } : {}),
|
|
97
|
+
words: next.words,
|
|
98
|
+
},
|
|
99
|
+
answer: {
|
|
100
|
+
words: next.words
|
|
101
|
+
.filter((w) => !w.isExample && w.path && w.path.length > 0)
|
|
102
|
+
.map((w) => ({
|
|
103
|
+
wordId: w.id,
|
|
104
|
+
word: w.word,
|
|
105
|
+
...(w.categoryId ? { categoryId: w.categoryId } : {}),
|
|
106
|
+
path: w.path,
|
|
107
|
+
})),
|
|
108
|
+
},
|
|
109
|
+
points: next.points,
|
|
110
|
+
explanation: next.explanation,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
onUnsavedChangesChange?.(true);
|
|
114
|
+
return next;
|
|
115
|
+
});
|
|
116
|
+
}, [onChange, onUnsavedChangesChange]);
|
|
117
|
+
// Resize Grid helper
|
|
118
|
+
const handleResize = (newRows, newCols) => {
|
|
119
|
+
const r = Math.max(4, Math.min(25, newRows));
|
|
120
|
+
const c = Math.max(4, Math.min(25, newCols));
|
|
121
|
+
updateState((prev) => {
|
|
122
|
+
const newGrid = Array.from({ length: r }, (_, rIdx) => Array.from({ length: c }, (_, cIdx) => prev.grid[rIdx]?.[cIdx] || ''));
|
|
123
|
+
return { ...prev, rows: r, cols: c, grid: newGrid };
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
// Cell character change
|
|
127
|
+
const handleCellChange = (rIdx, cIdx, val) => {
|
|
128
|
+
const char = val.slice(-1).toUpperCase();
|
|
129
|
+
updateState((prev) => {
|
|
130
|
+
const newGrid = prev.grid.map((row) => [...row]);
|
|
131
|
+
newGrid[rIdx][cIdx] = char;
|
|
132
|
+
return { ...prev, grid: newGrid };
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
// Fill empty with random letters
|
|
136
|
+
const handleRandomFill = () => {
|
|
137
|
+
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
138
|
+
updateState((prev) => {
|
|
139
|
+
const newGrid = prev.grid.map((row) => row.map((cell) => (cell.trim() ? cell : letters[Math.floor(Math.random() * letters.length)])));
|
|
140
|
+
return { ...prev, grid: newGrid };
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
// Paste matrix string
|
|
144
|
+
const handleApplyPaste = () => {
|
|
145
|
+
const lines = pasteText
|
|
146
|
+
.split('\n')
|
|
147
|
+
.map((l) => l.trim().replace(/\s+/g, '').toUpperCase())
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
if (lines.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
const r = lines.length;
|
|
152
|
+
const c = Math.max(...lines.map((l) => l.length));
|
|
153
|
+
const newGrid = Array.from({ length: r }, (_, rIdx) => Array.from({ length: c }, (_, cIdx) => lines[rIdx]?.[cIdx] || ''));
|
|
154
|
+
updateState((prev) => ({
|
|
155
|
+
...prev,
|
|
156
|
+
rows: r,
|
|
157
|
+
cols: c,
|
|
158
|
+
grid: newGrid,
|
|
159
|
+
}));
|
|
160
|
+
setPasteModalOpen(false);
|
|
161
|
+
setPasteText('');
|
|
162
|
+
};
|
|
163
|
+
// Category management
|
|
164
|
+
const handleAddCategory = () => {
|
|
165
|
+
const newCat = {
|
|
166
|
+
id: `cat_${Date.now()}_${Math.random().toString(36).substring(2, 5)}`,
|
|
167
|
+
name: `Nhóm #${state.categories.length + 1}`,
|
|
168
|
+
};
|
|
169
|
+
updateState((prev) => ({
|
|
170
|
+
...prev,
|
|
171
|
+
categories: [...prev.categories, newCat],
|
|
172
|
+
}));
|
|
173
|
+
};
|
|
174
|
+
const handleRemoveCategory = (catId) => {
|
|
175
|
+
updateState((prev) => ({
|
|
176
|
+
...prev,
|
|
177
|
+
categories: prev.categories.filter((c) => c.id !== catId),
|
|
178
|
+
words: prev.words.map((w) => (w.categoryId === catId ? { ...w, categoryId: undefined } : w)),
|
|
179
|
+
}));
|
|
180
|
+
};
|
|
181
|
+
// Add word to word bank
|
|
182
|
+
const handleAddWord = () => {
|
|
183
|
+
const newWord = {
|
|
184
|
+
id: `w_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`,
|
|
185
|
+
word: '',
|
|
186
|
+
isExample: false,
|
|
187
|
+
...(state.type === 'CATEGORIZE' && state.categories[0] ? { categoryId: state.categories[0].id } : {}),
|
|
188
|
+
path: [],
|
|
189
|
+
};
|
|
190
|
+
updateState((prev) => ({ ...prev, words: [...prev.words, newWord] }));
|
|
191
|
+
setSelectedWordIdx(state.words.length);
|
|
192
|
+
};
|
|
193
|
+
// Remove word
|
|
194
|
+
const handleRemoveWord = (idx) => {
|
|
195
|
+
updateState((prev) => ({
|
|
196
|
+
...prev,
|
|
197
|
+
words: prev.words.filter((_, i) => i !== idx),
|
|
198
|
+
}));
|
|
199
|
+
if (selectedWordIdx === idx) {
|
|
200
|
+
setSelectedWordIdx(null);
|
|
201
|
+
setIsPickingPath(false);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
// Handle cell click during Path Picking Mode
|
|
205
|
+
const handleCellClickForPath = (r, c) => {
|
|
206
|
+
if (selectedWordIdx === null || !isPickingPath)
|
|
207
|
+
return;
|
|
208
|
+
const activeWord = state.words[selectedWordIdx];
|
|
209
|
+
if (!activeWord)
|
|
210
|
+
return;
|
|
211
|
+
const currentPath = activeWord.path || [];
|
|
212
|
+
const cellIdx = currentPath.findIndex((item) => item.row === r && item.col === c);
|
|
213
|
+
let nextPath;
|
|
214
|
+
if (cellIdx >= 0) {
|
|
215
|
+
nextPath = currentPath.filter((_, i) => i !== cellIdx);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
nextPath = [...currentPath, { row: r, col: c }];
|
|
219
|
+
}
|
|
220
|
+
updateState((prev) => {
|
|
221
|
+
const nextWords = [...prev.words];
|
|
222
|
+
nextWords[selectedWordIdx] = { ...activeWord, path: nextPath };
|
|
223
|
+
return { ...prev, words: nextWords };
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
// Path highlight mapping
|
|
227
|
+
const cellColorMap = {};
|
|
228
|
+
state.words.forEach((w, idx) => {
|
|
229
|
+
const color = PALETTE[idx % PALETTE.length];
|
|
230
|
+
w.path?.forEach((coord) => {
|
|
231
|
+
cellColorMap[`${coord.row}-${coord.col}`] = { color, word: w.word };
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
return (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { className: "p-4 space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { className: "font-semibold text-sm mb-2 block", children: "D\u1EA1ng b\u00E0i t\u1EADp (Type / Mode)" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [_jsxs("button", { type: "button", onClick: () => updateState((p) => ({ ...p, type: 'WORD_BANK' })), className: `flex items-center gap-3 p-3 rounded-xl border text-left transition-all ${state.type === 'WORD_BANK'
|
|
235
|
+
? 'border-blue-500 bg-blue-50/60 dark:bg-blue-950/30 text-blue-900 dark:text-blue-200 ring-2 ring-blue-400'
|
|
236
|
+
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-blue-100 dark:bg-blue-900 text-blue-600", children: _jsx(ListFilter, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "1. Danh s\u00E1ch t\u1EEB (Word Bank)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm c\u00E1c t\u1EEB c\u00F3 s\u1EB5n trong b\u1EA3ng ch\u1EEF c\u00E1i (VD: Gadgets)." })] })] }), _jsxs("button", { type: "button", onClick: () => updateState((p) => ({ ...p, type: 'CATEGORIZE' })), className: `flex items-center gap-3 p-3 rounded-xl border text-left transition-all ${state.type === 'CATEGORIZE'
|
|
237
|
+
? 'border-emerald-500 bg-emerald-50/60 dark:bg-emerald-950/30 text-emerald-900 dark:text-emerald-200 ring-2 ring-emerald-400'
|
|
238
|
+
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900 text-emerald-600", children: _jsx(Layers, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "2. T\u00ECm t\u1EEB & Ph\u00E2n nh\u00F3m (Categorize)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm t\u1EEB \u1EA9n v\u00E0 x\u1EBFp v\u00E0o c\u00E1c nh\u00F3m ti\u00EAu \u0111\u1EC1 (VD: Plants / Animals)." })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4 pt-2 border-t", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsx(Label, { className: "font-semibold", children: "Ti\u00EAu \u0111\u1EC1 b\u00E0i t\u1EADp" }), _jsx(Input, { value: state.title || '', onChange: (e) => updateState((p) => ({ ...p, title: e.target.value })), placeholder: state.type === 'CATEGORIZE' ? 'VD: Plants and Animals Word Search' : 'VD: Technology Gadgets Word Search', className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { className: "font-semibold", children: "K\u00EDch th\u01B0\u1EDBc ma tr\u1EADn (H\u00E0ng x C\u1ED9t)" }), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx(Input, { type: "number", min: 4, max: 25, value: state.rows, onChange: (e) => handleResize(Number(e.target.value), state.cols), className: "w-20 text-center font-bold" }), _jsx("span", { className: "font-bold text-gray-400", children: "\u00D7" }), _jsx(Input, { type: "number", min: 4, max: 25, value: state.cols, onChange: (e) => handleResize(state.rows, Number(e.target.value)), className: "w-20 text-center font-bold" })] })] })] })] }), state.type === 'CATEGORIZE' && (_jsxs(Card, { className: "p-4 space-y-3 bg-emerald-50/30 dark:bg-emerald-950/10 border-emerald-200 dark:border-emerald-800", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-bold text-sm text-emerald-900 dark:text-emerald-300 flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-4 w-4 text-emerald-600" }), "C\u00E1c nh\u00F3m danh m\u1EE5c (Categories / Headings)"] }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E1c c\u1ED9t ti\u00EAu \u0111\u1EC1 \u0111\u1EC3 h\u1ECDc sinh ph\u00E2n lo\u1EA1i t\u1EEB v\u00E0o (t\u1ED1i thi\u1EC3u 2 nh\u00F3m)." })] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: handleAddCategory, className: "gap-1 text-xs", children: [_jsx(FolderPlus, { className: "h-3.5 w-3.5" }), " Th\u00EAm nh\u00F3m"] })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3", children: state.categories.map((cat, idx) => (_jsxs("div", { className: "flex items-center gap-2 p-2.5 bg-white dark:bg-gray-900 rounded-lg border shadow-xs", children: [_jsx(Badge, { variant: "secondary", className: "font-mono text-xs", children: idx + 1 }), _jsx(Input, { value: cat.name, onChange: (e) => {
|
|
239
|
+
const val = e.target.value;
|
|
240
|
+
updateState((p) => ({
|
|
241
|
+
...p,
|
|
242
|
+
categories: p.categories.map((c, i) => (i === idx ? { ...c, name: val } : c)),
|
|
243
|
+
}));
|
|
244
|
+
}, placeholder: "T\u00EAn nh\u00F3m (VD: Plants)", className: "h-8 text-sm font-semibold" }), state.categories.length > 2 && (_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => handleRemoveCategory(cat.id), className: "h-8 w-8 p-0 text-red-500 hover:text-red-700", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, cat.id || idx))) })] })), _jsxs(Card, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b pb-3", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-bold text-base text-gray-800 dark:text-gray-200", children: ["Ma tr\u1EADn ch\u1EEF c\u00E1i (", state.rows, " h\u00E0ng \u00D7 ", state.cols, " c\u1ED9t)"] }), _jsx("p", { className: "text-xs text-gray-500", children: isPickingPath
|
|
245
|
+
? `👉 Đang gán vị trí cho từ: "${state.words[selectedWordIdx]?.word}". Hãy click các ô trên lưới theo thứ tự.`
|
|
246
|
+
: 'Nhập trực tiếp ký tự vào ô, hoặc dùng các nút công cụ bên phải.' })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setPasteModalOpen(!pasteModalOpen), className: "gap-1 text-xs", children: [_jsx(Clipboard, { className: "h-3.5 w-3.5" }), "Paste ma tr\u1EADn"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: handleRandomFill, className: "gap-1 text-xs text-emerald-600 hover:text-emerald-700", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), "L\u1EA5p ng\u1EABu nhi\u00EAn (A-Z)"] })] })] }), pasteModalOpen && (_jsxs("div", { className: "p-3 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 space-y-2", children: [_jsx(Label, { className: "text-xs font-bold text-blue-900 dark:text-blue-200", children: "D\u00E1n v\u0103n b\u1EA3n ma tr\u1EADn (m\u1ED7i d\u00F2ng 1 h\u00E0ng):" }), _jsx(Textarea, { value: pasteText, onChange: (e) => setPasteText(e.target.value), placeholder: `SEMONOFFP\nCROCODILE\n...`, rows: 4, className: "font-mono text-xs" }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPasteModalOpen(false), children: "H\u1EE7y" }), _jsx(Button, { size: "sm", onClick: handleApplyPaste, children: "\u00C1p d\u1EE5ng v\u00E0o ma tr\u1EADn" })] })] })), _jsx("div", { className: "overflow-x-auto pb-2 flex justify-center", children: _jsx("div", { className: "inline-grid gap-1 p-2 bg-gray-100 dark:bg-gray-900 rounded-lg border border-gray-300", style: {
|
|
247
|
+
gridTemplateColumns: `repeat(${state.cols}, minmax(0, 1fr))`,
|
|
248
|
+
}, children: Array.from({ length: state.rows }).map((_, rIdx) => {
|
|
249
|
+
const r = rIdx + 1;
|
|
250
|
+
return Array.from({ length: state.cols }).map((_, cIdx) => {
|
|
251
|
+
const c = cIdx + 1;
|
|
252
|
+
const char = state.grid[rIdx]?.[cIdx] || '';
|
|
253
|
+
const highlight = cellColorMap[`${r}-${c}`];
|
|
254
|
+
return (_jsx("div", { className: "relative", children: _jsx("input", { type: "text", maxLength: 1, value: char, onChange: (e) => handleCellChange(rIdx, cIdx, e.target.value), onClick: () => isPickingPath && handleCellClickForPath(r, c), className: `h-8 w-8 sm:h-10 sm:w-10 text-center font-bold text-sm sm:text-base uppercase rounded border transition-all ${isPickingPath ? 'cursor-pointer hover:ring-2 hover:ring-blue-400' : ''} ${highlight
|
|
255
|
+
? 'border-2 text-white font-extrabold shadow-sm'
|
|
256
|
+
: 'bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 border-gray-300'}`, style: highlight ? { backgroundColor: highlight.color, borderColor: highlight.color } : {}, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})` }) }, `${r}-${c}`));
|
|
257
|
+
});
|
|
258
|
+
}) }) })] }), _jsxs(Card, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between border-b pb-2", children: [_jsxs("div", { children: [_jsx("h4", { className: "font-bold text-base", children: state.type === 'CATEGORIZE' ? 'Danh sách từ & Nhóm phân loại' : 'Danh sách từ cần tìm (Word Bank)' }), _jsxs("p", { className: "text-xs text-gray-500", children: ["Nh\u1EADp t\u1EEB v\u1EF1ng, ", state.type === 'CATEGORIZE' ? 'chọn nhóm thuộc tính, ' : '', "nh\u1EA5n \"G\u00E1n v\u1ECB tr\u00ED\" v\u00E0 click c\u00E1c \u00F4 t\u01B0\u01A1ng \u1EE9ng tr\u00EAn ma tr\u1EADn."] })] }), _jsxs(Button, { type: "button", size: "sm", onClick: handleAddWord, className: "gap-1", children: [_jsx(Plus, { className: "h-4 w-4" }), " Th\u00EAm t\u1EEB"] })] }), _jsx("div", { className: "space-y-3", children: state.words.map((w, idx) => {
|
|
259
|
+
const isSelected = selectedWordIdx === idx;
|
|
260
|
+
const color = PALETTE[idx % PALETTE.length];
|
|
261
|
+
const pathLength = w.path?.length || 0;
|
|
262
|
+
const formedText = (w.path || [])
|
|
263
|
+
.map((coord) => state.grid[coord.row - 1]?.[coord.col - 1] || '')
|
|
264
|
+
.join('');
|
|
265
|
+
return (_jsxs("div", { className: `p-3 rounded-lg border transition-all ${isSelected
|
|
266
|
+
? 'border-blue-500 bg-blue-50/50 dark:bg-blue-950/20 ring-1 ring-blue-400'
|
|
267
|
+
: 'border-gray-200 bg-white dark:bg-gray-900'}`, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("div", { className: "w-4 h-4 rounded-full flex-shrink-0", style: { backgroundColor: color }, title: `Màu đại diện: ${color}` }), _jsx("div", { className: "flex-1 min-w-[180px]", children: _jsx(Input, { value: w.word, onChange: (e) => {
|
|
268
|
+
const val = e.target.value;
|
|
269
|
+
updateState((prev) => {
|
|
270
|
+
const nextWords = [...prev.words];
|
|
271
|
+
nextWords[idx] = { ...w, word: val };
|
|
272
|
+
return { ...prev, words: nextWords };
|
|
273
|
+
});
|
|
274
|
+
}, placeholder: `Từ #${idx + 1} (VD: ${state.type === 'CATEGORIZE' ? 'monkey' : 'digital camera'})`, className: "font-medium" }) }), state.type === 'CATEGORIZE' && (_jsx("div", { className: "w-40", children: _jsxs(Select, { value: w.categoryId || state.categories[0]?.id || '', onValueChange: (catVal) => {
|
|
275
|
+
updateState((prev) => {
|
|
276
|
+
const nextWords = [...prev.words];
|
|
277
|
+
nextWords[idx] = { ...w, categoryId: catVal };
|
|
278
|
+
return { ...prev, words: nextWords };
|
|
279
|
+
});
|
|
280
|
+
}, children: [_jsx(SelectTrigger, { className: "h-9 text-xs font-semibold", children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn nh\u00F3m" }) }), _jsx(SelectContent, { children: state.categories.map((c) => (_jsx(SelectItem, { value: c.id, className: "text-xs", children: c.name }, c.id))) })] }) })), _jsx("div", { className: "flex items-center gap-2", children: _jsxs("label", { className: "flex items-center gap-1.5 text-xs text-amber-700 dark:text-amber-300 font-medium cursor-pointer", children: [_jsx(Checkbox, { checked: Boolean(w.isExample), onCheckedChange: (checked) => {
|
|
281
|
+
updateState((prev) => {
|
|
282
|
+
const nextWords = [...prev.words];
|
|
283
|
+
nextWords[idx] = { ...w, isExample: Boolean(checked) };
|
|
284
|
+
return { ...prev, words: nextWords };
|
|
285
|
+
});
|
|
286
|
+
} }), "C\u00E2u v\u00ED d\u1EE5"] }) }), _jsxs(Button, { type: "button", size: "sm", variant: isSelected && isPickingPath ? 'default' : 'outline', onClick: () => {
|
|
287
|
+
if (isSelected && isPickingPath) {
|
|
288
|
+
setIsPickingPath(false);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
setSelectedWordIdx(idx);
|
|
292
|
+
setIsPickingPath(true);
|
|
293
|
+
}
|
|
294
|
+
}, className: "gap-1 text-xs", children: [_jsx(MousePointer, { className: "h-3.5 w-3.5" }), isSelected && isPickingPath ? 'Đang chọn ô...' : `Gán vị trí (${pathLength} ô)`] }), pathLength > 0 && (_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => {
|
|
295
|
+
updateState((prev) => {
|
|
296
|
+
const nextWords = [...prev.words];
|
|
297
|
+
nextWords[idx] = { ...w, path: [] };
|
|
298
|
+
return { ...prev, words: nextWords };
|
|
299
|
+
});
|
|
300
|
+
}, className: "text-xs text-gray-500 hover:text-red-500", title: "X\u00F3a \u0111\u01B0\u1EDDng \u0111i \u0111\u00E3 ch\u1ECDn", children: _jsx(RotateCcw, { className: "h-3.5 w-3.5" }) })), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => handleRemoveWord(idx), className: "text-red-500 hover:text-red-700 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), pathLength > 0 && (_jsxs("div", { className: "mt-2 text-xs flex items-center gap-2 text-gray-600 dark:text-gray-400 pl-7", children: [_jsx("span", { className: "font-semibold", children: "Chu\u1ED7i qu\u00E9t \u0111\u01B0\u1EE3c:" }), _jsx(Badge, { variant: "secondary", className: "font-mono uppercase tracking-widest", children: formedText || '(chưa có chữ)' }), _jsxs("span", { className: "text-gray-400", children: ["(T\u1ECDa \u0111\u1ED9: ", w.path?.map((c) => `[${c.row},${c.col}]`).join(' → '), ")"] })] }))] }, w.id || idx));
|
|
301
|
+
}) })] })] }));
|
|
302
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FindWordsInMatrixCreator } from './FindWordsInMatrixCreator';
|
|
2
|
+
export const findWordsInMatrixRegistration = {
|
|
3
|
+
component: FindWordsInMatrixCreator,
|
|
4
|
+
useRawInitialData: true,
|
|
5
|
+
getExtraProps: (ctx) => ({
|
|
6
|
+
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
7
|
+
}),
|
|
8
|
+
wrapOnSave: (data, ctx) => ({
|
|
9
|
+
type: ctx.questionType,
|
|
10
|
+
content: ctx.state.content,
|
|
11
|
+
points: data.points ?? ctx.state.points ?? 1,
|
|
12
|
+
level: ctx.state.level,
|
|
13
|
+
difficulty: ctx.state.difficulty,
|
|
14
|
+
skill: ctx.state.skill,
|
|
15
|
+
...(data.content !== undefined ? { content: data.content } : {}),
|
|
16
|
+
answer: data.answer ?? data,
|
|
17
|
+
explanation: data.explanation || '',
|
|
18
|
+
}),
|
|
19
|
+
wrapOnChange: (data, ctx) => ({
|
|
20
|
+
type: ctx.questionType,
|
|
21
|
+
points: data.points ?? ctx.state.points ?? 1,
|
|
22
|
+
level: ctx.state.level,
|
|
23
|
+
difficulty: ctx.state.difficulty,
|
|
24
|
+
skill: ctx.state.skill,
|
|
25
|
+
content: data.content,
|
|
26
|
+
answer: data.answer,
|
|
27
|
+
explanation: data.explanation || '',
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const transformFindWordsInMatrix = (question) => {
|
|
2
|
+
const contentData = question.content;
|
|
3
|
+
const answerData = question.answer;
|
|
4
|
+
let content;
|
|
5
|
+
let correctAnswer;
|
|
6
|
+
const modeType = contentData?.type || (Array.isArray(contentData?.categories) && contentData.categories.length > 0 ? 'CATEGORIZE' : 'WORD_BANK');
|
|
7
|
+
if (contentData?.gridSize && Array.isArray(contentData?.grid) && Array.isArray(contentData?.wordBank)) {
|
|
8
|
+
// Already in API shape
|
|
9
|
+
content = {
|
|
10
|
+
type: modeType,
|
|
11
|
+
title: contentData.title || '',
|
|
12
|
+
gridSize: {
|
|
13
|
+
rows: Number(contentData.gridSize.rows) || 14,
|
|
14
|
+
cols: Number(contentData.gridSize.cols) || 14,
|
|
15
|
+
},
|
|
16
|
+
grid: contentData.grid,
|
|
17
|
+
...(modeType === 'CATEGORIZE' && Array.isArray(contentData.categories) ? { categories: contentData.categories } : {}),
|
|
18
|
+
wordBank: contentData.wordBank,
|
|
19
|
+
...(contentData.exampleSelection ? { exampleSelection: contentData.exampleSelection } : {}),
|
|
20
|
+
};
|
|
21
|
+
correctAnswer = {
|
|
22
|
+
words: Array.isArray(answerData?.words) ? answerData.words : [],
|
|
23
|
+
caseSensitive: Boolean(answerData?.caseSensitive),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
else if (contentData?.grid && Array.isArray(contentData?.words)) {
|
|
27
|
+
// Form state format from Creator
|
|
28
|
+
const rows = Number(contentData.rows) || (Array.isArray(contentData.grid) ? contentData.grid.length : 14);
|
|
29
|
+
const cols = Number(contentData.cols) || (Array.isArray(contentData.grid?.[0]) ? contentData.grid[0].length : 14);
|
|
30
|
+
const grid = contentData.grid;
|
|
31
|
+
const categories = Array.isArray(contentData.categories) ? contentData.categories : [];
|
|
32
|
+
const wordBank = [];
|
|
33
|
+
const correctWords = [];
|
|
34
|
+
let exampleSelection;
|
|
35
|
+
for (const w of contentData.words) {
|
|
36
|
+
const item = {
|
|
37
|
+
id: w.id || `w_${Math.random().toString(36).substring(2, 9)}`,
|
|
38
|
+
word: String(w.word || '').trim(),
|
|
39
|
+
...(w.isExample ? { isExample: true } : {}),
|
|
40
|
+
...(w.categoryId ? { categoryId: w.categoryId } : {}),
|
|
41
|
+
};
|
|
42
|
+
wordBank.push(item);
|
|
43
|
+
if (w.path && Array.isArray(w.path) && w.path.length > 0) {
|
|
44
|
+
if (w.isExample) {
|
|
45
|
+
exampleSelection = {
|
|
46
|
+
wordId: item.id,
|
|
47
|
+
...(item.categoryId ? { categoryId: item.categoryId } : {}),
|
|
48
|
+
path: w.path,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
correctWords.push({
|
|
53
|
+
wordId: item.id,
|
|
54
|
+
word: item.word,
|
|
55
|
+
...(item.categoryId ? { categoryId: item.categoryId } : {}),
|
|
56
|
+
path: w.path,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
content = {
|
|
62
|
+
type: modeType,
|
|
63
|
+
title: contentData.title || '',
|
|
64
|
+
gridSize: { rows, cols },
|
|
65
|
+
grid,
|
|
66
|
+
...(modeType === 'CATEGORIZE' ? { categories } : {}),
|
|
67
|
+
wordBank,
|
|
68
|
+
...(exampleSelection ? { exampleSelection } : {}),
|
|
69
|
+
};
|
|
70
|
+
correctAnswer = {
|
|
71
|
+
words: correctWords,
|
|
72
|
+
caseSensitive: Boolean(contentData.caseSensitive),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
content = {
|
|
77
|
+
type: 'WORD_BANK',
|
|
78
|
+
gridSize: { rows: 14, cols: 14 },
|
|
79
|
+
grid: Array(14).fill(null).map(() => Array(14).fill('')),
|
|
80
|
+
wordBank: [],
|
|
81
|
+
};
|
|
82
|
+
correctAnswer = { words: [], caseSensitive: false };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
apiContent: content,
|
|
86
|
+
apiCorrectAnswer: correctAnswer,
|
|
87
|
+
explanation: answerData?.explanation || question.explanation || '',
|
|
88
|
+
};
|
|
89
|
+
};
|
|
@@ -79,6 +79,7 @@ export const QUESTION_TYPE_SKILLS = {
|
|
|
79
79
|
'FILL_BLANK': SkillEnum.READING,
|
|
80
80
|
'WORD_FILL_STRUCTURED_FORM': SkillEnum.READING,
|
|
81
81
|
'CROSSWORD_PUZZLE': SkillEnum.READING,
|
|
82
|
+
'FIND_WORDS_IN_MATRIX': SkillEnum.READING,
|
|
82
83
|
// ========================================
|
|
83
84
|
// WRITING SKILLS (Viết)
|
|
84
85
|
// ========================================
|
|
@@ -206,6 +207,7 @@ export const QUESTION_TYPE_LABELS = {
|
|
|
206
207
|
'MATCH_BY_WRITING_ANSWER': 'Ghép đáp án bằng cách viết',
|
|
207
208
|
'WORD_FILL_STRUCTURED_FORM': 'Điền từ vào biểu mẫu có cấu trúc',
|
|
208
209
|
'CROSSWORD_PUZZLE': 'Giải ô chữ (Crossword)',
|
|
210
|
+
'FIND_WORDS_IN_MATRIX': 'Tìm từ trong ma trận (Word Search)',
|
|
209
211
|
// ========================================
|
|
210
212
|
// WRITING SKILLS (Viết)
|
|
211
213
|
// ========================================
|
|
@@ -447,6 +447,7 @@ const handlers = {
|
|
|
447
447
|
SPEAKING_CUE_CARD: reverseSimpleAnswer,
|
|
448
448
|
GN_SPEAKING_INTERVIEW: reverseGroupPayload,
|
|
449
449
|
CROSSWORD_PUZZLE: reverseCrosswordPuzzle,
|
|
450
|
+
FIND_WORDS_IN_MATRIX: reverseFindWordsInMatrix,
|
|
450
451
|
};
|
|
451
452
|
function reverseCrosswordPuzzle(apiQuestion) {
|
|
452
453
|
const apiContent = (apiQuestion.content || {});
|
|
@@ -501,6 +502,68 @@ function reverseCrosswordPuzzle(apiQuestion) {
|
|
|
501
502
|
},
|
|
502
503
|
};
|
|
503
504
|
}
|
|
505
|
+
function reverseFindWordsInMatrix(apiQuestion) {
|
|
506
|
+
const apiContent = (apiQuestion.content || {});
|
|
507
|
+
const apiAnswer = (apiQuestion.correctAnswer || apiQuestion.correct_answer || apiQuestion.answer || {});
|
|
508
|
+
const type = apiContent.type || (Array.isArray(apiContent.categories) && apiContent.categories.length > 0 ? 'CATEGORIZE' : 'WORD_BANK');
|
|
509
|
+
const rows = apiContent.gridSize?.rows || (Array.isArray(apiContent.grid) ? apiContent.grid.length : 14);
|
|
510
|
+
const cols = apiContent.gridSize?.cols || (Array.isArray(apiContent.grid?.[0]) ? apiContent.grid[0].length : 14);
|
|
511
|
+
const title = apiContent.title || '';
|
|
512
|
+
const grid = Array.isArray(apiContent.grid) ? apiContent.grid : [];
|
|
513
|
+
const categories = Array.isArray(apiContent.categories) ? apiContent.categories : [];
|
|
514
|
+
const wordBank = Array.isArray(apiContent.wordBank) ? apiContent.wordBank : [];
|
|
515
|
+
const correctWords = Array.isArray(apiAnswer.words) ? apiAnswer.words : [];
|
|
516
|
+
const correctMap = new Map();
|
|
517
|
+
for (const cw of correctWords) {
|
|
518
|
+
if (cw.wordId) {
|
|
519
|
+
correctMap.set(cw.wordId, cw);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const words = wordBank.map((wb) => {
|
|
523
|
+
const isEx = Boolean(wb.isExample);
|
|
524
|
+
const correctInfo = correctMap.get(wb.id);
|
|
525
|
+
let path = correctInfo?.path;
|
|
526
|
+
let categoryId = wb.categoryId || correctInfo?.categoryId;
|
|
527
|
+
if (isEx && apiContent.exampleSelection?.wordId === wb.id) {
|
|
528
|
+
path = apiContent.exampleSelection.path;
|
|
529
|
+
if (apiContent.exampleSelection.categoryId) {
|
|
530
|
+
categoryId = apiContent.exampleSelection.categoryId;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
id: wb.id || `w_${Math.random().toString(36).substring(2, 9)}`,
|
|
535
|
+
word: wb.word || '',
|
|
536
|
+
isExample: isEx,
|
|
537
|
+
...(categoryId ? { categoryId } : {}),
|
|
538
|
+
path: path || [],
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
content: {
|
|
543
|
+
type,
|
|
544
|
+
title,
|
|
545
|
+
rows,
|
|
546
|
+
cols,
|
|
547
|
+
gridSize: { rows, cols },
|
|
548
|
+
grid,
|
|
549
|
+
categories,
|
|
550
|
+
wordBank,
|
|
551
|
+
words,
|
|
552
|
+
...(apiContent.exampleSelection ? { exampleSelection: apiContent.exampleSelection } : {}),
|
|
553
|
+
},
|
|
554
|
+
answer: {
|
|
555
|
+
type,
|
|
556
|
+
title,
|
|
557
|
+
rows,
|
|
558
|
+
cols,
|
|
559
|
+
grid,
|
|
560
|
+
categories,
|
|
561
|
+
words,
|
|
562
|
+
caseSensitive: Boolean(apiAnswer.caseSensitive),
|
|
563
|
+
explanation: apiQuestion.explanation || '',
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
504
567
|
export function transformApiQuestionToFrontend(apiQuestion) {
|
|
505
568
|
const questionType = String(apiQuestion.questionType ?? apiQuestion.question_type ?? apiQuestion.type ?? '');
|
|
506
569
|
const handlerResult = (handlers[questionType] ?? defaultReverse)(apiQuestion);
|
|
@@ -42,6 +42,7 @@ import { transformSpeakingCueCard } from '../../../components/questions/types/sp
|
|
|
42
42
|
import { transformGNSpeakingInterview } from '../../../components/questions/types/gn-speaking-interview/transform';
|
|
43
43
|
import { transformWordFillStructuredForm } from '../../../components/questions/types/word-fill-structured-form/transform';
|
|
44
44
|
import { transformCrosswordPuzzle } from '../../../components/questions/types/crossword-puzzle/transform';
|
|
45
|
+
import { transformFindWordsInMatrix } from '../../../components/questions/types/find-words-in-matrix/transform';
|
|
45
46
|
// ─── Handler map ────────────────────────────────────────────────────────────
|
|
46
47
|
const questionTransformHandlers = {
|
|
47
48
|
// Look picture
|
|
@@ -90,6 +91,7 @@ const questionTransformHandlers = {
|
|
|
90
91
|
GN_SPEAKING_INTERVIEW: transformGNSpeakingInterview,
|
|
91
92
|
WORD_FILL_STRUCTURED_FORM: transformWordFillStructuredForm,
|
|
92
93
|
CROSSWORD_PUZZLE: transformCrosswordPuzzle,
|
|
94
|
+
FIND_WORDS_IN_MATRIX: transformFindWordsInMatrix,
|
|
93
95
|
};
|
|
94
96
|
/**
|
|
95
97
|
* Transform frontend question format to API format.
|
|
@@ -29,7 +29,7 @@ export declare enum StudentSelectionType {
|
|
|
29
29
|
}
|
|
30
30
|
export type ResultStatus = 'PENDING' | 'PARTIAL_GRADED' | 'GRADED';
|
|
31
31
|
export type StudentStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'SUBMITTED';
|
|
32
|
-
export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE';
|
|
32
|
+
export type QuestionType = 'FILL_IN_BLANK' | 'TRUE_FALSE' | 'MATCHING' | 'ORDERING' | 'LISTENING' | 'COLORING' | 'ESSAY' | 'LISTEN_AND_FILL_IN_THE_BLANK_WITH_IMAGE' | 'LISTEN_AND_TICK_ANSWER' | 'LISTEN_AND_FILL_NAME_NUMBER' | 'LISTENING_FILL_BLANK' | 'LISTENING_COLOR' | 'TICK_TRUE_OR_FALSE' | 'TICK_YES_OR_NO' | 'READING_TICK_CROSS' | 'ARRANGE_LETTERS_INTO_WORDS' | 'READ_PASSAGE_AND_COMPLETE_STORY' | 'CLOZE_TEST_WITH_TICK_BOX' | 'READ_AND_CHOOSE_APPROPRIATE_WORD' | 'WRITE_SHORT_PARAGRAPH' | 'LABEL_THE_PICTURE' | 'READING_LETTER_ARRANGE' | 'READING_PASSAGE_FILL' | 'FILL_BLANK' | 'FILL_MISSING_WORDS_IN_GRID' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'LABEL_THE_PICTURE' | 'READ_AND_COLOR_OBJECTS' | 'IMAGE_OBJECT_MATCHING' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'SPONTANEOUS_QA_GROUP' | 'WORD_ORDER_AND_MATCH_GROUP' | 'WRITE_A_SHORT_LETTER' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_CORRECT_ADJECTIVE' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX';
|
|
33
33
|
export type Level = 'Pre-A1' | 'A1' | 'A2' | 'B1' | 'B2' | 'C1' | 'C2';
|
|
34
34
|
export type Difficulty = 'EASY' | 'MEDIUM' | 'HARD';
|
|
35
35
|
export type Skill = 'LISTENING' | 'READING' | 'WRITING' | 'SPEAKING';
|