@tinyweb_dev/oe-exam-sdk 0.2.9 → 0.2.11

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 (93) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +7 -3
  3. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
  4. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -0
  5. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  6. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  7. package/dist/components/exams/take/utils/answer-transformers.js +8 -1
  8. package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
  9. package/dist/components/exams/take/utils/question-transformers.js +47 -2
  10. package/dist/components/questions/_shared/config/question-types.config.js +18 -0
  11. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
  12. package/dist/components/questions/_shared/types/index.d.ts +1 -0
  13. package/dist/components/questions/_shared/types/index.js +1 -0
  14. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
  15. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
  16. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
  17. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
  18. package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
  19. package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
  20. package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
  21. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  22. package/dist/components/questions/creator/question-type-registry.js +6 -0
  23. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +1 -1
  24. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +5 -3
  25. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
  26. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  27. package/dist/components/questions/types/fill-in-blank-group/transform.js +21 -2
  28. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
  29. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
  30. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
  31. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
  32. package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
  33. package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
  34. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
  35. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
  36. package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
  37. package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
  38. package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
  39. package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
  40. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
  41. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
  42. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
  43. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
  44. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
  45. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
  46. package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
  47. package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
  48. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
  49. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
  50. package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
  51. package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
  52. package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
  53. package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
  54. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
  55. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
  56. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
  57. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +121 -0
  58. package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
  59. package/dist/components/questions/types/word-ordering-group/index.js +5 -0
  60. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
  61. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
  62. package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
  63. package/dist/components/questions/types/word-ordering-group/register.js +36 -0
  64. package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
  65. package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
  66. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +5 -5
  67. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
  68. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
  69. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
  70. package/dist/components/questions/viewer/QuestionViewer.js +6 -0
  71. package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
  72. package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
  73. package/dist/components/questions/viewer/index.d.ts +2 -0
  74. package/dist/components/questions/viewer/index.js +2 -0
  75. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  76. package/dist/components/results/renderers/review-question-body-movers.js +6 -2
  77. package/dist/components/results/review-data.js +13 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/shared/constants/question-skills.js +6 -0
  81. package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
  82. package/dist/shared/lib/utils/question-transform.js +6 -0
  83. package/dist/shared/types/common.types.d.ts +1 -1
  84. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  85. package/dist/shared/types/questions/index.d.ts +3 -0
  86. package/dist/shared/types/questions/index.js +6 -0
  87. package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
  88. package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
  89. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
  90. package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
  91. package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
  92. package/dist/shared/types/questions/word-ordering-group.js +1 -0
  93. package/package.json +1 -1
@@ -8,6 +8,16 @@ function asRecord(value) {
8
8
  function asString(value, fallback = '') {
9
9
  return typeof value === 'string' ? value : fallback;
10
10
  }
11
+ function asOptionId(value) {
12
+ if (typeof value === 'string') {
13
+ return value.trim();
14
+ }
15
+ if (Array.isArray(value)) {
16
+ const first = value.find((item) => typeof item === 'string' && item.trim() !== '');
17
+ return typeof first === 'string' ? first.trim() : '';
18
+ }
19
+ return '';
20
+ }
11
21
  function asOptionalUrl(value) {
12
22
  if (typeof value !== 'string') {
13
23
  return undefined;
@@ -63,7 +73,7 @@ export function mapChooseAnswerGroupItem(item, index, fallbackAnswer) {
63
73
  question: asString(content.question) || asString(record.question),
64
74
  optionType: asString(content.optionType) === 'image' ? 'image' : 'text',
65
75
  options,
66
- correctAnswer: asString(correctAnswer.answer) || fallbackAnswer || options[0]?.id || 'opt-1',
76
+ correctAnswer: asOptionId(correctAnswer.answer) || fallbackAnswer || options[0]?.id || 'opt-1',
67
77
  isExample,
68
78
  points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
69
79
  questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
@@ -78,15 +88,17 @@ export function mapQuestionToChooseAnswerGroupData(question) {
78
88
  const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
79
89
  const explanation = String(source.explanation || answer.explanation || '');
80
90
  const fallbackAnswers = Array.isArray(correctAnswer.answer)
81
- ? correctAnswer.answer.map((value) => typeof value === 'string' ? value : undefined)
91
+ ? correctAnswer.answer.map((value) => asOptionId(value))
82
92
  : [];
83
93
  if (Array.isArray(content.items)) {
84
94
  const items = content.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index]));
85
95
  const audioUrl = asOptionalUrl(asRecord(content.meta).audioUrl);
96
+ const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(asRecord(content.meta).imageUrl));
86
97
  return {
87
98
  instruction: asString(asRecord(content.meta).instruction) || 'Choose the correct answer.',
88
99
  passage: asString(asRecord(content.meta).passage),
89
100
  ...(audioUrl ? { audioUrl } : {}),
101
+ ...(imageUrl !== undefined ? { imageUrl } : {}),
90
102
  items,
91
103
  explanation,
92
104
  points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
@@ -94,10 +106,12 @@ export function mapQuestionToChooseAnswerGroupData(question) {
94
106
  }
95
107
  if (Array.isArray(answer.items)) {
96
108
  const audioUrl = asOptionalUrl(answer.audioUrl);
109
+ const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answer.imageUrl));
97
110
  return {
98
111
  instruction: asString(answer.instruction, 'Choose the correct answer.'),
99
112
  passage: asString(answer.passage),
100
113
  ...(audioUrl ? { audioUrl } : {}),
114
+ ...(imageUrl !== undefined ? { imageUrl } : {}),
101
115
  items: answer.items.map((item, index) => mapChooseAnswerGroupItem(item, index, fallbackAnswers[index])),
102
116
  explanation,
103
117
  points: typeof answer.points === 'number' ? answer.points : undefined,
@@ -51,6 +51,10 @@ export const transformChooseTheCorrectAnswerGroup = (question) => {
51
51
  if (answerData?.audioUrl && answerData.audioUrl.trim()) {
52
52
  meta.audioUrl = answerData.audioUrl.trim();
53
53
  }
54
+ const groupImageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answerData?.imageUrl));
55
+ if (groupImageUrl !== undefined) {
56
+ meta.imageUrl = groupImageUrl;
57
+ }
54
58
  return {
55
59
  apiContent: {
56
60
  meta,
@@ -20,9 +20,28 @@ function toItemAnswer(item) {
20
20
  caseSensitive: item.caseSensitive === true,
21
21
  };
22
22
  }
23
- export const transformFillInBlankGroup = (question) => {
23
+ /**
24
+ * wrapOnSave nests creator fields under `answer`. getFormData() (Quiz Playground
25
+ * Update save) returns the same fields at the root. Prefer nested items, then root.
26
+ */
27
+ function resolveFillInBlankGroupSource(question) {
24
28
  const answerData = question.answer;
25
- const items = Array.isArray(answerData?.items) ? answerData.items : [];
29
+ const rootData = question;
30
+ const answerItems = Array.isArray(answerData?.items) ? answerData.items : null;
31
+ const rootItems = Array.isArray(rootData.items) ? rootData.items : null;
32
+ if (answerItems && answerItems.length > 0) {
33
+ return { items: answerItems, source: answerData ?? {} };
34
+ }
35
+ if (rootItems && rootItems.length > 0) {
36
+ return { items: rootItems, source: rootData };
37
+ }
38
+ return {
39
+ items: answerItems ?? rootItems ?? [],
40
+ source: answerData ?? {},
41
+ };
42
+ }
43
+ export const transformFillInBlankGroup = (question) => {
44
+ const { items, source: answerData } = resolveFillInBlankGroupSource(question);
26
45
  if (items.length === 0) {
27
46
  return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
28
47
  }
@@ -0,0 +1,2 @@
1
+ import type { MatchColumnsGroupClientProps } from '../../_shared/types/match-columns-to-make-sentences.type';
2
+ export declare function MatchColumnsToMakeSentencesClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: MatchColumnsGroupClientProps): import("react").JSX.Element;
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Lightbulb, Star } from 'lucide-react';
4
+ import { cn } from '../../../../shared/lib/utils';
5
+ import { assembleMatchColumnsSentence, getMatchColumnsChoiceColumns, getMatchColumnsStemColumn, } from './map-match-columns-to-make-sentences-data';
6
+ function emptyAnswer(stemFragmentId) {
7
+ return { fragmentIds: stemFragmentId ? [stemFragmentId] : [], sentence: '' };
8
+ }
9
+ export function MatchColumnsToMakeSentencesClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
10
+ const items = questionData.items || [];
11
+ const columns = questionData.columns || [];
12
+ const stemColumn = getMatchColumnsStemColumn(columns);
13
+ const choiceColumns = getMatchColumnsChoiceColumns(columns);
14
+ const canEdit = Boolean(onAnswerChange) && !isReviewMode;
15
+ const resolveAnswer = (itemIndex) => {
16
+ const item = items[itemIndex];
17
+ if (!item)
18
+ return emptyAnswer('');
19
+ if (item.isExample) {
20
+ return { fragmentIds: item.fragmentIds, sentence: item.sentence };
21
+ }
22
+ const raw = userAnswers[itemIndex];
23
+ if (raw && Array.isArray(raw.fragmentIds)) {
24
+ return raw;
25
+ }
26
+ return emptyAnswer(item.stemFragmentId);
27
+ };
28
+ const handleChoiceChange = (itemIndex, choiceIndex, fragmentId) => {
29
+ if (!canEdit || items[itemIndex]?.isExample)
30
+ return;
31
+ const next = items.map((item, index) => {
32
+ if (item.isExample) {
33
+ return { fragmentIds: item.fragmentIds, sentence: item.sentence };
34
+ }
35
+ const current = resolveAnswer(index);
36
+ if (index !== itemIndex)
37
+ return current;
38
+ const fragmentIds = [
39
+ item.stemFragmentId,
40
+ ...choiceColumns.map((column, colIndex) => {
41
+ if (colIndex === choiceIndex)
42
+ return fragmentId;
43
+ return current.fragmentIds[colIndex + 1] || '';
44
+ }),
45
+ ];
46
+ return {
47
+ fragmentIds,
48
+ sentence: assembleMatchColumnsSentence(fragmentIds, columns),
49
+ };
50
+ });
51
+ onAnswerChange?.(next);
52
+ };
53
+ return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-indigo-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-indigo-800", children: questionData.instruction }))] })), _jsx("div", { className: "overflow-x-auto rounded-xl border border-gray-200 bg-white", children: _jsxs("table", { className: "min-w-full text-sm", children: [_jsx("thead", { children: _jsx("tr", { className: "bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500", children: columns.map((column) => (_jsxs("th", { className: "px-3 py-2 font-semibold", children: [column.label || column.id, column.role === 'stem' ? ' (stem)' : ''] }, column.id))) }) }), _jsx("tbody", { children: _jsx("tr", { className: "align-top", children: columns.map((column) => (_jsx("td", { className: "px-3 py-2", children: _jsx("ul", { className: "space-y-1", children: column.fragments.map((fragment) => (_jsxs("li", { className: "text-slate-700", children: [_jsxs("span", { className: "mr-1 font-semibold text-slate-500", children: [fragment.id, "."] }), fragment.text || '—'] }, fragment.id))) }) }, `${column.id}-bank`))) }) })] }) }), _jsx("div", { className: "space-y-3", children: items.map((item, itemIndex) => {
54
+ const current = resolveAnswer(itemIndex);
55
+ const canSelect = canEdit && !item.isExample;
56
+ const stemText = stemColumn?.fragments.find((fragment) => fragment.id === item.stemFragmentId)?.text
57
+ || item.stemFragmentId
58
+ || '—';
59
+ const isCorrect = isReviewMode &&
60
+ current.sentence.trim().toLowerCase() === item.sentence.trim().toLowerCase();
61
+ return (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "grid gap-3 md:grid-cols-[minmax(0,1fr)_repeat(2,minmax(0,1fr))]", children: [_jsx("div", { className: "rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-semibold text-slate-800", children: stemText }), choiceColumns.map((column, choiceIndex) => {
62
+ const selectedId = current.fragmentIds[choiceIndex + 1] || '';
63
+ return (_jsxs("select", { className: cn('rounded-lg border-2 px-3 py-2 text-sm', isReviewMode && selectedId === item.fragmentIds[choiceIndex + 1]
64
+ ? 'border-green-400 bg-green-50'
65
+ : selectedId
66
+ ? 'border-blue-400 bg-blue-50'
67
+ : 'border-gray-200 bg-white'), disabled: !canSelect, value: selectedId, onChange: (event) => handleChoiceChange(itemIndex, choiceIndex, event.target.value), children: [_jsxs("option", { value: "", children: ["Ch\u1ECDn ", column.label || column.id] }), column.fragments.map((fragment) => (_jsxs("option", { value: fragment.id, children: [fragment.id, ". ", fragment.text] }, fragment.id)))] }, `${itemIndex}-${column.id}`));
68
+ })] }), _jsx("p", { className: cn('mt-3 text-sm', isReviewMode
69
+ ? isCorrect
70
+ ? 'text-green-700'
71
+ : 'text-red-700'
72
+ : 'text-slate-600'), children: current.sentence || 'Chưa ghép câu' }), isReviewMode && !item.isExample && (_jsxs("p", { className: "mt-1 text-xs text-slate-500", children: ["\u0110\u00E1p \u00E1n: ", item.sentence] }))] }, `${item.questionNumber}-${itemIndex}`));
73
+ }) }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
74
+ }
@@ -0,0 +1,2 @@
1
+ import type { MatchColumnsGroupCreatorProps } from '../../_shared/types/match-columns-to-make-sentences.type';
2
+ export declare function MatchColumnsToMakeSentencesCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: MatchColumnsGroupCreatorProps): import("react").JSX.Element;
@@ -0,0 +1,222 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { Button } from '../../../../components/ui/button';
5
+ import { Label } from '../../../../components/ui/label';
6
+ import { Input } from '../../../../components/ui/input';
7
+ import { PointsInput } from '../../../../components/ui/points-input';
8
+ import { Textarea } from '../../../../components/ui/textarea';
9
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
10
+ import { Switch } from '../../../../components/ui/switch';
11
+ import { useDebouncedCallback } from '../../../../shared/lib/hooks';
12
+ import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
13
+ import { MatchColumnsToMakeSentencesClient } from './MatchColumnsToMakeSentencesClient';
14
+ import { MATCH_COLUMNS_DEFAULT_INSTRUCTION } from '../../_shared/types/match-columns-to-make-sentences.type';
15
+ function parsePointsValue(raw, allowZero = false) {
16
+ if (raw === '')
17
+ return allowZero ? 0 : 1;
18
+ const parsed = parseInt(raw.replace(/[^0-9]/g, ''), 10);
19
+ if (Number.isNaN(parsed))
20
+ return allowZero ? 0 : 1;
21
+ const min = allowZero ? 0 : 1;
22
+ return Math.max(min, parsed);
23
+ }
24
+ import { assembleMatchColumnsSentence, createEmptyMatchColumnsColumns, createEmptyMatchColumnsItem, getMatchColumnsChoiceColumns, getMatchColumnsStemColumn, mapQuestionToMatchColumnsGroupData, nextMatchColumnsFragmentId, } from './map-match-columns-to-make-sentences-data';
25
+ function sumPoints(items) {
26
+ return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
27
+ }
28
+ function syncItemAnswers(items, columns) {
29
+ const choiceCount = getMatchColumnsChoiceColumns(columns).length;
30
+ return items.map((item, index) => {
31
+ const stemFragmentId = item.stemFragmentId || getMatchColumnsStemColumn(columns)?.fragments[0]?.id || '';
32
+ const fragmentIds = [
33
+ stemFragmentId,
34
+ ...Array.from({ length: choiceCount }, (_, choiceIndex) => item.fragmentIds[choiceIndex + 1] || ''),
35
+ ];
36
+ return {
37
+ ...item,
38
+ stemFragmentId,
39
+ fragmentIds,
40
+ sentence: assembleMatchColumnsSentence(fragmentIds, columns),
41
+ questionNumber: item.questionNumber || index + 1,
42
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
43
+ };
44
+ });
45
+ }
46
+ export function MatchColumnsToMakeSentencesCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
47
+ const seeded = mapQuestionToMatchColumnsGroupData(initialData ? { answer: initialData } : null);
48
+ const [title, setTitle] = useState(initialData?.title ?? seeded.title ?? '');
49
+ const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || MATCH_COLUMNS_DEFAULT_INSTRUCTION);
50
+ const [columns, setColumns] = useState(() => (initialData?.columns?.length ? initialData.columns : seeded.columns.length ? seeded.columns : createEmptyMatchColumnsColumns()));
51
+ const [items, setItems] = useState(() => (initialData?.items?.length ? syncItemAnswers(initialData.items, initialData.columns || columns) : seeded.items));
52
+ const [explanation, setExplanation] = useState(initialData?.explanation || '');
53
+ const [isClientMode, setIsClientMode] = useState(false);
54
+ const [errors, setErrors] = useState([]);
55
+ const onChangeRef = useRef(onChange);
56
+ const previousPayloadRef = useRef('');
57
+ useEffect(() => {
58
+ onChangeRef.current = onChange;
59
+ }, [onChange]);
60
+ const buildPayload = (nextTitle = title, nextInstruction = instruction, nextColumns = columns, nextItems = items, nextExplanation = explanation) => ({
61
+ title: nextTitle,
62
+ instruction: nextInstruction,
63
+ columns: nextColumns,
64
+ items: syncItemAnswers(nextItems, nextColumns),
65
+ explanation: nextExplanation,
66
+ points: sumPoints(nextItems),
67
+ });
68
+ const debouncedOnChange = useDebouncedCallback((payload) => {
69
+ onChangeRef.current?.(payload);
70
+ }, 300);
71
+ useEffect(() => {
72
+ const payload = buildPayload();
73
+ const serialized = JSON.stringify(payload);
74
+ if (serialized !== previousPayloadRef.current) {
75
+ previousPayloadRef.current = serialized;
76
+ debouncedOnChange(payload);
77
+ onUnsavedChangesChange?.(true);
78
+ }
79
+ }, [title, instruction, columns, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
80
+ const validate = () => {
81
+ const nextErrors = [];
82
+ if (!instruction.trim())
83
+ nextErrors.push('Hướng dẫn (Instruction) không được để trống.');
84
+ if (columns.length < 2)
85
+ nextErrors.push('Cần ít nhất 2 cột (1 stem + 1 choice).');
86
+ const stems = columns.filter((column) => column.role === 'stem');
87
+ if (stems.length !== 1)
88
+ nextErrors.push('Cần đúng 1 cột stem.');
89
+ if (getMatchColumnsChoiceColumns(columns).length < 1)
90
+ nextErrors.push('Cần ít nhất 1 cột choice.');
91
+ columns.forEach((column, index) => {
92
+ if (!column.fragments.length) {
93
+ nextErrors.push(`Cột ${index + 1}: cần ít nhất một fragment.`);
94
+ }
95
+ column.fragments.forEach((fragment, fragmentIndex) => {
96
+ if (!fragment.id.trim() || !fragment.text.trim()) {
97
+ nextErrors.push(`Cột ${index + 1}, fragment ${fragmentIndex + 1}: id và text bắt buộc.`);
98
+ }
99
+ });
100
+ });
101
+ if (items.length < 1)
102
+ nextErrors.push('Cần ít nhất một câu hỏi.');
103
+ const expectedCount = 1 + getMatchColumnsChoiceColumns(columns).length;
104
+ items.forEach((item, index) => {
105
+ if (!item.stemFragmentId) {
106
+ nextErrors.push(`Câu ${index + 1}: chưa chọn stem.`);
107
+ }
108
+ if (item.fragmentIds.filter(Boolean).length !== expectedCount) {
109
+ nextErrors.push(`Câu ${index + 1}: cần chọn đủ ${expectedCount} fragment.`);
110
+ }
111
+ });
112
+ setErrors(nextErrors);
113
+ return nextErrors.length === 0;
114
+ };
115
+ useEffect(() => {
116
+ if (validationRef) {
117
+ validationRef.current = {
118
+ triggerValidation: async () => validate(),
119
+ getFormData: () => buildPayload(),
120
+ };
121
+ }
122
+ }, [validationRef, title, instruction, columns, items, explanation]);
123
+ const updateColumn = (index, patch) => {
124
+ setColumns((prev) => prev.map((column, i) => (i === index ? { ...column, ...patch } : column)));
125
+ };
126
+ const updateFragment = (columnIndex, fragmentIndex, patch) => {
127
+ setColumns((prev) => prev.map((column, i) => {
128
+ if (i !== columnIndex)
129
+ return column;
130
+ return {
131
+ ...column,
132
+ fragments: column.fragments.map((fragment, fi) => (fi === fragmentIndex ? { ...fragment, ...patch } : fragment)),
133
+ };
134
+ }));
135
+ };
136
+ const addFragment = (columnIndex) => {
137
+ setColumns((prev) => prev.map((column, i) => {
138
+ if (i !== columnIndex)
139
+ return column;
140
+ return {
141
+ ...column,
142
+ fragments: [...column.fragments, { id: nextMatchColumnsFragmentId(prev), text: '' }],
143
+ };
144
+ }));
145
+ };
146
+ const removeFragment = (columnIndex, fragmentIndex) => {
147
+ setColumns((prev) => prev.map((column, i) => {
148
+ if (i !== columnIndex || column.fragments.length <= 1)
149
+ return column;
150
+ return {
151
+ ...column,
152
+ fragments: column.fragments.filter((_, fi) => fi !== fragmentIndex),
153
+ };
154
+ }));
155
+ };
156
+ const addChoiceColumn = () => {
157
+ setColumns((prev) => [
158
+ ...prev,
159
+ {
160
+ id: `col-${prev.length + 1}`,
161
+ label: String(prev.length + 1),
162
+ role: 'choice',
163
+ fragments: [{ id: nextMatchColumnsFragmentId(prev), text: '' }],
164
+ },
165
+ ]);
166
+ };
167
+ const removeColumn = (index) => {
168
+ setColumns((prev) => {
169
+ const target = prev[index];
170
+ if (!target || target.role === 'stem' || prev.filter((column) => column.role === 'choice').length <= 1) {
171
+ return prev;
172
+ }
173
+ return prev.filter((_, i) => i !== index);
174
+ });
175
+ };
176
+ const updateItem = (index, patch) => {
177
+ setItems((prev) => {
178
+ const next = [...prev];
179
+ const current = next[index];
180
+ if (!current)
181
+ return prev;
182
+ const merged = { ...current, ...patch };
183
+ if (patch.stemFragmentId) {
184
+ merged.fragmentIds = [patch.stemFragmentId, ...current.fragmentIds.slice(1)];
185
+ }
186
+ next[index] = {
187
+ ...merged,
188
+ sentence: assembleMatchColumnsSentence(merged.fragmentIds, columns),
189
+ };
190
+ return next;
191
+ });
192
+ };
193
+ const setItemChoice = (itemIndex, choiceIndex, fragmentId) => {
194
+ setItems((prev) => prev.map((item, index) => {
195
+ if (index !== itemIndex)
196
+ return item;
197
+ const fragmentIds = item.fragmentIds.map((id, fi) => (fi === choiceIndex + 1 ? fragmentId : id));
198
+ return {
199
+ ...item,
200
+ fragmentIds,
201
+ sentence: assembleMatchColumnsSentence(fragmentIds, columns),
202
+ };
203
+ }));
204
+ };
205
+ const addItem = () => {
206
+ setItems((prev) => [...prev, createEmptyMatchColumnsItem(prev.length + 1, columns)]);
207
+ };
208
+ const removeItem = (index) => {
209
+ setItems((prev) => {
210
+ if (prev.length <= 1)
211
+ return prev;
212
+ return prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, questionNumber: i + 1 }));
213
+ });
214
+ };
215
+ const currentPayload = buildPayload();
216
+ const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
217
+ const stemColumn = getMatchColumnsStemColumn(columns);
218
+ const choiceColumns = getMatchColumnsChoiceColumns(columns);
219
+ return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-medium text-gray-900", children: "T\u1EA1o c\u00E2u h\u1ECFi Gh\u00E9p c\u1ED9t th\u00E0nh c\u00E2u" }), _jsx("p", { className: "text-sm text-gray-500", children: "C\u1ED9t stem c\u1ED1 \u0111\u1ECBnh theo d\u00F2ng; h\u1ECDc sinh ch\u1ECDn 1 fragment m\u1ED7i c\u1ED9t choice." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode((prev) => !prev), className: "gap-2", children: [isClientMode ? _jsx(Pencil, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }), isClientMode ? 'Chế độ chỉnh sửa' : 'Xem trước học sinh'] })] }), displayErrors.length > 0 && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-5", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), isClientMode ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Xem tr\u01B0\u1EDBc hi\u1EC3n th\u1ECB h\u1ECDc sinh" }) }), _jsx(CardContent, { children: _jsx(MatchColumnsToMakeSentencesClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Th\u00F4ng tin chung" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "match-columns-title", children: "Ti\u00EAu \u0111\u1EC1" }), _jsx(Input, { id: "match-columns-title", className: "mt-1", value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "match-columns-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "match-columns-instruction", className: "mt-1", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: MATCH_COLUMNS_DEFAULT_INSTRUCTION })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { className: "text-base font-semibold", children: "C\u1ED9t fragment" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addChoiceColumn, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u1ED9t choice"] })] }), _jsx(CardContent, { className: "grid gap-4 md:grid-cols-3", children: columns.map((column, columnIndex) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Input, { value: column.label || '', onChange: (e) => updateColumn(columnIndex, { label: e.target.value }), placeholder: "Nh\u00E3n c\u1ED9t" }), _jsx("span", { className: "text-xs text-slate-500", children: column.role }), column.role === 'choice' && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeColumn(columnIndex), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), column.fragments.map((fragment, fragmentIndex) => (_jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { className: "w-16", value: fragment.id, onChange: (e) => updateFragment(columnIndex, fragmentIndex, { id: e.target.value }) }), _jsx(Input, { value: fragment.text, onChange: (e) => updateFragment(columnIndex, fragmentIndex, { text: e.target.value }), placeholder: "Text" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeFragment(columnIndex, fragmentIndex), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, `${column.id}-${fragmentIndex}`))), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => addFragment(columnIndex), children: "Th\u00EAm fragment" })] }, column.id))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["C\u00E2u h\u1ECFi (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "gap-2", children: [_jsx(Plus, { className: "h-4 w-4" }), "Th\u00EAm c\u00E2u"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 bg-gray-50/50 p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: `match-columns-example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `match-columns-example-${index}`, className: "flex items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
220
+ points: parsePointsValue(event.target.value, true),
221
+ }) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(index), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }), _jsxs("div", { className: "grid gap-3 md:grid-cols-3", children: [_jsxs("select", { className: "rounded-md border px-3 py-2 text-sm", value: item.stemFragmentId, onChange: (e) => updateItem(index, { stemFragmentId: e.target.value }), children: [_jsx("option", { value: "", children: "Ch\u1ECDn stem" }), (stemColumn?.fragments || []).map((fragment) => (_jsxs("option", { value: fragment.id, children: [fragment.id, ". ", fragment.text || '(trống)'] }, fragment.id)))] }), choiceColumns.map((column, choiceIndex) => (_jsxs("select", { className: "rounded-md border px-3 py-2 text-sm", value: item.fragmentIds[choiceIndex + 1] || '', onChange: (e) => setItemChoice(index, choiceIndex, e.target.value), children: [_jsxs("option", { value: "", children: ["Ch\u1ECDn ", column.label || column.id] }), column.fragments.map((fragment) => (_jsxs("option", { value: fragment.id, children: [fragment.id, ". ", fragment.text || '(trống)'] }, fragment.id)))] }, `${index}-${column.id}`)))] }), _jsx("p", { className: "text-sm text-slate-600", children: item.sentence || 'Câu sẽ tự ghép từ fragment đã chọn.' })] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { id: "match-columns-explanation", value: explanation, onChange: (event) => setExplanation(event.target.value), placeholder: "VD: C\u00E2u tr\u1EA3 l\u1EDDi \u0111\u00FAng l\u00E0... v\u00EC...", rows: 3 }) })] })] }))] }));
222
+ }
@@ -0,0 +1,5 @@
1
+ export * from './MatchColumnsToMakeSentencesCreator';
2
+ export * from './MatchColumnsToMakeSentencesClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-match-columns-to-make-sentences-data';
@@ -0,0 +1,5 @@
1
+ export * from './MatchColumnsToMakeSentencesCreator';
2
+ export * from './MatchColumnsToMakeSentencesClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-match-columns-to-make-sentences-data';
@@ -0,0 +1,8 @@
1
+ import type { MatchColumnsColumn, MatchColumnsGroupData, MatchColumnsGroupItemData } from '../../_shared/types/match-columns-to-make-sentences.type';
2
+ export declare function assembleMatchColumnsSentence(fragmentIds: string[], columns: MatchColumnsColumn[]): string;
3
+ export declare function getMatchColumnsStemColumn(columns: MatchColumnsColumn[]): MatchColumnsColumn | undefined;
4
+ export declare function getMatchColumnsChoiceColumns(columns: MatchColumnsColumn[]): MatchColumnsColumn[];
5
+ export declare function nextMatchColumnsFragmentId(columns: MatchColumnsColumn[]): string;
6
+ export declare function createEmptyMatchColumnsColumns(): MatchColumnsColumn[];
7
+ export declare function createEmptyMatchColumnsItem(questionNumber: number, columns: MatchColumnsColumn[]): MatchColumnsGroupItemData;
8
+ export declare function mapQuestionToMatchColumnsGroupData(question: Record<string, unknown> | null | undefined): MatchColumnsGroupData;
@@ -0,0 +1,199 @@
1
+ import { MATCH_COLUMNS_DEFAULT_INSTRUCTION } from '../../_shared/types/match-columns-to-make-sentences.type';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function asRecord(value) {
6
+ return isRecord(value) ? value : {};
7
+ }
8
+ function asString(value, fallback = '') {
9
+ return typeof value === 'string' ? value : fallback;
10
+ }
11
+ export function assembleMatchColumnsSentence(fragmentIds, columns) {
12
+ const texts = new Map();
13
+ columns.forEach((column) => {
14
+ column.fragments.forEach((fragment) => {
15
+ texts.set(fragment.id, fragment.text);
16
+ });
17
+ });
18
+ return fragmentIds
19
+ .map((id) => texts.get(id) || '')
20
+ .filter(Boolean)
21
+ .join(' ')
22
+ .replace(/\s+/g, ' ')
23
+ .trim();
24
+ }
25
+ export function getMatchColumnsStemColumn(columns) {
26
+ return columns.find((column) => column.role === 'stem');
27
+ }
28
+ export function getMatchColumnsChoiceColumns(columns) {
29
+ return columns.filter((column) => column.role === 'choice');
30
+ }
31
+ export function nextMatchColumnsFragmentId(columns) {
32
+ const used = new Set();
33
+ columns.forEach((column) => {
34
+ column.fragments.forEach((fragment) => used.add(fragment.id));
35
+ });
36
+ let n = 1;
37
+ while (used.has(String(n)))
38
+ n += 1;
39
+ return String(n);
40
+ }
41
+ export function createEmptyMatchColumnsColumns() {
42
+ return [
43
+ {
44
+ id: 'col-1',
45
+ label: '1',
46
+ role: 'stem',
47
+ fragments: [
48
+ { id: '1', text: '' },
49
+ { id: '2', text: '' },
50
+ ],
51
+ },
52
+ {
53
+ id: 'col-2',
54
+ label: '2',
55
+ role: 'choice',
56
+ fragments: [
57
+ { id: 'a', text: '' },
58
+ { id: 'b', text: '' },
59
+ ],
60
+ },
61
+ {
62
+ id: 'col-3',
63
+ label: '3',
64
+ role: 'choice',
65
+ fragments: [
66
+ { id: 'c', text: '' },
67
+ { id: 'd', text: '' },
68
+ ],
69
+ },
70
+ ];
71
+ }
72
+ export function createEmptyMatchColumnsItem(questionNumber, columns) {
73
+ const stem = getMatchColumnsStemColumn(columns);
74
+ const choices = getMatchColumnsChoiceColumns(columns);
75
+ const stemFragmentId = stem?.fragments[0]?.id || '';
76
+ const fragmentIds = [
77
+ stemFragmentId,
78
+ ...choices.map((column) => column.fragments[0]?.id || ''),
79
+ ];
80
+ return {
81
+ stemFragmentId,
82
+ fragmentIds,
83
+ sentence: assembleMatchColumnsSentence(fragmentIds, columns),
84
+ isExample: false,
85
+ points: 1,
86
+ questionNumber,
87
+ };
88
+ }
89
+ function parseItemAnswer(raw) {
90
+ const record = asRecord(raw);
91
+ const fragmentIds = Array.isArray(record.fragmentIds)
92
+ ? record.fragmentIds.filter((id) => typeof id === 'string')
93
+ : [];
94
+ return {
95
+ fragmentIds,
96
+ sentence: asString(record.sentence),
97
+ };
98
+ }
99
+ function mapColumn(raw, index) {
100
+ const record = asRecord(raw);
101
+ const fragments = Array.isArray(record.fragments)
102
+ ? record.fragments.map((fragment, fragmentIndex) => {
103
+ const item = asRecord(fragment);
104
+ return {
105
+ id: asString(item.id, `${index + 1}-${fragmentIndex + 1}`),
106
+ text: asString(item.text),
107
+ };
108
+ })
109
+ : [];
110
+ return {
111
+ id: asString(record.id, `col-${index + 1}`),
112
+ ...(asString(record.label) ? { label: asString(record.label) } : {}),
113
+ role: record.role === 'stem' ? 'stem' : 'choice',
114
+ fragments,
115
+ };
116
+ }
117
+ function mapApiItem(item, index, columns, fallbackAnswer) {
118
+ const record = asRecord(item);
119
+ const content = asRecord(record.content);
120
+ const correctAnswer = asRecord(record.correctAnswer);
121
+ const parsed = parseItemAnswer(correctAnswer.answer ?? fallbackAnswer);
122
+ const isExample = record.isExample === true;
123
+ const stemFragmentId = asString(content.stemFragmentId) || parsed.fragmentIds[0] || '';
124
+ const fragmentIds = parsed.fragmentIds.length > 0
125
+ ? parsed.fragmentIds
126
+ : [stemFragmentId];
127
+ const sentence = parsed.sentence.trim() || assembleMatchColumnsSentence(fragmentIds, columns);
128
+ return {
129
+ stemFragmentId,
130
+ fragmentIds,
131
+ sentence,
132
+ isExample,
133
+ points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
134
+ questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
135
+ };
136
+ }
137
+ export function mapQuestionToMatchColumnsGroupData(question) {
138
+ const source = asRecord(question);
139
+ const content = asRecord(source.content);
140
+ const answer = asRecord(source.answer);
141
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
142
+ const fallbackAnswers = Array.isArray(correctAnswer.answer)
143
+ ? correctAnswer.answer
144
+ : Array.isArray(answer.answer)
145
+ ? answer.answer
146
+ : [];
147
+ const explanation = asString(source.explanation) || asString(answer.explanation);
148
+ const meta = asRecord(content.meta ?? answer.meta);
149
+ if (Array.isArray(content.columns) || Array.isArray(content.items)) {
150
+ const columns = Array.isArray(content.columns)
151
+ ? content.columns.map((column, index) => mapColumn(column, index))
152
+ : [];
153
+ const items = Array.isArray(content.items)
154
+ ? content.items.map((item, index) => mapApiItem(item, index, columns, fallbackAnswers[index]))
155
+ : [];
156
+ return {
157
+ title: asString(meta.title) || asString(answer.title),
158
+ instruction: asString(meta.instruction) ||
159
+ asString(answer.instruction) ||
160
+ MATCH_COLUMNS_DEFAULT_INSTRUCTION,
161
+ columns,
162
+ items,
163
+ explanation,
164
+ points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
165
+ };
166
+ }
167
+ if (Array.isArray(answer.columns) || Array.isArray(answer.items)) {
168
+ const columns = Array.isArray(answer.columns)
169
+ ? answer.columns.map((column, index) => mapColumn(column, index))
170
+ : [];
171
+ const items = Array.isArray(answer.items)
172
+ ? answer.items.map((item, index) => ({
173
+ ...createEmptyMatchColumnsItem(index + 1, columns),
174
+ ...item,
175
+ questionNumber: item.questionNumber || index + 1,
176
+ }))
177
+ : [];
178
+ return {
179
+ title: asString(answer.title),
180
+ instruction: asString(answer.instruction) || MATCH_COLUMNS_DEFAULT_INSTRUCTION,
181
+ columns,
182
+ items,
183
+ explanation,
184
+ points: typeof answer.points === 'number'
185
+ ? answer.points
186
+ : items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
187
+ };
188
+ }
189
+ const columns = createEmptyMatchColumnsColumns();
190
+ const items = [createEmptyMatchColumnsItem(1, columns)];
191
+ return {
192
+ title: '',
193
+ instruction: MATCH_COLUMNS_DEFAULT_INSTRUCTION,
194
+ columns,
195
+ items,
196
+ explanation,
197
+ points: 1,
198
+ };
199
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export declare const matchColumnsToMakeSentencesRegistration: QuestionTypeRegistration;