@tinyweb_dev/oe-exam-sdk 0.2.8 → 0.2.10

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 (104) 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/MoversChooseBestAnswerRenderer.js +5 -4
  4. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.d.ts +13 -0
  5. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingGroupRenderer.js +30 -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/utils/answer-transformers.js +8 -1
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +16 -1
  10. package/dist/components/exams/take/utils/question-transformers.js +49 -4
  11. package/dist/components/index.d.ts +1 -0
  12. package/dist/components/index.js +1 -0
  13. package/dist/components/questions/_shared/config/question-types.config.js +18 -0
  14. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +1 -0
  15. package/dist/components/questions/_shared/types/index.d.ts +1 -0
  16. package/dist/components/questions/_shared/types/index.js +1 -0
  17. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.d.ts +56 -0
  18. package/dist/components/questions/_shared/types/match-columns-to-make-sentences.type.js +2 -0
  19. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +35 -0
  20. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +2 -0
  21. package/dist/components/questions/_shared/types/word-ordering-group.type.d.ts +34 -0
  22. package/dist/components/questions/_shared/types/word-ordering-group.type.js +2 -0
  23. package/dist/components/questions/components/QuestionSearchDropdown.js +3 -0
  24. package/dist/components/questions/components/QuestionTypeSelector.js +3 -0
  25. package/dist/components/questions/creator/question-type-registry.js +6 -0
  26. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +6 -4
  27. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +16 -4
  28. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -3
  29. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +6 -4
  30. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +16 -2
  31. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +4 -0
  32. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.d.ts +2 -0
  33. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +74 -0
  34. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.d.ts +2 -0
  35. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesCreator.js +222 -0
  36. package/dist/components/questions/types/match-columns-to-make-sentences/index.d.ts +5 -0
  37. package/dist/components/questions/types/match-columns-to-make-sentences/index.js +5 -0
  38. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.d.ts +8 -0
  39. package/dist/components/questions/types/match-columns-to-make-sentences/map-match-columns-to-make-sentences-data.js +199 -0
  40. package/dist/components/questions/types/match-columns-to-make-sentences/register.d.ts +2 -0
  41. package/dist/components/questions/types/match-columns-to-make-sentences/register.js +36 -0
  42. package/dist/components/questions/types/match-columns-to-make-sentences/transform.d.ts +2 -0
  43. package/dist/components/questions/types/match-columns-to-make-sentences/transform.js +66 -0
  44. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.d.ts +2 -0
  45. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +65 -0
  46. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.d.ts +2 -0
  47. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +95 -0
  48. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +6 -0
  49. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +16 -0
  50. package/dist/components/questions/types/match-word-to-picture-group/index.d.ts +5 -0
  51. package/dist/components/questions/types/match-word-to-picture-group/index.js +5 -0
  52. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.d.ts +4 -0
  53. package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +77 -0
  54. package/dist/components/questions/types/match-word-to-picture-group/register.d.ts +2 -0
  55. package/dist/components/questions/types/match-word-to-picture-group/register.js +36 -0
  56. package/dist/components/questions/types/match-word-to-picture-group/transform.d.ts +2 -0
  57. package/dist/components/questions/types/match-word-to-picture-group/transform.js +36 -0
  58. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.d.ts +2 -0
  59. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +25 -0
  60. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.d.ts +2 -0
  61. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupCreator.js +106 -0
  62. package/dist/components/questions/types/word-ordering-group/index.d.ts +5 -0
  63. package/dist/components/questions/types/word-ordering-group/index.js +5 -0
  64. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.d.ts +4 -0
  65. package/dist/components/questions/types/word-ordering-group/map-word-ordering-group-data.js +74 -0
  66. package/dist/components/questions/types/word-ordering-group/register.d.ts +2 -0
  67. package/dist/components/questions/types/word-ordering-group/register.js +36 -0
  68. package/dist/components/questions/types/word-ordering-group/transform.d.ts +2 -0
  69. package/dist/components/questions/types/word-ordering-group/transform.js +33 -0
  70. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.d.ts +8 -0
  71. package/dist/components/questions/viewer/MatchColumnsToMakeSentencesViewer.js +49 -0
  72. package/dist/components/questions/viewer/MatchWordToPictureGroupViewer.js +2 -2
  73. package/dist/components/questions/viewer/QuestionViewer.js +6 -0
  74. package/dist/components/questions/viewer/WordOrderingGroupViewer.d.ts +7 -0
  75. package/dist/components/questions/viewer/WordOrderingGroupViewer.js +29 -0
  76. package/dist/components/questions/viewer/index.d.ts +2 -0
  77. package/dist/components/questions/viewer/index.js +2 -0
  78. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +3 -2
  79. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
  80. package/dist/components/results/renderers/review-question-body-movers.js +6 -2
  81. package/dist/components/results/review-data.js +13 -0
  82. package/dist/components/shared/RichTextEditor.d.ts +4 -2
  83. package/dist/components/shared/RichTextEditor.js +34 -11
  84. package/dist/components/shared/RichTextHtml.d.ts +8 -0
  85. package/dist/components/shared/RichTextHtml.js +9 -0
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/shared/constants/question-skills.js +6 -0
  89. package/dist/shared/lib/rich-text.d.ts +3 -0
  90. package/dist/shared/lib/rich-text.js +24 -0
  91. package/dist/shared/lib/utils/question-reverse-transform.js +111 -2
  92. package/dist/shared/lib/utils/question-transform.js +6 -0
  93. package/dist/shared/lib/validation/choose-correct-answer.validation.js +4 -2
  94. package/dist/shared/types/common.types.d.ts +1 -1
  95. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -0
  96. package/dist/shared/types/questions/index.d.ts +3 -0
  97. package/dist/shared/types/questions/index.js +6 -0
  98. package/dist/shared/types/questions/match-columns-to-make-sentences.d.ts +50 -0
  99. package/dist/shared/types/questions/match-columns-to-make-sentences.js +1 -0
  100. package/dist/shared/types/questions/match-word-to-picture-group.d.ts +31 -0
  101. package/dist/shared/types/questions/match-word-to-picture-group.js +1 -0
  102. package/dist/shared/types/questions/word-ordering-group.d.ts +28 -0
  103. package/dist/shared/types/questions/word-ordering-group.js +1 -0
  104. package/package.json +1 -1
@@ -0,0 +1,95 @@
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
+ function parsePointsValue(raw, allowZero = false) {
13
+ if (raw === '')
14
+ return allowZero ? 0 : 1;
15
+ const parsed = parseInt(raw.replace(/[^0-9]/g, ''), 10);
16
+ if (Number.isNaN(parsed))
17
+ return allowZero ? 0 : 1;
18
+ const min = allowZero ? 0 : 1;
19
+ return Math.max(min, parsed);
20
+ }
21
+ import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
22
+ import { MatchWordToPictureGroupClient } from './MatchWordToPictureGroupClient';
23
+ import { MatchWordToPictureGroupImageField } from './MatchWordToPictureGroupImageField';
24
+ import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/match-word-to-picture-group.type';
25
+ import { createEmptyMatchWordToPictureGroupItem, mapQuestionToMatchWordToPictureGroupData, } from './map-match-word-to-picture-group-data';
26
+ function sumPoints(items) {
27
+ return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
28
+ }
29
+ export function MatchWordToPictureGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
30
+ const seeded = mapQuestionToMatchWordToPictureGroupData(initialData ? { answer: initialData } : null);
31
+ const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION);
32
+ const [wordBank, setWordBank] = useState(initialData?.wordBank?.length ? initialData.wordBank : seeded.wordBank);
33
+ const [items, setItems] = useState(() => (initialData?.items?.length ? initialData.items : seeded.items));
34
+ const [explanation, setExplanation] = useState(initialData?.explanation || '');
35
+ const [isClientMode, setIsClientMode] = useState(false);
36
+ const [errors, setErrors] = useState([]);
37
+ const onChangeRef = useRef(onChange);
38
+ const previousPayloadRef = useRef('');
39
+ useEffect(() => {
40
+ onChangeRef.current = onChange;
41
+ }, [onChange]);
42
+ const buildPayload = (nextInstruction = instruction, nextWordBank = wordBank, nextItems = items, nextExplanation = explanation) => ({
43
+ instruction: nextInstruction,
44
+ wordBank: nextWordBank,
45
+ items: nextItems,
46
+ explanation: nextExplanation,
47
+ points: sumPoints(nextItems),
48
+ });
49
+ const debouncedOnChange = useDebouncedCallback((payload) => {
50
+ onChangeRef.current?.(payload);
51
+ }, 300);
52
+ useEffect(() => {
53
+ const payload = buildPayload();
54
+ const serialized = JSON.stringify(payload);
55
+ if (serialized !== previousPayloadRef.current) {
56
+ previousPayloadRef.current = serialized;
57
+ debouncedOnChange(payload);
58
+ onUnsavedChangesChange?.(true);
59
+ }
60
+ }, [instruction, wordBank, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
61
+ const validate = () => {
62
+ const nextErrors = [];
63
+ if (!instruction.trim())
64
+ nextErrors.push('Hướng dẫn không được để trống.');
65
+ const words = wordBank.map((word) => word.trim()).filter(Boolean);
66
+ if (words.length < 1)
67
+ nextErrors.push('Word bank cần ít nhất một từ.');
68
+ if (items.length < 1)
69
+ nextErrors.push('Cần ít nhất một câu hỏi.');
70
+ items.forEach((item, index) => {
71
+ if (!item.imageUrl.trim())
72
+ nextErrors.push(`Câu ${index + 1}: cần ảnh.`);
73
+ if (!item.correctAnswer.trim())
74
+ nextErrors.push(`Câu ${index + 1}: cần chọn từ đúng.`);
75
+ });
76
+ setErrors(nextErrors);
77
+ return nextErrors.length === 0;
78
+ };
79
+ useEffect(() => {
80
+ if (validationRef) {
81
+ validationRef.current = {
82
+ triggerValidation: async () => validate(),
83
+ getFormData: () => buildPayload(),
84
+ };
85
+ }
86
+ }, [validationRef, instruction, wordBank, items, explanation]);
87
+ const updateItem = (index, patch) => {
88
+ setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
89
+ };
90
+ const currentPayload = buildPayload();
91
+ const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
92
+ 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 N\u1ED1i t\u1EEB v\u1EDBi tranh (nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "Word bank d\u00F9ng chung; m\u1ED7i d\u00F2ng m\u1ED9t tranh." })] }), _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(MatchWordToPictureGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "Word bank" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "mwtp-group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-group-instruction", className: "mt-1", value: instruction, onChange: (e) => setInstruction(e.target.value) })] }), wordBank.map((word, index) => (_jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { value: word, onChange: (e) => setWordBank((prev) => prev.map((item, i) => (i === index ? e.target.value : item))), placeholder: `Từ ${index + 1}` }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setWordBank((prev) => prev.filter((_, i) => i !== index)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setWordBank((prev) => [...prev, '']), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Th\u00EAm t\u1EEB"] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "text-base font-semibold", children: ["Tranh (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setItems((prev) => [...prev, createEmptyMatchWordToPictureGroupItem(prev.length + 1)]), children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Th\u00EAm tranh"] })] }), _jsx(CardContent, { className: "space-y-4", children: items.map((item, index) => (_jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-medium", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { 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, {
93
+ points: parsePointsValue(event.target.value, true),
94
+ }) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setItems((prev) => prev.filter((_, i) => i !== index).map((entry, i) => ({ ...entry, questionNumber: i + 1 }))), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }), _jsx(MatchWordToPictureGroupImageField, { id: `mwtp-group-image-${index}`, label: "\u1EA2nh", value: item.imageUrl, onChange: (url) => updateItem(index, { imageUrl: url }) }), _jsxs("select", { className: "rounded-md border px-3 py-2 text-sm", value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), wordBank.filter((word) => word.trim()).map((word) => (_jsx("option", { value: word, children: word }, word)))] })] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { value: explanation, onChange: (event) => setExplanation(event.target.value), rows: 3 }) })] })] }))] }));
95
+ }
@@ -0,0 +1,6 @@
1
+ export declare function MatchWordToPictureGroupImageField({ id, label, value, onChange, }: {
2
+ id: string;
3
+ label: string;
4
+ value: string;
5
+ onChange: (url: string) => void;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { Button } from '../../../../components/ui/button';
5
+ import { FileUpload } from '../../../../components/ui/file-upload';
6
+ import { ImagePreview } from '../../../../components/ui/image-preview';
7
+ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
8
+ import { Trash2 } from 'lucide-react';
9
+ export function MatchWordToPictureGroupImageField({ id, label, value, onChange, }) {
10
+ const [previewOverride, setPreviewOverride] = useState('');
11
+ const { previewUrl: fetchedPreviewUrl } = usePresignedFileUrl(value);
12
+ const displayUrl = previewOverride ||
13
+ fetchedPreviewUrl ||
14
+ (value.startsWith('http') || value.startsWith('blob:') ? value : '');
15
+ return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: "image/*", value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: 5, placeholder: "Upload \u1EA3nh ho\u1EB7c paste URL", autoUpload: true, prefix: "questions" }), value && (_jsxs("div", { className: "flex items-center gap-2", children: [displayUrl && _jsx(ImagePreview, { src: displayUrl, alt: label, className: "h-20 w-20" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onChange(''), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) })] }))] }));
16
+ }
@@ -0,0 +1,5 @@
1
+ export * from './MatchWordToPictureGroupCreator';
2
+ export * from './MatchWordToPictureGroupClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-match-word-to-picture-group-data';
@@ -0,0 +1,5 @@
1
+ export * from './MatchWordToPictureGroupCreator';
2
+ export * from './MatchWordToPictureGroupClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-match-word-to-picture-group-data';
@@ -0,0 +1,4 @@
1
+ import { type MatchWordToPictureGroupData, type MatchWordToPictureGroupItemData } from '../../_shared/types/match-word-to-picture-group.type';
2
+ export declare function createEmptyMatchWordToPictureGroupItem(questionNumber: number): MatchWordToPictureGroupItemData;
3
+ export declare function mapMatchWordToPictureGroupItem(item: unknown, index: number, fallbackAnswer?: string): MatchWordToPictureGroupItemData;
4
+ export declare function mapQuestionToMatchWordToPictureGroupData(question: Record<string, unknown> | null | undefined): MatchWordToPictureGroupData;
@@ -0,0 +1,77 @@
1
+ import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture-group.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
+ function asStringList(value) {
12
+ if (!Array.isArray(value))
13
+ return [];
14
+ return value
15
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
16
+ .filter(Boolean);
17
+ }
18
+ export function createEmptyMatchWordToPictureGroupItem(questionNumber) {
19
+ return {
20
+ imageUrl: '',
21
+ correctAnswer: '',
22
+ isExample: false,
23
+ points: 1,
24
+ questionNumber,
25
+ };
26
+ }
27
+ export function mapMatchWordToPictureGroupItem(item, index, fallbackAnswer) {
28
+ const record = asRecord(item);
29
+ const content = asRecord(record.content);
30
+ const correctAnswer = asRecord(record.correctAnswer);
31
+ const isExample = record.isExample === true;
32
+ return {
33
+ imageUrl: asString(content.imageUrl) || asString(record.imageUrl),
34
+ correctAnswer: asString(correctAnswer.answer) ||
35
+ asString(record.correctAnswer) ||
36
+ asString(fallbackAnswer),
37
+ isExample,
38
+ points: isExample ? 0 : typeof record.points === 'number' ? record.points : 1,
39
+ questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
40
+ };
41
+ }
42
+ export function mapQuestionToMatchWordToPictureGroupData(question) {
43
+ const source = asRecord(question);
44
+ const content = asRecord(source.content);
45
+ const meta = asRecord(content.meta);
46
+ const answer = asRecord(source.answer);
47
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
48
+ const explanation = asString(source.explanation) || asString(answer.explanation);
49
+ const fallbackAnswers = Array.isArray(correctAnswer.answer)
50
+ ? correctAnswer.answer.map((value) => (typeof value === 'string' ? value : ''))
51
+ : [];
52
+ if (Array.isArray(content.items) || Array.isArray(answer.items)) {
53
+ const rawItems = Array.isArray(content.items) ? content.items : answer.items;
54
+ const items = rawItems.map((item, index) => mapMatchWordToPictureGroupItem(item, index, fallbackAnswers[index]));
55
+ const wordBank = asStringList(meta.wordBank).length
56
+ ? asStringList(meta.wordBank)
57
+ : asStringList(answer.wordBank).length
58
+ ? asStringList(answer.wordBank)
59
+ : Array.from(new Set(items.map((item) => item.correctAnswer).filter(Boolean)));
60
+ return {
61
+ instruction: asString(meta.instruction) ||
62
+ asString(answer.instruction) ||
63
+ MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
64
+ wordBank,
65
+ items,
66
+ explanation,
67
+ points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
68
+ };
69
+ }
70
+ return {
71
+ instruction: MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
72
+ wordBank: ['', ''],
73
+ items: [createEmptyMatchWordToPictureGroupItem(1)],
74
+ explanation,
75
+ points: 1,
76
+ };
77
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export declare const matchWordToPictureGroupRegistration: QuestionTypeRegistration;
@@ -0,0 +1,36 @@
1
+ import { MatchWordToPictureGroupCreator } from './MatchWordToPictureGroupCreator';
2
+ import { mapQuestionToMatchWordToPictureGroupData } from './map-match-word-to-picture-group-data';
3
+ export const matchWordToPictureGroupRegistration = {
4
+ component: MatchWordToPictureGroupCreator,
5
+ transformInitialData: (initialData) => mapQuestionToMatchWordToPictureGroupData(initialData),
6
+ getExtraProps: (ctx) => ({
7
+ onUnsavedChangesChange: ctx.onUnsavedChangesChange,
8
+ validationRef: ctx.validationRef,
9
+ }),
10
+ wrapOnSave: (data, ctx) => {
11
+ const payload = data;
12
+ return {
13
+ type: ctx.questionType,
14
+ content: ctx.state.content,
15
+ points: payload.points ?? ctx.state.points ?? 1,
16
+ level: ctx.state.level,
17
+ difficulty: ctx.state.difficulty,
18
+ skill: ctx.state.skill,
19
+ answer: payload,
20
+ explanation: payload.explanation || '',
21
+ };
22
+ },
23
+ wrapOnChange: (data, ctx) => {
24
+ const payload = data;
25
+ return {
26
+ type: ctx.questionType,
27
+ content: ctx.state.content,
28
+ points: payload.points ?? ctx.state.points ?? 1,
29
+ level: ctx.state.level,
30
+ difficulty: ctx.state.difficulty,
31
+ skill: ctx.state.skill,
32
+ answer: payload,
33
+ explanation: payload.explanation || '',
34
+ };
35
+ },
36
+ };
@@ -0,0 +1,2 @@
1
+ import type { TransformHandler } from '../../../../shared/lib/utils/question-transform-types';
2
+ export declare const transformMatchWordToPictureGroup: TransformHandler;
@@ -0,0 +1,36 @@
1
+ import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, MATCH_WORD_TO_PICTURE_ITEM_TYPE, } from '../../_shared/types/match-word-to-picture-group.type';
2
+ function sumGradablePoints(items) {
3
+ return items.reduce((total, item) => {
4
+ if (item.isExample)
5
+ return total;
6
+ return total + (typeof item.points === 'number' ? item.points : 1);
7
+ }, 0);
8
+ }
9
+ export const transformMatchWordToPictureGroup = (question) => {
10
+ const answerData = question.answer;
11
+ const items = Array.isArray(answerData?.items) ? answerData.items : [];
12
+ const wordBank = Array.isArray(answerData?.wordBank)
13
+ ? answerData.wordBank.filter((word) => typeof word === 'string').map((word) => word.trim()).filter(Boolean)
14
+ : [];
15
+ const apiItems = items.map((item, index) => ({
16
+ questionType: MATCH_WORD_TO_PICTURE_ITEM_TYPE,
17
+ ...(item.isExample ? { isExample: true } : {}),
18
+ content: { imageUrl: item.imageUrl || '' },
19
+ correctAnswer: { answer: item.correctAnswer || '' },
20
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
21
+ questionNumber: item.questionNumber || index + 1,
22
+ }));
23
+ return {
24
+ apiContent: {
25
+ meta: {
26
+ instruction: answerData?.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
27
+ wordBank,
28
+ },
29
+ items: apiItems,
30
+ },
31
+ apiCorrectAnswer: {
32
+ answer: apiItems.map((item) => item.correctAnswer.answer),
33
+ },
34
+ totalPoints: sumGradablePoints(items),
35
+ };
36
+ };
@@ -0,0 +1,2 @@
1
+ import type { WordOrderingGroupClientProps } from '../../_shared/types/word-ordering-group.type';
2
+ export declare function WordOrderingGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: WordOrderingGroupClientProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Lightbulb, Star } from 'lucide-react';
4
+ import { WordOrderingClient } from '../word-ordering/WordOrderingClient';
5
+ export function WordOrderingGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
6
+ const items = questionData.items || [];
7
+ const handleItemSubmit = (itemIndex, ordered) => {
8
+ if (isReviewMode || items[itemIndex]?.isExample || !onAnswerChange)
9
+ return;
10
+ const next = items.map((item, index) => {
11
+ if (item.isExample)
12
+ return item.answer;
13
+ if (index === itemIndex)
14
+ return ordered;
15
+ return userAnswers[index] || [];
16
+ });
17
+ onAnswerChange(next);
18
+ };
19
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-4", 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"] })] }), _jsx(WordOrderingClient, { questionData: {
20
+ words: item.words,
21
+ answer: item.answer,
22
+ isExample: item.isExample,
23
+ points: item.points,
24
+ }, isReviewMode: isReviewMode || item.isExample, userAnswer: item.isExample ? item.answer : userAnswers[itemIndex], onSubmit: (ordered) => handleItemSubmit(itemIndex, ordered) })] }, `${item.questionNumber}-${itemIndex}`))), 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] }))] }));
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { WordOrderingGroupCreatorProps } from '../../_shared/types/word-ordering-group.type';
2
+ export declare function WordOrderingGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: WordOrderingGroupCreatorProps): import("react").JSX.Element;
@@ -0,0 +1,106 @@
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
+ function parsePointsValue(raw, allowZero = false) {
13
+ if (raw === '')
14
+ return allowZero ? 0 : 1;
15
+ const parsed = parseInt(raw.replace(/[^0-9]/g, ''), 10);
16
+ if (Number.isNaN(parsed))
17
+ return allowZero ? 0 : 1;
18
+ const min = allowZero ? 0 : 1;
19
+ return Math.max(min, parsed);
20
+ }
21
+ import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
22
+ import { WordOrderingGroupClient } from './WordOrderingGroupClient';
23
+ import { WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/word-ordering-group.type';
24
+ import { createEmptyWordOrderingGroupItem, mapQuestionToWordOrderingGroupData, } from './map-word-ordering-group-data';
25
+ function parseTokens(value) {
26
+ return value
27
+ .split(/[,\n]/)
28
+ .map((token) => token.trim())
29
+ .filter(Boolean);
30
+ }
31
+ function joinTokens(tokens) {
32
+ return tokens.join(', ');
33
+ }
34
+ function sumPoints(items) {
35
+ return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
36
+ }
37
+ export function WordOrderingGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
38
+ const seeded = mapQuestionToWordOrderingGroupData(initialData ? { answer: initialData } : null);
39
+ const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION);
40
+ const [items, setItems] = useState(() => (initialData?.items?.length ? initialData.items : seeded.items));
41
+ const [explanation, setExplanation] = useState(initialData?.explanation || '');
42
+ const [isClientMode, setIsClientMode] = useState(false);
43
+ const [errors, setErrors] = useState([]);
44
+ const onChangeRef = useRef(onChange);
45
+ const previousPayloadRef = useRef('');
46
+ useEffect(() => {
47
+ onChangeRef.current = onChange;
48
+ }, [onChange]);
49
+ const buildPayload = (nextInstruction = instruction, nextItems = items, nextExplanation = explanation) => ({
50
+ instruction: nextInstruction,
51
+ items: nextItems,
52
+ explanation: nextExplanation,
53
+ points: sumPoints(nextItems),
54
+ });
55
+ const debouncedOnChange = useDebouncedCallback((payload) => {
56
+ onChangeRef.current?.(payload);
57
+ }, 300);
58
+ useEffect(() => {
59
+ const payload = buildPayload();
60
+ const serialized = JSON.stringify(payload);
61
+ if (serialized !== previousPayloadRef.current) {
62
+ previousPayloadRef.current = serialized;
63
+ debouncedOnChange(payload);
64
+ onUnsavedChangesChange?.(true);
65
+ }
66
+ }, [instruction, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
67
+ const validate = () => {
68
+ const nextErrors = [];
69
+ if (!instruction.trim())
70
+ nextErrors.push('Hướng dẫn không được để trống.');
71
+ if (items.length < 1)
72
+ nextErrors.push('Cần ít nhất một câu.');
73
+ items.forEach((item, index) => {
74
+ if (item.words.filter(Boolean).length < 2) {
75
+ nextErrors.push(`Câu ${index + 1}: cần ít nhất 2 token (đã xáo).`);
76
+ }
77
+ if (item.answer.filter(Boolean).length < 2) {
78
+ nextErrors.push(`Câu ${index + 1}: cần thứ tự đúng.`);
79
+ }
80
+ const normalizeSet = (tokens) => tokens.map((token) => token.trim().toLowerCase()).filter(Boolean).sort().join('\0');
81
+ if (item.words.filter(Boolean).length >= 2 &&
82
+ item.answer.filter(Boolean).length >= 2 &&
83
+ normalizeSet(item.words) !== normalizeSet(item.answer)) {
84
+ nextErrors.push(`Câu ${index + 1}: thứ tự đúng phải dùng cùng bộ token đã xáo.`);
85
+ }
86
+ });
87
+ setErrors(nextErrors);
88
+ return nextErrors.length === 0;
89
+ };
90
+ useEffect(() => {
91
+ if (validationRef) {
92
+ validationRef.current = {
93
+ triggerValidation: async () => validate(),
94
+ getFormData: () => buildPayload(),
95
+ };
96
+ }
97
+ }, [validationRef, instruction, items, explanation]);
98
+ const updateItem = (index, patch) => {
99
+ setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
100
+ };
101
+ const currentPayload = buildPayload();
102
+ const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
103
+ 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 S\u1EAFp x\u1EBFp t\u1EEB (nh\u00F3m)" }), _jsx("p", { className: "text-sm text-gray-500", children: "M\u1ED7i d\u00F2ng m\u1ED9t c\u00E2u x\u00E1o tr\u1ED9n. Token c\u00E1ch nhau b\u1EB1ng d\u1EA5u ph\u1EA9y." })] }), _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(WordOrderingGroupClient, { questionData: currentPayload }) })] })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base font-semibold", children: "H\u01B0\u1EDBng d\u1EABn" }) }), _jsx(CardContent, { children: _jsx(Input, { value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION }) })] }), _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: () => setItems((prev) => [...prev, createEmptyWordOrderingGroupItem(prev.length + 1)]), children: [_jsx(Plus, { className: "mr-1 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 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-medium", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { 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, {
104
+ points: parsePointsValue(event.target.value, true),
105
+ }) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setItems((prev) => prev.filter((_, i) => i !== index).map((entry, i) => ({ ...entry, questionNumber: i + 1 }))), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Token x\u00E1o tr\u1ED9n" }), _jsx(Input, { className: "mt-1", value: joinTokens(item.words), onChange: (e) => updateItem(index, { words: parseTokens(e.target.value) }), placeholder: "do, you, What, like, subjects, ?" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Th\u1EE9 t\u1EF1 \u0111\u00FAng" }), _jsx(Input, { className: "mt-1", value: joinTokens(item.answer), onChange: (e) => updateItem(index, { answer: parseTokens(e.target.value) }), placeholder: "What, subjects, do, you, like, ?" })] })] }, index))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Gi\u1EA3i th\u00EDch" }) }), _jsx(CardContent, { children: _jsx(Textarea, { value: explanation, onChange: (event) => setExplanation(event.target.value), rows: 3 }) })] })] }))] }));
106
+ }
@@ -0,0 +1,5 @@
1
+ export * from './WordOrderingGroupCreator';
2
+ export * from './WordOrderingGroupClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-word-ordering-group-data';
@@ -0,0 +1,5 @@
1
+ export * from './WordOrderingGroupCreator';
2
+ export * from './WordOrderingGroupClient';
3
+ export * from './transform';
4
+ export * from './register';
5
+ export * from './map-word-ordering-group-data';
@@ -0,0 +1,4 @@
1
+ import { type WordOrderingGroupData, type WordOrderingGroupItemData } from '../../_shared/types/word-ordering-group.type';
2
+ export declare function createEmptyWordOrderingGroupItem(questionNumber: number): WordOrderingGroupItemData;
3
+ export declare function mapWordOrderingGroupItem(item: unknown, index: number, fallbackAnswer?: unknown): WordOrderingGroupItemData;
4
+ export declare function mapQuestionToWordOrderingGroupData(question: Record<string, unknown> | null | undefined): WordOrderingGroupData;
@@ -0,0 +1,74 @@
1
+ import { WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION, } from '../../_shared/types/word-ordering-group.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
+ function asStringList(value) {
12
+ if (!Array.isArray(value))
13
+ return [];
14
+ return value
15
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
16
+ .filter(Boolean);
17
+ }
18
+ export function createEmptyWordOrderingGroupItem(questionNumber) {
19
+ return {
20
+ words: ['', ''],
21
+ answer: ['', ''],
22
+ isExample: false,
23
+ points: 1,
24
+ questionNumber,
25
+ };
26
+ }
27
+ export function mapWordOrderingGroupItem(item, index, fallbackAnswer) {
28
+ const record = asRecord(item);
29
+ const content = asRecord(record.content);
30
+ const correctAnswer = asRecord(record.correctAnswer);
31
+ const isExample = record.isExample === true;
32
+ const words = asStringList(content.words).length
33
+ ? asStringList(content.words)
34
+ : asStringList(record.words);
35
+ const answer = asStringList(correctAnswer.answer).length
36
+ ? asStringList(correctAnswer.answer)
37
+ : asStringList(record.answer).length
38
+ ? asStringList(record.answer)
39
+ : asStringList(fallbackAnswer);
40
+ return {
41
+ words,
42
+ answer,
43
+ isExample,
44
+ points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
45
+ questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
46
+ };
47
+ }
48
+ export function mapQuestionToWordOrderingGroupData(question) {
49
+ const source = asRecord(question);
50
+ const content = asRecord(source.content);
51
+ const meta = asRecord(content.meta);
52
+ const answer = asRecord(source.answer);
53
+ const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
54
+ const explanation = asString(source.explanation) || asString(answer.explanation);
55
+ const fallbackAnswers = Array.isArray(correctAnswer.answer) ? correctAnswer.answer : [];
56
+ if (Array.isArray(content.items) || Array.isArray(answer.items)) {
57
+ const rawItems = Array.isArray(content.items) ? content.items : answer.items;
58
+ const items = rawItems.map((item, index) => mapWordOrderingGroupItem(item, index, fallbackAnswers[index]));
59
+ return {
60
+ instruction: asString(meta.instruction) ||
61
+ asString(answer.instruction) ||
62
+ WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION,
63
+ items,
64
+ explanation,
65
+ points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
66
+ };
67
+ }
68
+ return {
69
+ instruction: WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION,
70
+ items: [createEmptyWordOrderingGroupItem(1)],
71
+ explanation,
72
+ points: 1,
73
+ };
74
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
2
+ export declare const wordOrderingGroupRegistration: QuestionTypeRegistration;
@@ -0,0 +1,36 @@
1
+ import { WordOrderingGroupCreator } from './WordOrderingGroupCreator';
2
+ import { mapQuestionToWordOrderingGroupData } from './map-word-ordering-group-data';
3
+ export const wordOrderingGroupRegistration = {
4
+ component: WordOrderingGroupCreator,
5
+ transformInitialData: (initialData) => mapQuestionToWordOrderingGroupData(initialData),
6
+ getExtraProps: (ctx) => ({
7
+ onUnsavedChangesChange: ctx.onUnsavedChangesChange,
8
+ validationRef: ctx.validationRef,
9
+ }),
10
+ wrapOnSave: (data, ctx) => {
11
+ const payload = data;
12
+ return {
13
+ type: ctx.questionType,
14
+ content: ctx.state.content,
15
+ points: payload.points ?? ctx.state.points ?? 1,
16
+ level: ctx.state.level,
17
+ difficulty: ctx.state.difficulty,
18
+ skill: ctx.state.skill,
19
+ answer: payload,
20
+ explanation: payload.explanation || '',
21
+ };
22
+ },
23
+ wrapOnChange: (data, ctx) => {
24
+ const payload = data;
25
+ return {
26
+ type: ctx.questionType,
27
+ content: ctx.state.content,
28
+ points: payload.points ?? ctx.state.points ?? 1,
29
+ level: ctx.state.level,
30
+ difficulty: ctx.state.difficulty,
31
+ skill: ctx.state.skill,
32
+ answer: payload,
33
+ explanation: payload.explanation || '',
34
+ };
35
+ },
36
+ };
@@ -0,0 +1,2 @@
1
+ import type { TransformHandler } from '../../../../shared/lib/utils/question-transform-types';
2
+ export declare const transformWordOrderingGroup: TransformHandler;
@@ -0,0 +1,33 @@
1
+ import { WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION, WORD_ORDERING_ITEM_TYPE, } from '../../_shared/types/word-ordering-group.type';
2
+ function tokens(value) {
3
+ if (!Array.isArray(value))
4
+ return [];
5
+ return value
6
+ .filter((item) => typeof item === 'string')
7
+ .map((item) => item.trim())
8
+ .filter(Boolean);
9
+ }
10
+ export const transformWordOrderingGroup = (question) => {
11
+ const answerData = question.answer;
12
+ const items = Array.isArray(answerData?.items) ? answerData.items : [];
13
+ const apiItems = items.map((item, index) => ({
14
+ questionType: WORD_ORDERING_ITEM_TYPE,
15
+ ...(item.isExample ? { isExample: true } : {}),
16
+ content: { words: tokens(item.words) },
17
+ correctAnswer: { answer: tokens(item.answer) },
18
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
19
+ questionNumber: item.questionNumber || index + 1,
20
+ }));
21
+ return {
22
+ apiContent: {
23
+ meta: {
24
+ instruction: answerData?.instruction || WORD_ORDERING_GROUP_DEFAULT_INSTRUCTION,
25
+ },
26
+ items: apiItems,
27
+ },
28
+ apiCorrectAnswer: {
29
+ answer: apiItems.map((item) => item.correctAnswer.answer),
30
+ },
31
+ totalPoints: items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0),
32
+ };
33
+ };