@tinyweb_dev/oe-exam-sdk 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/components/CueCardEditor.d.ts +1 -7
  2. package/dist/components/CueCardEditor.js +1 -58
  3. package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
  4. package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.d.ts +4 -0
  5. package/dist/components/exams/take/components/question-renderers/FindWordsInMatrixRenderer.js +389 -0
  6. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  7. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  8. package/dist/components/exams/take/types.d.ts +50 -0
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +41 -1
  10. package/dist/components/exams/take/utils/question-transformers.js +24 -0
  11. package/dist/components/questions/_shared/config/question-types.config.js +6 -0
  12. package/dist/components/questions/_shared/types/find-words-in-matrix.type.d.ts +18 -0
  13. package/dist/components/questions/_shared/types/find-words-in-matrix.type.js +1 -0
  14. package/dist/components/questions/creator/question-type-registry.js +2 -0
  15. package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
  16. package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
  17. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +3 -3
  18. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +9 -0
  19. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +69 -0
  20. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.d.ts +8 -0
  21. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +302 -0
  22. package/dist/components/questions/types/find-words-in-matrix/index.d.ts +4 -0
  23. package/dist/components/questions/types/find-words-in-matrix/index.js +4 -0
  24. package/dist/components/questions/types/find-words-in-matrix/register.d.ts +2 -0
  25. package/dist/components/questions/types/find-words-in-matrix/register.js +29 -0
  26. package/dist/components/questions/types/find-words-in-matrix/transform.d.ts +2 -0
  27. package/dist/components/questions/types/find-words-in-matrix/transform.js +89 -0
  28. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -1
  29. package/dist/components/shared/CueCardEditor.d.ts +7 -0
  30. package/dist/components/shared/CueCardEditor.js +44 -0
  31. package/dist/shared/constants/question-skills.js +2 -0
  32. package/dist/shared/lib/utils/question-reverse-transform.js +63 -0
  33. package/dist/shared/lib/utils/question-transform.js +2 -0
  34. package/dist/shared/types/common.types.d.ts +1 -1
  35. package/dist/shared/types/questions/find-words-in-matrix.d.ts +71 -0
  36. package/dist/shared/types/questions/find-words-in-matrix.js +8 -0
  37. package/dist/shared/types/questions/index.d.ts +1 -0
  38. package/dist/shared/types/questions/index.js +2 -0
  39. package/package.json +1 -1
@@ -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
+ };
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
4
4
  import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
5
5
  import { Input } from '../../../../components/ui/input';
6
6
  import { Label } from '../../../../components/ui/label';
7
- import { CueCardEditor } from '../../../../components/CueCardEditor';
7
+ import { CueCardEditor } from '../../../../components/shared/CueCardEditor';
8
8
  import { CreatorGuide } from '../../components/CreatorGuide';
9
9
  import { CreditCard } from 'lucide-react';
10
10
  export function SpeakingCueCardCreator({ initialData, onChange, onUnsavedChangesChange, externalErrors, validationRef, }) {
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export interface CueCardEditorProps {
3
+ value?: string;
4
+ onChange: (html: string) => void;
5
+ label?: React.ReactNode;
6
+ }
7
+ export declare function CueCardEditor({ value, onChange, label, }: CueCardEditorProps): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { useEditor, EditorContent } from '@tiptap/react';
5
+ import StarterKit from '@tiptap/starter-kit';
6
+ const DEFAULT_CONTENT = `
7
+ <p><strong>Describe a topic you know well.</strong></p>
8
+ <p>You should say:</p>
9
+ <ul>
10
+ <li>What it is</li>
11
+ <li>When you first learned about it</li>
12
+ <li>Why you find it interesting</li>
13
+ </ul>
14
+ <p><em>and explain how it has impacted your life.</em></p>
15
+ `;
16
+ export function CueCardEditor({ value, onChange, label, }) {
17
+ const [mounted, setMounted] = useState(false);
18
+ const editor = useEditor({
19
+ immediatelyRender: false,
20
+ extensions: [StarterKit],
21
+ content: value || DEFAULT_CONTENT,
22
+ onUpdate: ({ editor }) => {
23
+ onChange(editor.getHTML());
24
+ },
25
+ editorProps: {
26
+ attributes: {
27
+ class: 'tiptap-editor focus:outline-none min-h-[250px] p-4 bg-white border border-gray-200 rounded-b-md',
28
+ },
29
+ },
30
+ });
31
+ useEffect(() => {
32
+ setMounted(true);
33
+ }, []);
34
+ // Sync value if changed from outside (e.g., initial load)
35
+ useEffect(() => {
36
+ if (editor && value !== undefined && value !== editor.getHTML()) {
37
+ // Small timeout to prevent update loops
38
+ setTimeout(() => editor.commands.setContent(value), 0);
39
+ }
40
+ }, [value, editor]);
41
+ if (!mounted)
42
+ return null;
43
+ return (_jsxs("div", { className: "flex flex-col gap-1.5 w-full", children: [label && (_jsx("label", { className: "text-sm font-medium leading-none text-gray-700", children: label })), _jsxs("div", { className: "rounded-md border border-gray-200 shadow-sm overflow-hidden focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500", children: [editor && (_jsxs("div", { className: "flex items-center gap-1 p-2 bg-gray-50 border-b border-gray-200 flex-wrap", children: [_jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleBold().run(), className: `p-1.5 rounded hover:bg-gray-200 text-xs font-semibold text-gray-700 ${editor.isActive('bold') ? 'bg-gray-200' : ''}`, title: "Bold", children: "B" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleItalic().run(), className: `p-1.5 rounded hover:bg-gray-200 text-xs italic text-gray-700 ${editor.isActive('italic') ? 'bg-gray-200' : ''}`, title: "Italic", children: "I" }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), className: `p-1.5 rounded hover:bg-gray-200 text-xs font-semibold text-gray-700 ${editor.isActive('heading', { level: 3 }) ? 'bg-gray-200' : ''}`, title: "Heading", children: "H3" }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleBulletList().run(), className: `p-1.5 rounded hover:bg-gray-200 text-xs text-gray-700 ${editor.isActive('bulletList') ? 'bg-gray-200' : ''}`, title: "Bullet List", children: "\u2022 List" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleOrderedList().run(), className: `p-1.5 rounded hover:bg-gray-200 text-xs text-gray-700 ${editor.isActive('orderedList') ? 'bg-gray-200' : ''}`, title: "Numbered List", children: "1. List" }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().clearNodes().unsetAllMarks().run(), className: "p-1.5 rounded hover:bg-gray-200 text-xs text-gray-500", title: "Clear Formatting", children: "Clear" })] })), _jsx(EditorContent, { editor: editor })] })] }));
44
+ }
@@ -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';
@@ -0,0 +1,71 @@
1
+ /**
2
+ * FIND_WORDS_IN_MATRIX Question Type Interfaces (SDK)
3
+ *
4
+ * Supported types:
5
+ * - 'WORD_BANK': Standard word search with given word bank (default).
6
+ * - 'CATEGORIZE': Word search where words are categorized under headings (e.g. Plants vs Animals).
7
+ */
8
+ import { QuestionBankSettings } from './choose-the-correct-answer';
9
+ export type FindWordsInMatrixType = 'WORD_BANK' | 'CATEGORIZE';
10
+ export interface MatrixCellCoord {
11
+ row: number;
12
+ col: number;
13
+ }
14
+ export interface CategoryHeadingItem {
15
+ id: string;
16
+ name: string;
17
+ color?: string;
18
+ }
19
+ export interface WordBankItem {
20
+ id: string;
21
+ word: string;
22
+ isExample?: boolean;
23
+ categoryId?: string;
24
+ }
25
+ export interface ExampleSelection {
26
+ wordId: string;
27
+ categoryId?: string;
28
+ path: MatrixCellCoord[];
29
+ }
30
+ export interface FindWordsInMatrixContent {
31
+ type?: FindWordsInMatrixType;
32
+ title?: string;
33
+ gridSize: {
34
+ rows: number;
35
+ cols: number;
36
+ };
37
+ grid: string[][];
38
+ categories?: CategoryHeadingItem[];
39
+ wordBank: WordBankItem[];
40
+ exampleSelection?: ExampleSelection;
41
+ }
42
+ export interface CorrectWordItem {
43
+ wordId: string;
44
+ word?: string;
45
+ categoryId?: string;
46
+ path: MatrixCellCoord[];
47
+ }
48
+ export interface FindWordsInMatrixCorrectAnswer {
49
+ words: CorrectWordItem[];
50
+ caseSensitive?: boolean;
51
+ }
52
+ export interface StudentWordSelectionItem {
53
+ wordId?: string;
54
+ word?: string;
55
+ categoryId?: string;
56
+ path: MatrixCellCoord[];
57
+ }
58
+ export interface FindWordsInMatrixAPIPayload {
59
+ id?: string;
60
+ questionType: 'FIND_WORDS_IN_MATRIX';
61
+ content: FindWordsInMatrixContent;
62
+ correctAnswer: FindWordsInMatrixCorrectAnswer;
63
+ points: number;
64
+ explanation?: string;
65
+ questionNumber: number;
66
+ partId?: string;
67
+ partNo?: number;
68
+ addToQuestionBank?: boolean;
69
+ questionBank?: QuestionBankSettings;
70
+ }
71
+ export type FindWordsInMatrixLocalStorage = Omit<FindWordsInMatrixAPIPayload, 'addToQuestionBank' | 'questionBank'>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * FIND_WORDS_IN_MATRIX Question Type Interfaces (SDK)
3
+ *
4
+ * Supported types:
5
+ * - 'WORD_BANK': Standard word search with given word bank (default).
6
+ * - 'CATEGORIZE': Word search where words are categorized under headings (e.g. Plants vs Animals).
7
+ */
8
+ export {};
@@ -31,3 +31,4 @@ export * from './speaking-describe-image';
31
31
  export * from './spontaneous-qa';
32
32
  export * from './word-order-and-match-group';
33
33
  export * from './crossword-puzzle';
34
+ export * from './find-words-in-matrix';
@@ -48,3 +48,5 @@ export * from './spontaneous-qa';
48
48
  export * from './word-order-and-match-group';
49
49
  // CROSSWORD_PUZZLE
50
50
  export * from './crossword-puzzle';
51
+ // FIND_WORDS_IN_MATRIX
52
+ export * from './find-words-in-matrix';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",