@tinyweb_dev/oe-exam-sdk 0.2.5 → 0.2.6

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 (75) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
  3. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
  4. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
  6. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
  7. package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
  8. package/dist/components/exams/take/components/question-renderers/index.js +2 -0
  9. package/dist/components/exams/take/utils/answer-transformers.js +16 -0
  10. package/dist/components/exams/take/utils/question-transformers.d.ts +45 -1
  11. package/dist/components/exams/take/utils/question-transformers.js +90 -0
  12. package/dist/components/questions/_shared/config/question-types.config.js +12 -0
  13. package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
  14. package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
  15. package/dist/components/questions/_shared/types/index.d.ts +2 -0
  16. package/dist/components/questions/_shared/types/index.js +2 -0
  17. package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
  18. package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
  19. package/dist/components/questions/components/QuestionSearchDropdown.js +4 -0
  20. package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
  21. package/dist/components/questions/creator/question-type-registry.js +4 -0
  22. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
  23. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
  24. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
  25. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
  26. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
  27. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
  28. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
  29. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
  30. package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
  31. package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
  32. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
  33. package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
  34. package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
  35. package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
  36. package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
  37. package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
  38. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
  39. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
  40. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
  41. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
  42. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
  43. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
  44. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
  45. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
  46. package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
  47. package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
  48. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
  49. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
  50. package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
  51. package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
  52. package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
  53. package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
  54. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
  55. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
  56. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
  57. package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
  58. package/dist/components/questions/viewer/QuestionViewer.js +6 -0
  59. package/dist/components/questions/viewer/index.d.ts +2 -0
  60. package/dist/components/questions/viewer/index.js +2 -0
  61. package/dist/components/results/renderers/review-question-body-movers.js +37 -2
  62. package/dist/components/results/review-data.js +37 -0
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.js +1 -1
  65. package/dist/shared/constants/question-skills.js +4 -0
  66. package/dist/shared/lib/utils/question-reverse-transform.js +86 -0
  67. package/dist/shared/lib/utils/question-transform.js +4 -0
  68. package/dist/shared/types/common.types.d.ts +1 -1
  69. package/dist/shared/types/questions/fill-in-blank-group.d.ts +45 -0
  70. package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
  71. package/dist/shared/types/questions/index.d.ts +2 -0
  72. package/dist/shared/types/questions/index.js +4 -0
  73. package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
  74. package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
  75. package/package.json +1 -1
@@ -631,6 +631,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
631
631
  map.set(`${question.id}-item-${index}`, question.id);
632
632
  });
633
633
  }
634
+ if (part.questionType === 'FILL_IN_BLANK_GROUP' &&
635
+ Array.isArray(content?.items)) {
636
+ content.items.forEach((_, index) => {
637
+ map.set(`${question.id}-item-${index}`, question.id);
638
+ });
639
+ }
634
640
  });
635
641
  });
636
642
  return map;
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * 2. Transforms data to renderer-specific props
9
9
  * 3. Renders the correct component for each question type
10
10
  */
11
- import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
11
+ import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, } from './question-renderers';
12
12
  import { SpeakingQuestionRenderer } from './speaking/renderers';
13
- import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
13
+ import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, } from '../utils/question-transformers';
14
14
  import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
15
15
  import { normalizeColoringAssignments } from '../utils/coloring-assignments';
16
16
  export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
@@ -161,6 +161,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
161
161
  const data = transformTrueFalseCorrectGroup(questions);
162
162
  return (_jsx(MoversTrueFalseCorrectGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
163
163
  }
164
+ case 'FILL_IN_BLANK_GROUP': {
165
+ const data = transformFillInBlankGroup(questions);
166
+ return (_jsx(MoversFillInBlankGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: data.questions.filter((question) => !question.isExample).length, instruction: part.instructions || data.instruction, audioUrl: data.audioUrl || part.audioUrl, imageUrl: data.imageUrl, showHints: data.showHints, hints: data.hints, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
167
+ }
164
168
  case 'CROSS_OUT_WORD_GROUP': {
165
169
  const data = transformCrossOutWordGroup(questions);
166
170
  return (_jsx(MoversCrossOutWordGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
@@ -169,6 +173,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
169
173
  const data = transformWordOrderAndMatchGroup(questions);
170
174
  return (_jsx(MoversWordOrderAndMatchGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, matchOptions: data.matchOptions, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
171
175
  }
176
+ case 'MATCHING_WITH_LINES_GROUP': {
177
+ const data = transformMatchingWithLinesGroup(questions);
178
+ return (_jsx(MoversMatchingWithLinesGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
179
+ }
172
180
  case 'TRUE_FALSE': {
173
181
  const data = transformTrueFalse(questions);
174
182
  return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
@@ -0,0 +1,22 @@
1
+ /**
2
+ * FILL_IN_BLANK_GROUP Renderer
3
+ * Shared audio/image on top, then each item: picture + fill blanks.
4
+ */
5
+ import type { FillInBlankGroupQuestion, FillInBlankGroupItemAnswer, FillInBlankGroupStudentItemAnswer } from '../../utils/question-transformers';
6
+ interface MoversFillInBlankGroupRendererProps {
7
+ partNumber: number;
8
+ partName?: string;
9
+ questionCount: number;
10
+ instruction: string;
11
+ audioUrl?: string;
12
+ imageUrl?: string;
13
+ showHints?: boolean;
14
+ hints?: string[];
15
+ questions: FillInBlankGroupQuestion[];
16
+ answers: Record<string, FillInBlankGroupStudentItemAnswer | undefined>;
17
+ onAnswerChange: (questionId: string, value: FillInBlankGroupStudentItemAnswer) => void;
18
+ isReviewMode?: boolean;
19
+ correctAnswerMap?: Record<string, FillInBlankGroupItemAnswer>;
20
+ }
21
+ export declare function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints, hints, questions, answers, onAnswerChange, isReviewMode, correctAnswerMap, }: MoversFillInBlankGroupRendererProps): import("react").JSX.Element;
22
+ export {};
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * FILL_IN_BLANK_GROUP Renderer
4
+ * Shared audio/image on top, then each item: picture + fill blanks.
5
+ */
6
+ import { Fragment } from 'react';
7
+ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
8
+ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
9
+ import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
10
+ import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
11
+ import { extractPlaceholderIndices, isBlankMatch, padStudentAnswers, splitStem, } from '../../../../questions/types/fill-in-blank-group/blank-utils';
12
+ function emptyAnswer(blankCount) {
13
+ return { answers: Array.from({ length: blankCount }, () => '') };
14
+ }
15
+ function ItemCard({ question, answer, onAnswerChange, isReviewMode, correctAnswer, }) {
16
+ const isDisabled = isReviewMode || question.isExample;
17
+ const current = question.isExample
18
+ ? { answers: question.exampleAnswers || emptyAnswer(question.blankCount).answers }
19
+ : { answers: padStudentAnswers(answer, question.blankCount) };
20
+ const stem = splitStem(question.question || '{0}');
21
+ const expected = correctAnswer?.answers || [];
22
+ const blankIndices = extractPlaceholderIndices(question.question || '{0}');
23
+ const allCorrect = isReviewMode
24
+ && !question.isExample
25
+ && blankIndices.length > 0
26
+ && blankIndices.every((index) => isBlankMatch(current.answers[index], expected[index]));
27
+ const anyFilled = current.answers.some((value) => value.trim().length > 0);
28
+ const isWrong = isReviewMode && !question.isExample && anyFilled && !allCorrect;
29
+ const setBlank = (blankIndex, value) => {
30
+ const next = [...current.answers];
31
+ next[blankIndex] = value;
32
+ onAnswerChange(question.id, { answers: next });
33
+ };
34
+ return (_jsxs("div", { id: `question-${question.id}`, className: `rounded-2xl border p-3 sm:p-4 transition-all ${question.isExample
35
+ ? 'border-amber-200 bg-amber-50/60'
36
+ : allCorrect
37
+ ? 'border-emerald-300 bg-emerald-50/30'
38
+ : isWrong
39
+ ? 'border-rose-300 bg-rose-50/30'
40
+ : 'border-slate-200 bg-white'}`, children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx("span", { className: `flex h-7 w-7 items-center justify-center rounded-full text-xs font-black text-white ${question.isExample ? 'bg-amber-500' : allCorrect ? 'bg-emerald-500' : isWrong ? 'bg-rose-500' : 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-extrabold uppercase text-amber-800", children: "Example" }))] }), question.audioUrl && (_jsx("div", { className: "mb-2", children: _jsx(CambridgeYleAudioPlayer, { audioSrc: question.audioUrl, compact: true, dense: true }) })), _jsxs("div", { className: "flex items-center gap-3", children: [question.imageUrl && (_jsx(ResolvedImage, { src: question.imageUrl, alt: `Question ${question.questionNumber}`, className: "h-24 w-24 flex-shrink-0 rounded-lg object-contain" })), _jsx("div", { className: "min-w-0 flex-1 text-[15px] leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
41
+ if (part.type === 'text') {
42
+ const lines = part.value.split('\n');
43
+ return (_jsx(Fragment, { children: lines.map((line, lineIndex) => (_jsxs(Fragment, { children: [lineIndex > 0 ? _jsx("br", {}) : null, line] }, lineIndex))) }, partIndex));
44
+ }
45
+ const student = current.answers[part.index] || '';
46
+ const alts = expected[part.index] || [];
47
+ const blankCorrect = isReviewMode && !question.isExample && isBlankMatch(student, alts);
48
+ const blankWrong = isReviewMode && !question.isExample && student.trim().length > 0 && !blankCorrect;
49
+ return (_jsx("input", { type: "text", value: student, disabled: isDisabled, onChange: (event) => setBlank(part.index, event.target.value), placeholder: question.isExample ? '' : '...', className: `mx-1 inline-block w-[4.5rem] rounded-md border-2 px-1 py-1 text-center text-sm font-semibold outline-none sm:w-20 ${question.isExample
50
+ ? 'border-amber-300 bg-amber-100/70 text-amber-800'
51
+ : blankCorrect
52
+ ? 'border-emerald-400 bg-emerald-50 text-emerald-800'
53
+ : blankWrong
54
+ ? 'border-rose-400 bg-rose-50 text-rose-800'
55
+ : 'border-blue-200 bg-blue-50/40 text-blue-900 focus:border-blue-500 focus:bg-white'}` }, partIndex));
56
+ }) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsxs("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-sm text-emerald-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", expected.map((alts) => alts.join(' / ') || '(trống)').join(' | ')] }))] }));
57
+ }
58
+ export function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints = false, hints = [], questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap, }) {
59
+ return (_jsxs("div", { className: "relative flex h-full flex-col gap-4 overflow-y-auto custom-scrollbar sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 p-3 sm:gap-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), (audioUrl || imageUrl) && (_jsxs("div", { className: "sticky top-0 z-20 flex flex-col gap-3 border-b border-gray-100 bg-white/95 p-3 pb-2 shadow-sm backdrop-blur sm:px-6", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), imageUrl && (_jsx("div", { className: "flex justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Shared illustration", className: "max-h-[180px] rounded-lg object-contain" }) }))] })), showHints && hints.length > 0 && !isReviewMode && (_jsx("div", { className: "flex flex-wrap gap-2 px-3 sm:px-6", children: hints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:px-6 lg:grid-cols-3", children: questions.map((question) => (_jsx(ItemCard, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap?.[question.id] }, question.id))) })] }));
60
+ }
@@ -0,0 +1,13 @@
1
+ import type { MatchingWithLinesGroupPartData } from '../../utils/question-transformers';
2
+ interface MoversMatchingWithLinesGroupRendererProps {
3
+ partNumber: number;
4
+ partName?: string;
5
+ questionCount: number;
6
+ instruction: string;
7
+ data: MatchingWithLinesGroupPartData;
8
+ answers: Record<string, string | undefined>;
9
+ onAnswerChange: (questionId: string, value: string) => void;
10
+ isReviewMode?: boolean;
11
+ }
12
+ export declare function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode, }: MoversMatchingWithLinesGroupRendererProps): import("react").JSX.Element;
13
+ export {};
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
4
+ import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
5
+ import { MatchingWithLinesGroupClient } from '../../../../questions/types/matching-with-lines-group/MatchingWithLinesGroupClient';
6
+ export function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, questionCount, instruction, data, answers, onAnswerChange, isReviewMode = false, }) {
7
+ const userAnswers = data.questions.map((question) => {
8
+ if (question.isExample)
9
+ return question.correctRightId;
10
+ const raw = answers[question.id];
11
+ return typeof raw === 'string' ? raw : '';
12
+ });
13
+ return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(MatchingWithLinesGroupClient, { questionData: {
14
+ title: data.title,
15
+ instruction: data.instruction,
16
+ rightItems: data.rightItems,
17
+ items: data.questions.map((question) => ({
18
+ imageUrl: question.imageUrl,
19
+ correctAnswer: question.correctRightId,
20
+ isExample: question.isExample,
21
+ points: question.points,
22
+ questionNumber: question.questionNumber,
23
+ })),
24
+ explanation: data.explanation,
25
+ }, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: (index, rightId) => {
26
+ const target = data.questions[index];
27
+ if (target && !target.isExample) {
28
+ onAnswerChange(target.id, rightId);
29
+ }
30
+ } }) })] }));
31
+ }
@@ -11,6 +11,7 @@ export { MoversFillWordHintRenderer } from './MoversFillWordHintRenderer';
11
11
  export { MoversGridFillRenderer } from './MoversGridFillRenderer';
12
12
  export { MoversTrueFalseRenderer } from './MoversTrueFalseRenderer';
13
13
  export { MoversTrueFalseCorrectGroupRenderer } from './MoversTrueFalseCorrectGroupRenderer';
14
+ export { MoversFillInBlankGroupRenderer } from './MoversFillInBlankGroupRenderer';
14
15
  export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRenderer';
15
16
  export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
16
17
  export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
@@ -20,6 +21,7 @@ export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswer
20
21
  export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
21
22
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
22
23
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
24
+ export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
23
25
  export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
24
26
  export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
25
27
  export { ArticlesViewer } from './ArticlesViewer';
@@ -15,6 +15,7 @@ export { MoversGridFillRenderer } from './MoversGridFillRenderer';
15
15
  // Starters & Movers renderers
16
16
  export { MoversTrueFalseRenderer } from './MoversTrueFalseRenderer';
17
17
  export { MoversTrueFalseCorrectGroupRenderer } from './MoversTrueFalseCorrectGroupRenderer';
18
+ export { MoversFillInBlankGroupRenderer } from './MoversFillInBlankGroupRenderer';
18
19
  export { MoversCrossOutWordGroupRenderer } from './MoversCrossOutWordGroupRenderer';
19
20
  export { MoversAnswerTheQuestionRenderer } from './MoversAnswerTheQuestionRenderer';
20
21
  export { MoversWriteSentencesRenderer } from './MoversWriteSentencesRenderer';
@@ -24,6 +25,7 @@ export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswer
24
25
  export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
25
26
  export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
26
27
  export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
28
+ export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
27
29
  export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
28
30
  export { FindWordsInMatrixRenderer } from './FindWordsInMatrixRenderer';
29
31
  export { ArticlesViewer } from './ArticlesViewer';
@@ -88,6 +88,8 @@ function findParentQuestion(subQuestionId, parts) {
88
88
  part.questionType === 'CROSS_OUT_WORD_GROUP' ||
89
89
  part.questionType === 'TRUE_FALSE_GROUP' ||
90
90
  part.questionType === 'TRUE_FALSE_CORRECT_GROUP' ||
91
+ part.questionType === 'FILL_IN_BLANK_GROUP' ||
92
+ part.questionType === 'MATCHING_WITH_LINES_GROUP' ||
91
93
  part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
92
94
  part.questionType === 'SPONTANEOUS_QA_GROUP' ||
93
95
  part.questionType === 'ANSWER_THE_QUESTION_GROUP') {
@@ -381,6 +383,20 @@ export function transformAnswersForSubmission(answers, parts) {
381
383
  }
382
384
  return { isTrue: null, correction: '' };
383
385
  }
386
+ if (parentType === 'FILL_IN_BLANK_GROUP') {
387
+ if (raw && typeof raw === 'object' && !Array.isArray(raw) && Array.isArray(raw.answers)) {
388
+ return {
389
+ answers: raw.answers.map((value) => String(value ?? '')),
390
+ };
391
+ }
392
+ if (Array.isArray(raw)) {
393
+ return { answers: raw.map((value) => String(value ?? '')) };
394
+ }
395
+ if (typeof raw === 'string') {
396
+ return { answers: [raw] };
397
+ }
398
+ return { answers: [] };
399
+ }
384
400
  if (parentType === 'WORD_ORDER_AND_MATCH_GROUP') {
385
401
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
386
402
  const obj = raw;
@@ -5,7 +5,7 @@
5
5
  * Each transformer handles specific question type data structure.
6
6
  */
7
7
  import type { FillBlankQuestion, ChooseBestAnswerQuestion, PictureChooseSubQuestion, PictureFillBlankSubQuestion, ReadingPassageQuestion, LabelThePictureLabel, FillWordHintSubQuestion, ColoringHintItem, ColoringRegion } from '../types';
8
- export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
8
+ export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
9
9
  export interface ApiQuestion {
10
10
  id: string;
11
11
  content: any;
@@ -285,6 +285,31 @@ export declare function transformTrueFalseCorrectGroup(questions: ApiQuestion[])
285
285
  questions: TrueFalseCorrectGroupQuestion[];
286
286
  instruction: string;
287
287
  };
288
+ export interface FillInBlankGroupItemAnswer {
289
+ answers: string[][];
290
+ caseSensitive?: boolean;
291
+ }
292
+ export interface FillInBlankGroupStudentItemAnswer {
293
+ answers: string[];
294
+ }
295
+ export interface FillInBlankGroupQuestion {
296
+ id: string;
297
+ questionNumber: number;
298
+ question: string;
299
+ imageUrl?: string;
300
+ audioUrl?: string;
301
+ isExample: boolean;
302
+ blankCount: number;
303
+ exampleAnswers?: string[];
304
+ }
305
+ export declare function transformFillInBlankGroup(questions: ApiQuestion[]): {
306
+ questions: FillInBlankGroupQuestion[];
307
+ instruction: string;
308
+ audioUrl?: string;
309
+ imageUrl?: string;
310
+ showHints?: boolean;
311
+ hints?: string[];
312
+ };
288
313
  export interface WordOrderingQuestion {
289
314
  id: string;
290
315
  questionNumber: number;
@@ -505,3 +530,22 @@ export interface FindWordsInMatrixPartData {
505
530
  explanation?: string;
506
531
  }
507
532
  export declare function transformFindWordsInMatrix(questions: ApiQuestion[]): FindWordsInMatrixPartData;
533
+ export interface MatchingWithLinesGroupQuestionItem {
534
+ id: string;
535
+ questionNumber: number;
536
+ imageUrl: string;
537
+ correctRightId: string;
538
+ isExample: boolean;
539
+ points: number;
540
+ }
541
+ export interface MatchingWithLinesGroupPartData {
542
+ questions: MatchingWithLinesGroupQuestionItem[];
543
+ rightItems: Array<{
544
+ id: string;
545
+ text: string;
546
+ }>;
547
+ title?: string;
548
+ instruction: string;
549
+ explanation?: string;
550
+ }
551
+ export declare function transformMatchingWithLinesGroup(questions: ApiQuestion[]): MatchingWithLinesGroupPartData;
@@ -962,6 +962,63 @@ export function transformTrueFalseCorrectGroup(questions) {
962
962
  || 'Tick (✓) the true and cross (✗) the false sentences. Correct the false sentences.',
963
963
  };
964
964
  }
965
+ function parseFillInBlankGroupAnswer(raw) {
966
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
967
+ return undefined;
968
+ }
969
+ const obj = raw;
970
+ if (!Array.isArray(obj.answers)) {
971
+ return undefined;
972
+ }
973
+ return {
974
+ answers: obj.answers.map((blank) => (Array.isArray(blank)
975
+ ? blank.map((alt) => String(alt ?? '').trim()).filter(Boolean)
976
+ : [String(blank ?? '').trim()].filter(Boolean))),
977
+ caseSensitive: obj.caseSensitive === true,
978
+ };
979
+ }
980
+ function blankCountFromMatches(matches) {
981
+ const indices = matches.map((match) => Number.parseInt(match[1], 10));
982
+ return Math.max(...indices, 0) + 1;
983
+ }
984
+ export function transformFillInBlankGroup(questions) {
985
+ const parent = questions[0];
986
+ const content = parent?.content;
987
+ const items = Array.isArray(content?.items) ? content.items : [];
988
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
989
+ ? parent.correct_answer.answer
990
+ : [];
991
+ const groupAudioUrl = content?.meta?.audioUrl?.trim() || '';
992
+ const transformed = items.map((item, index) => {
993
+ const parsed = parseFillInBlankGroupAnswer(item.correctAnswer ?? parentCorrect[index]);
994
+ const question = item.content?.question || '{0}';
995
+ const blankMatches = [...question.matchAll(/\{(\d+)\}/g)];
996
+ const blankCount = blankMatches.length > 0 ? blankCountFromMatches(blankMatches) : 1;
997
+ const isExample = item.isExample === true;
998
+ return {
999
+ id: `${parent.id}-item-${index}`,
1000
+ questionNumber: item.questionNumber || index + 1,
1001
+ question,
1002
+ imageUrl: item.content?.imageUrl,
1003
+ audioUrl: groupAudioUrl ? undefined : item.content?.audioUrl,
1004
+ isExample,
1005
+ blankCount,
1006
+ exampleAnswers: isExample
1007
+ ? Array.from({ length: blankCount }, (_, slot) => parsed?.answers?.[slot]?.[0] || '')
1008
+ : undefined,
1009
+ };
1010
+ });
1011
+ return {
1012
+ questions: transformed,
1013
+ instruction: content?.meta?.instruction || 'Complete the sentences.',
1014
+ audioUrl: groupAudioUrl || undefined,
1015
+ imageUrl: content?.meta?.imageUrl,
1016
+ showHints: content?.meta?.showHints === true,
1017
+ hints: Array.isArray(content?.meta?.hints)
1018
+ ? content.meta.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
1019
+ : [],
1020
+ };
1021
+ }
965
1022
  export function transformWordOrdering(questions) {
966
1023
  const transformed = questions.map((q) => {
967
1024
  const content = q.content;
@@ -1326,3 +1383,36 @@ export function transformFindWordsInMatrix(questions) {
1326
1383
  explanation: question?.explanation,
1327
1384
  };
1328
1385
  }
1386
+ export function transformMatchingWithLinesGroup(questions) {
1387
+ const parent = questions[0];
1388
+ const content = (parent?.content || {});
1389
+ const items = Array.isArray(content.items) ? content.items : [];
1390
+ const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
1391
+ ? parent.correct_answer.answer
1392
+ : [];
1393
+ const rightItems = Array.isArray(content.rightItems)
1394
+ ? content.rightItems.map((item, index) => ({
1395
+ id: typeof item?.id === 'string' ? item.id : `r${index + 1}`,
1396
+ text: typeof item?.text === 'string' ? item.text : '',
1397
+ }))
1398
+ : [];
1399
+ return {
1400
+ questions: items.map((item, index) => {
1401
+ const isExample = item.isExample === true;
1402
+ const fromItem = typeof item.correctAnswer?.answer === 'string' ? item.correctAnswer.answer : '';
1403
+ const fromParent = typeof parentCorrect[index] === 'string' ? parentCorrect[index] : '';
1404
+ return {
1405
+ id: `${parent?.id || 'q'}-item-${index}`,
1406
+ questionNumber: item.questionNumber || index + 1,
1407
+ imageUrl: item.content?.imageUrl || '',
1408
+ correctRightId: fromItem || fromParent,
1409
+ isExample,
1410
+ points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
1411
+ };
1412
+ }),
1413
+ rightItems,
1414
+ title: content.meta?.title || '',
1415
+ instruction: content.meta?.instruction || 'Look and match.',
1416
+ explanation: parent?.explanation,
1417
+ };
1418
+ }
@@ -177,6 +177,11 @@ export const QUESTION_TYPE_CONFIG = {
177
177
  requiresContent: true,
178
178
  hasDedicatedComponent: true,
179
179
  },
180
+ FILL_IN_BLANK_GROUP: {
181
+ label: 'Điền chỗ trống (nhóm)',
182
+ requiresContent: true,
183
+ hasDedicatedComponent: true,
184
+ },
180
185
  CROSS_OUT_WORD_GROUP: {
181
186
  label: 'Cross out word (group)',
182
187
  requiresContent: true,
@@ -237,6 +242,11 @@ export const QUESTION_TYPE_CONFIG = {
237
242
  requiresContent: true,
238
243
  hasDedicatedComponent: true,
239
244
  },
245
+ MATCHING_WITH_LINES_GROUP: {
246
+ label: 'Nối bằng đường kẻ (nhóm)',
247
+ requiresContent: true,
248
+ hasDedicatedComponent: true,
249
+ },
240
250
  WRITE_SENTENCES: {
241
251
  label: 'Viết câu',
242
252
  requiresContent: true,
@@ -354,6 +364,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
354
364
  'CHOOSE_THE_CORRECT_ANSWER_GROUP',
355
365
  'TRUE_FALSE_GROUP',
356
366
  'TRUE_FALSE_CORRECT_GROUP',
367
+ 'FILL_IN_BLANK_GROUP',
357
368
  'CROSS_OUT_WORD_GROUP',
358
369
  'WRITE_A_SHORT_LETTER',
359
370
  'WRITE_CORRECT_VERB_FORM',
@@ -365,6 +376,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
365
376
  'WORD_FILL_PARAGRAPH',
366
377
  'MATCH_BY_WRITING_ANSWER',
367
378
  'MATCH_WORD_TO_PICTURE',
379
+ 'MATCHING_WITH_LINES_GROUP',
368
380
  'WRITE_SENTENCES',
369
381
  'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP',
370
382
  'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE',
@@ -0,0 +1,50 @@
1
+ import type { MutableRefObject } from 'react';
2
+ export declare const FILL_IN_BLANK_ITEM_TYPE: "FILL_IN_BLANK";
3
+ export declare const FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION = "Complete the sentences.";
4
+ export interface FillInBlankGroupItemAnswer {
5
+ answers: string[][];
6
+ caseSensitive?: boolean;
7
+ }
8
+ export interface FillInBlankGroupStudentItemAnswer {
9
+ answers: string[];
10
+ }
11
+ export interface FillInBlankGroupItemData {
12
+ question: string;
13
+ answers: string[][];
14
+ caseSensitive?: boolean;
15
+ imageUrl?: string;
16
+ audioUrl?: string;
17
+ showHints?: boolean;
18
+ hints?: string[];
19
+ isExample?: boolean;
20
+ points: number;
21
+ questionNumber: number;
22
+ }
23
+ export interface FillInBlankGroupData {
24
+ instruction: string;
25
+ audioUrl?: string;
26
+ imageUrl?: string;
27
+ showHints?: boolean;
28
+ hints?: string[];
29
+ items: FillInBlankGroupItemData[];
30
+ explanation?: string;
31
+ points?: number;
32
+ }
33
+ export interface FillInBlankGroupCreatorProps {
34
+ initialData?: FillInBlankGroupData;
35
+ onSave?: (data: FillInBlankGroupData) => void;
36
+ onCancel?: () => void;
37
+ onChange?: (data: FillInBlankGroupData) => void;
38
+ externalErrors?: string[];
39
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
40
+ validationRef?: MutableRefObject<{
41
+ triggerValidation: () => Promise<boolean>;
42
+ getFormData?: () => FillInBlankGroupData;
43
+ } | null>;
44
+ }
45
+ export interface FillInBlankGroupClientProps {
46
+ questionData: FillInBlankGroupData;
47
+ isReviewMode?: boolean;
48
+ userAnswers?: FillInBlankGroupStudentItemAnswer[];
49
+ onAnswerChange?: (answers: FillInBlankGroupStudentItemAnswer[]) => void;
50
+ }
@@ -0,0 +1,2 @@
1
+ export const FILL_IN_BLANK_ITEM_TYPE = 'FILL_IN_BLANK';
2
+ export const FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION = 'Complete the sentences.';
@@ -12,5 +12,7 @@ export * from './fill-in-blank.type';
12
12
  export * from './read-passage-and-answer-questions.type';
13
13
  export * from './word-fill-structured-form.type';
14
14
  export * from './true-false-correct-group.type';
15
+ export * from './fill-in-blank-group.type';
15
16
  export * from './match-word-to-picture.type';
17
+ export * from './matching-with-lines-group.type';
16
18
  export * from './spontaneous-qa-group.type';
@@ -12,5 +12,7 @@ export * from './fill-in-blank.type';
12
12
  export * from './read-passage-and-answer-questions.type';
13
13
  export * from './word-fill-structured-form.type';
14
14
  export * from './true-false-correct-group.type';
15
+ export * from './fill-in-blank-group.type';
15
16
  export * from './match-word-to-picture.type';
17
+ export * from './matching-with-lines-group.type';
16
18
  export * from './spontaneous-qa-group.type';
@@ -0,0 +1,40 @@
1
+ import type { MutableRefObject } from 'react';
2
+ export declare const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = "Look and match.";
3
+ export interface MatchingWithLinesRightItem {
4
+ id: string;
5
+ text: string;
6
+ }
7
+ export interface MatchingWithLinesGroupItemData {
8
+ imageUrl: string;
9
+ correctAnswer: string;
10
+ isExample?: boolean;
11
+ points: number;
12
+ questionNumber: number;
13
+ }
14
+ export interface MatchingWithLinesGroupData {
15
+ title?: string;
16
+ instruction: string;
17
+ rightItems: MatchingWithLinesRightItem[];
18
+ items: MatchingWithLinesGroupItemData[];
19
+ explanation?: string;
20
+ points?: number;
21
+ }
22
+ export interface MatchingWithLinesGroupCreatorProps {
23
+ initialData?: MatchingWithLinesGroupData;
24
+ onSave?: (data: MatchingWithLinesGroupData) => void;
25
+ onCancel?: () => void;
26
+ onChange?: (data: MatchingWithLinesGroupData) => void;
27
+ externalErrors?: string[];
28
+ onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
29
+ validationRef?: MutableRefObject<{
30
+ triggerValidation: () => Promise<boolean>;
31
+ getFormData?: () => MatchingWithLinesGroupData;
32
+ } | null>;
33
+ }
34
+ export interface MatchingWithLinesGroupClientProps {
35
+ questionData: MatchingWithLinesGroupData;
36
+ isReviewMode?: boolean;
37
+ userAnswers?: string[];
38
+ onAnswerChange?: (index: number, rightId: string) => void;
39
+ onAnswersChange?: (next: string[]) => void;
40
+ }
@@ -0,0 +1 @@
1
+ export const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = 'Look and match.';
@@ -23,11 +23,13 @@ const IMAGE_BASED_TYPES = [
23
23
  'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER',
24
24
  'LOOK_PICTURE_FILL_WORD_HINT',
25
25
  'FILL_IN_BLANK',
26
+ 'FILL_IN_BLANK_GROUP',
26
27
  'LABEL_THE_PICTURE',
27
28
  'READ_AND_COLOR_OBJECTS',
28
29
  'IMAGE_OBJECT_MATCHING',
29
30
  'FILL_IN_BLANK',
30
31
  'MATCH_WORD_TO_PICTURE',
32
+ 'MATCHING_WITH_LINES_GROUP',
31
33
  ];
32
34
  // Passage-based question types
33
35
  const PASSAGE_BASED_TYPES = [
@@ -62,8 +64,10 @@ const getQuestionTypeName = (type) => {
62
64
  'IMAGE_OBJECT_MATCHING': 'Ghép hình ảnh - Đối tượng',
63
65
  'WRITE_CORRECT_VERB_FORM': 'Viết dạng động từ',
64
66
  'FILL_IN_BLANK': 'Điền vào chỗ trống',
67
+ 'FILL_IN_BLANK_GROUP': 'Điền chỗ trống (nhóm)',
65
68
  'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu',
66
69
  'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
70
+ 'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
67
71
  };
68
72
  return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
69
73
  };
@@ -27,7 +27,9 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
27
27
  'WORD_ORDERING',
28
28
  'MATCH_BY_WRITING_ANSWER',
29
29
  'MATCH_WORD_TO_PICTURE',
30
+ 'MATCHING_WITH_LINES_GROUP',
30
31
  'WRITE_SENTENCES',
32
+ 'FILL_IN_BLANK_GROUP',
31
33
  ];
32
34
  return (_jsxs(Select, { value: value, onValueChange: onValueChange, disabled: disabled, children: [_jsx(SelectTrigger, { id: "questionType", className: "mt-1", children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn lo\u1EA1i c\u00E2u h\u1ECFi" }) }), _jsx(SelectContent, { children: questionTypes.map((type) => (_jsx(SelectItem, { value: type, children: getQuestionTypeLabel(type) }, type))) })] }));
33
35
  }