@tinyweb_dev/oe-exam-sdk 0.2.1 → 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/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 +3 -3
- 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
|
@@ -44,6 +44,7 @@ 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
46
|
import { crosswordPuzzleRegistration } from '../types/crossword-puzzle/register';
|
|
47
|
+
import { findWordsInMatrixRegistration } from '../types/find-words-in-matrix/register';
|
|
47
48
|
// ─── Registry map ───────────────────────────────────────────────────────────
|
|
48
49
|
export const questionTypeRegistry = {
|
|
49
50
|
CHOOSE_THE_CORRECT_ANSWER: chooseTheCorrectAnswerRegistration,
|
|
@@ -91,4 +92,5 @@ export const questionTypeRegistry = {
|
|
|
91
92
|
SPEAKING_CUE_CARD: speakingCueCardRegistration,
|
|
92
93
|
WORD_FILL_STRUCTURED_FORM: wordFillStructuredFormRegistration,
|
|
93
94
|
CROSSWORD_PUZZLE: crosswordPuzzleRegistration,
|
|
95
|
+
FIND_WORDS_IN_MATRIX: findWordsInMatrixRegistration,
|
|
94
96
|
};
|
|
@@ -15,7 +15,8 @@ export declare enum QuestionBankEditorType {
|
|
|
15
15
|
WRITE_CORRECT_VERB_FORM = "WRITE_CORRECT_VERB_FORM",
|
|
16
16
|
FILL_IN_BLANK = "FILL_IN_BLANK",
|
|
17
17
|
CHOOSE_CORRECT_ADJECTIVE = "CHOOSE_CORRECT_ADJECTIVE",
|
|
18
|
-
CROSSWORD_PUZZLE = "CROSSWORD_PUZZLE"
|
|
18
|
+
CROSSWORD_PUZZLE = "CROSSWORD_PUZZLE",
|
|
19
|
+
FIND_WORDS_IN_MATRIX = "FIND_WORDS_IN_MATRIX"
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Renders the appropriate Creator for a question-bank item.
|
|
@@ -4,6 +4,7 @@ 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
6
|
import { CrosswordPuzzleCreator } from '../types/crossword-puzzle/CrosswordPuzzleCreator';
|
|
7
|
+
import { FindWordsInMatrixCreator } from '../types/find-words-in-matrix/FindWordsInMatrixCreator';
|
|
7
8
|
/** Marketing question types supported by the question-bank editor. */
|
|
8
9
|
export var QuestionBankEditorType;
|
|
9
10
|
(function (QuestionBankEditorType) {
|
|
@@ -21,6 +22,7 @@ export var QuestionBankEditorType;
|
|
|
21
22
|
QuestionBankEditorType["FILL_IN_BLANK"] = "FILL_IN_BLANK";
|
|
22
23
|
QuestionBankEditorType["CHOOSE_CORRECT_ADJECTIVE"] = "CHOOSE_CORRECT_ADJECTIVE";
|
|
23
24
|
QuestionBankEditorType["CROSSWORD_PUZZLE"] = "CROSSWORD_PUZZLE";
|
|
25
|
+
QuestionBankEditorType["FIND_WORDS_IN_MATRIX"] = "FIND_WORDS_IN_MATRIX";
|
|
24
26
|
})(QuestionBankEditorType || (QuestionBankEditorType = {}));
|
|
25
27
|
/**
|
|
26
28
|
* Renders the appropriate Creator for a question-bank item.
|
|
@@ -85,6 +87,20 @@ export function QuestionEditorRenderer({ questionType, content, correctAnswer, e
|
|
|
85
87
|
points: data.points ?? points,
|
|
86
88
|
});
|
|
87
89
|
} }));
|
|
90
|
+
case QuestionBankEditorType.FIND_WORDS_IN_MATRIX:
|
|
91
|
+
return (_jsx(FindWordsInMatrixCreator, { initialData: {
|
|
92
|
+
content,
|
|
93
|
+
answer: correctAnswer,
|
|
94
|
+
explanation,
|
|
95
|
+
points,
|
|
96
|
+
}, onChange: (data) => {
|
|
97
|
+
onChange?.({
|
|
98
|
+
content: data.content,
|
|
99
|
+
answer: data.answer,
|
|
100
|
+
explanation: data.explanation || (explanation ?? undefined),
|
|
101
|
+
points: data.points ?? points,
|
|
102
|
+
});
|
|
103
|
+
} }));
|
|
88
104
|
default:
|
|
89
105
|
return (_jsx(UnsupportedEditorFallback, { questionType: questionType, content: content, correctAnswer: correctAnswer, explanation: explanation }));
|
|
90
106
|
}
|
|
@@ -4,8 +4,8 @@ import { useMemo } from 'react';
|
|
|
4
4
|
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, }) {
|
|
5
5
|
const rows = content?.gridSize?.rows || 12;
|
|
6
6
|
const cols = content?.gridSize?.cols || 12;
|
|
7
|
-
const acrossClues = content?.clues?.across || [];
|
|
8
|
-
const downClues = content?.clues?.down || [];
|
|
7
|
+
const acrossClues = useMemo(() => content?.clues?.across || [], [content?.clues?.across]);
|
|
8
|
+
const downClues = useMemo(() => content?.clues?.down || [], [content?.clues?.down]);
|
|
9
9
|
// Build grid cell mapping
|
|
10
10
|
const { cellMap, clueNumberMap } = useMemo(() => {
|
|
11
11
|
const cMap = new Map();
|
|
@@ -69,7 +69,7 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
69
69
|
else if (userAnswers?.[key]) {
|
|
70
70
|
previewLetter = userAnswers[key];
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
|
|
73
73
|
let textColor = isExample
|
|
74
74
|
? 'text-orange-600 font-bold italic'
|
|
75
75
|
: isMarked
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { FindWordsInMatrixContent, FindWordsInMatrixCorrectAnswer } from '../../../../shared/types/questions/find-words-in-matrix';
|
|
3
|
+
interface FindWordsInMatrixClientProps {
|
|
4
|
+
content: FindWordsInMatrixContent;
|
|
5
|
+
correctAnswer?: FindWordsInMatrixCorrectAnswer;
|
|
6
|
+
showSolution?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function FindWordsInMatrixClient({ content, correctAnswer, showSolution, }: FindWordsInMatrixClientProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Card } from '../../../../components/ui/card';
|
|
4
|
+
import { Badge } from '../../../../components/ui/badge';
|
|
5
|
+
// 10 Distinct pastel highlighter colors for word selections
|
|
6
|
+
const WORD_COLORS = [
|
|
7
|
+
{ bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white', stroke: '#f59e0b' },
|
|
8
|
+
{ bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white', stroke: '#10b981' },
|
|
9
|
+
{ bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white', stroke: '#0ea5e9' },
|
|
10
|
+
{ bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white', stroke: '#a855f7' },
|
|
11
|
+
{ bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white', stroke: '#f43f5e' },
|
|
12
|
+
{ bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white', stroke: '#6366f1' },
|
|
13
|
+
{ bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white', stroke: '#14b8a6' },
|
|
14
|
+
{ bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white', stroke: '#eab308' },
|
|
15
|
+
{ bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white', stroke: '#ec4899' },
|
|
16
|
+
{ bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white', stroke: '#06b6d4' },
|
|
17
|
+
];
|
|
18
|
+
export function FindWordsInMatrixClient({ content, correctAnswer, showSolution = false, }) {
|
|
19
|
+
const rows = content.gridSize?.rows || content.grid?.length || 14;
|
|
20
|
+
const cols = content.gridSize?.cols || content.grid?.[0]?.length || 14;
|
|
21
|
+
const grid = content.grid || [];
|
|
22
|
+
const wordBank = content.wordBank || [];
|
|
23
|
+
const isCategorize = content.type === 'CATEGORIZE' && Array.isArray(content.categories) && content.categories.length > 0;
|
|
24
|
+
const categories = content.categories || [];
|
|
25
|
+
// Build cell highlight map
|
|
26
|
+
const cellColorMap = {};
|
|
27
|
+
// Example selection
|
|
28
|
+
if (content.exampleSelection?.path) {
|
|
29
|
+
for (const c of content.exampleSelection.path) {
|
|
30
|
+
cellColorMap[`${c.row}-${c.col}`] = { colorIdx: 0, word: 'Example' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Solution paths if visible
|
|
34
|
+
if (showSolution && correctAnswer?.words) {
|
|
35
|
+
correctAnswer.words.forEach((item, idx) => {
|
|
36
|
+
const colorIdx = (idx + 1) % WORD_COLORS.length;
|
|
37
|
+
item.path?.forEach((c) => {
|
|
38
|
+
cellColorMap[`${c.row}-${c.col}`] = { colorIdx, word: item.word || item.wordId };
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs("div", { className: "space-y-6", children: [content.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 dark:text-gray-100", children: content.title })), isCategorize ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4", children: categories.map((cat) => {
|
|
43
|
+
const wordsInCat = wordBank.filter((w) => w.categoryId === cat.id);
|
|
44
|
+
return (_jsxs(Card, { className: "p-3.5 bg-gray-50/70 dark:bg-gray-800/40 border", children: [_jsxs("div", { className: "font-bold text-sm text-gray-800 dark:text-gray-200 border-b pb-1.5 mb-2.5 flex items-center justify-between", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((w, idx) => {
|
|
45
|
+
const isExample = Boolean(w.isExample);
|
|
46
|
+
return (_jsxs(Badge, { variant: "outline", className: `px-2.5 py-1 text-xs font-medium ${isExample
|
|
47
|
+
? 'line-through bg-amber-50 text-amber-800 border-amber-300'
|
|
48
|
+
: 'bg-white dark:bg-gray-800'}`, children: [w.word, isExample && _jsx("span", { className: "ml-1 text-amber-600 font-bold", children: "(Ex)" })] }, w.id || idx));
|
|
49
|
+
}) })] }, cat.id));
|
|
50
|
+
}) })) : (_jsxs(Card, { className: "p-4 bg-gray-50 dark:bg-gray-800/50 border-dashed border-2", children: [_jsx("div", { className: "text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3 flex items-center gap-2", children: _jsxs("span", { children: ["Word Bank (", wordBank.length, " words)"] }) }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((w, idx) => {
|
|
51
|
+
const isExample = Boolean(w.isExample);
|
|
52
|
+
return (_jsxs(Badge, { variant: "outline", className: `px-3 py-1.5 text-sm font-medium transition-all ${isExample
|
|
53
|
+
? 'line-through bg-amber-50 text-amber-800 border-amber-300'
|
|
54
|
+
: 'bg-white dark:bg-gray-800 hover:shadow-sm'}`, children: [w.word, isExample && (_jsx("span", { className: "ml-1.5 text-xs text-amber-600 font-bold no-underline", children: "(Example)" }))] }, w.id || idx));
|
|
55
|
+
}) })] })), _jsx("div", { className: "overflow-x-auto pb-2 flex justify-center", children: _jsx("div", { className: "inline-grid gap-1 p-3 bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800", style: {
|
|
56
|
+
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
57
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
58
|
+
const r = rIdx + 1;
|
|
59
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
60
|
+
const c = cIdx + 1;
|
|
61
|
+
const char = grid[rIdx]?.[cIdx] || '';
|
|
62
|
+
const highlight = cellColorMap[`${r}-${c}`];
|
|
63
|
+
const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
|
|
64
|
+
return (_jsx("div", { className: `h-9 w-9 sm:h-11 sm:w-11 flex items-center justify-center font-bold text-sm sm:text-base select-none rounded-md transition-colors border ${colorConfig
|
|
65
|
+
? `${colorConfig.bg} font-extrabold shadow-sm scale-95`
|
|
66
|
+
: 'border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/30 text-gray-700 dark:text-gray-300'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
|
|
67
|
+
});
|
|
68
|
+
}) }) })] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface FindWordsInMatrixCreatorProps {
|
|
3
|
+
initialData?: any;
|
|
4
|
+
onChange?: (data: any) => void;
|
|
5
|
+
onUnsavedChangesChange?: (hasChanges: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChangesChange, }: FindWordsInMatrixCreatorProps): React.JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -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
|
+
};
|