@tinyweb_dev/oe-exam-sdk 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +60 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +2 -2
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +58 -14
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.d.ts +13 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +31 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +2 -0
- package/dist/components/exams/take/types.d.ts +12 -5
- package/dist/components/exams/take/utils/answer-transformers.js +17 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +45 -1
- package/dist/components/exams/take/utils/question-transformers.js +109 -53
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.d.ts +50 -0
- package/dist/components/questions/_shared/types/fill-in-blank-group.type.js +2 -0
- package/dist/components/questions/_shared/types/fill-in-blank.type.d.ts +16 -1
- package/dist/components/questions/_shared/types/index.d.ts +2 -0
- package/dist/components/questions/_shared/types/index.js +2 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +40 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +4 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.d.ts +1 -1
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +37 -60
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +122 -149
- package/dist/components/questions/types/fill-in-blank/answer-utils.d.ts +34 -0
- package/dist/components/questions/types/fill-in-blank/answer-utils.js +58 -0
- package/dist/components/questions/types/fill-in-blank/register.js +19 -6
- package/dist/components/questions/types/fill-in-blank/transform.js +23 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +64 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +158 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.d.ts +7 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +30 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +20 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +99 -0
- package/dist/components/questions/types/fill-in-blank-group/index.d.ts +5 -0
- package/dist/components/questions/types/fill-in-blank-group/index.js +5 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.d.ts +4 -0
- package/dist/components/questions/types/fill-in-blank-group/map-fill-in-blank-group-data.js +102 -0
- package/dist/components/questions/types/fill-in-blank-group/register.d.ts +3 -0
- package/dist/components/questions/types/fill-in-blank-group/register.js +37 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.d.ts +2 -0
- package/dist/components/questions/types/fill-in-blank-group/transform.js +68 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.d.ts +20 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +109 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +30 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +180 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +6 -0
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +26 -0
- package/dist/components/questions/types/matching-with-lines-group/index.d.ts +4 -0
- package/dist/components/questions/types/matching-with-lines-group/index.js +4 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.d.ts +8 -0
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +104 -0
- package/dist/components/questions/types/matching-with-lines-group/register.d.ts +3 -0
- package/dist/components/questions/types/matching-with-lines-group/register.js +37 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.d.ts +2 -0
- package/dist/components/questions/types/matching-with-lines-group/transform.js +44 -0
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +14 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.d.ts +7 -0
- package/dist/components/questions/viewer/MatchingWithLinesGroupViewer.js +48 -0
- package/dist/components/questions/viewer/QuestionViewer.js +6 -0
- package/dist/components/questions/viewer/index.d.ts +2 -0
- package/dist/components/questions/viewer/index.js +2 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +2 -3
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +17 -8
- package/dist/components/results/renderers/review-question-body-dedicated.js +2 -2
- package/dist/components/results/renderers/review-question-body-movers.js +37 -2
- package/dist/components/results/review-data.js +37 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +4 -0
- package/dist/shared/lib/utils/fill-in-blank.d.ts +46 -0
- package/dist/shared/lib/utils/fill-in-blank.js +96 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +150 -3
- package/dist/shared/lib/utils/question-transform.js +4 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/fill-in-blank-group.d.ts +45 -0
- package/dist/shared/types/questions/fill-in-blank-group.js +1 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +4 -0
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +39 -0
- package/dist/shared/types/questions/matching-with-lines-group.js +1 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Maps backend question types to frontend renderer props.
|
|
5
5
|
* Each transformer handles specific question type data structure.
|
|
6
6
|
*/
|
|
7
|
+
import { countBlanks, normalizeAcceptedAnswers, parseFillBlankStem, } from '../../../../shared/lib/utils/fill-in-blank';
|
|
7
8
|
/**
|
|
8
9
|
* Group questions by part_id AND questionGroup.id
|
|
9
10
|
* This ensures questions with different passages (different questionGroups)
|
|
@@ -154,55 +155,20 @@ export function transformFillInBlank(questions) {
|
|
|
154
155
|
const content = firstQuestion?.content;
|
|
155
156
|
const groupPayload = firstQuestion?.questionGroup?.payload;
|
|
156
157
|
const transformed = [];
|
|
157
|
-
/** Take UI shows plain textBefore/textAfter — strip `**` so markers never leak. */
|
|
158
|
-
const stripStemBoldMarkers = (s) => s.replace(/\*\*/g, '');
|
|
159
158
|
for (const q of questions) {
|
|
160
159
|
const qContent = q.content;
|
|
161
|
-
const isExample = q.is_example || false;
|
|
162
160
|
// Support both: content.question (new) and content.blanks[0].text (legacy)
|
|
163
161
|
const text = qContent.question || qContent.blanks?.[0]?.text || '';
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const parts = line.split(/\{\d+\}/);
|
|
175
|
-
const textBefore = stripStemBoldMarkers(parts[0]?.trim() || '');
|
|
176
|
-
const textAfter = stripStemBoldMarkers(parts[1]?.trim() || '');
|
|
177
|
-
transformed.push({
|
|
178
|
-
id: `${q.id}-blank-${blankIndex}`,
|
|
179
|
-
questionNumber: q.question_number,
|
|
180
|
-
textBefore,
|
|
181
|
-
textAfter,
|
|
182
|
-
imageUrl: qContent.imageUrl,
|
|
183
|
-
hints: qContent.hints,
|
|
184
|
-
isExample,
|
|
185
|
-
correctAnswer: isExample ? q.correct_answer?.answers?.[blankIndex] : undefined,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
// Single placeholder or no placeholder.
|
|
191
|
-
// Use [\s\S] so stems with newlines still match (`.` does not cross `\n`).
|
|
192
|
-
const match = text.match(/^([\s\S]*?)\{0\}([\s\S]*)$/);
|
|
193
|
-
const textBefore = stripStemBoldMarkers(match ? match[1].trim() : text);
|
|
194
|
-
const textAfter = stripStemBoldMarkers(match ? match[2].trim() : '');
|
|
195
|
-
transformed.push({
|
|
196
|
-
id: q.id,
|
|
197
|
-
questionNumber: q.question_number,
|
|
198
|
-
textBefore,
|
|
199
|
-
textAfter,
|
|
200
|
-
imageUrl: qContent.imageUrl,
|
|
201
|
-
hints: qContent.hints,
|
|
202
|
-
isExample,
|
|
203
|
-
correctAnswer: isExample ? q.correct_answer?.answers?.[0] : undefined,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
162
|
+
const segments = parseFillBlankStem(text);
|
|
163
|
+
transformed.push({
|
|
164
|
+
id: q.id,
|
|
165
|
+
questionNumber: q.question_number,
|
|
166
|
+
segments,
|
|
167
|
+
acceptedAnswers: normalizeAcceptedAnswers(q.correct_answer?.answers, countBlanks(segments)),
|
|
168
|
+
imageUrl: qContent.imageUrl,
|
|
169
|
+
hints: qContent.hints,
|
|
170
|
+
isExample: q.is_example || false,
|
|
171
|
+
});
|
|
206
172
|
}
|
|
207
173
|
return {
|
|
208
174
|
questions: transformed,
|
|
@@ -228,17 +194,17 @@ export function transformWriteCorrectVerbForm(questions) {
|
|
|
228
194
|
const content = firstQuestion?.content;
|
|
229
195
|
const transformed = questions.map((q) => {
|
|
230
196
|
const qContent = q.content;
|
|
231
|
-
const
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
197
|
+
const segments = parseFillBlankStem(qContent.sentence || '');
|
|
198
|
+
// Stored as { correctAnswer: "doesn't like" }; older rows used { answers: { 'blank-1': … } }.
|
|
199
|
+
const correct = q.correct_answer;
|
|
200
|
+
const rawAccepted = Array.isArray(correct?.answers)
|
|
201
|
+
? correct.answers
|
|
202
|
+
: (correct?.answers?.['blank-1'] ?? correct?.correctAnswer ?? correct?.answer);
|
|
237
203
|
return {
|
|
238
204
|
id: q.id,
|
|
239
205
|
questionNumber: q.question_number,
|
|
240
|
-
|
|
241
|
-
|
|
206
|
+
segments,
|
|
207
|
+
acceptedAnswers: normalizeAcceptedAnswers(rawAccepted, countBlanks(segments)),
|
|
242
208
|
verbHint: qContent.verbHint, // Pass verbHint per question
|
|
243
209
|
};
|
|
244
210
|
});
|
|
@@ -962,6 +928,63 @@ export function transformTrueFalseCorrectGroup(questions) {
|
|
|
962
928
|
|| 'Tick (✓) the true and cross (✗) the false sentences. Correct the false sentences.',
|
|
963
929
|
};
|
|
964
930
|
}
|
|
931
|
+
function parseFillInBlankGroupAnswer(raw) {
|
|
932
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
const obj = raw;
|
|
936
|
+
if (!Array.isArray(obj.answers)) {
|
|
937
|
+
return undefined;
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
answers: obj.answers.map((blank) => (Array.isArray(blank)
|
|
941
|
+
? blank.map((alt) => String(alt ?? '').trim()).filter(Boolean)
|
|
942
|
+
: [String(blank ?? '').trim()].filter(Boolean))),
|
|
943
|
+
caseSensitive: obj.caseSensitive === true,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function blankCountFromMatches(matches) {
|
|
947
|
+
const indices = matches.map((match) => Number.parseInt(match[1], 10));
|
|
948
|
+
return Math.max(...indices, 0) + 1;
|
|
949
|
+
}
|
|
950
|
+
export function transformFillInBlankGroup(questions) {
|
|
951
|
+
const parent = questions[0];
|
|
952
|
+
const content = parent?.content;
|
|
953
|
+
const items = Array.isArray(content?.items) ? content.items : [];
|
|
954
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
955
|
+
? parent.correct_answer.answer
|
|
956
|
+
: [];
|
|
957
|
+
const groupAudioUrl = content?.meta?.audioUrl?.trim() || '';
|
|
958
|
+
const transformed = items.map((item, index) => {
|
|
959
|
+
const parsed = parseFillInBlankGroupAnswer(item.correctAnswer ?? parentCorrect[index]);
|
|
960
|
+
const question = item.content?.question || '{0}';
|
|
961
|
+
const blankMatches = [...question.matchAll(/\{(\d+)\}/g)];
|
|
962
|
+
const blankCount = blankMatches.length > 0 ? blankCountFromMatches(blankMatches) : 1;
|
|
963
|
+
const isExample = item.isExample === true;
|
|
964
|
+
return {
|
|
965
|
+
id: `${parent.id}-item-${index}`,
|
|
966
|
+
questionNumber: item.questionNumber || index + 1,
|
|
967
|
+
question,
|
|
968
|
+
imageUrl: item.content?.imageUrl,
|
|
969
|
+
audioUrl: groupAudioUrl ? undefined : item.content?.audioUrl,
|
|
970
|
+
isExample,
|
|
971
|
+
blankCount,
|
|
972
|
+
exampleAnswers: isExample
|
|
973
|
+
? Array.from({ length: blankCount }, (_, slot) => parsed?.answers?.[slot]?.[0] || '')
|
|
974
|
+
: undefined,
|
|
975
|
+
};
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
questions: transformed,
|
|
979
|
+
instruction: content?.meta?.instruction || 'Complete the sentences.',
|
|
980
|
+
audioUrl: groupAudioUrl || undefined,
|
|
981
|
+
imageUrl: content?.meta?.imageUrl,
|
|
982
|
+
showHints: content?.meta?.showHints === true,
|
|
983
|
+
hints: Array.isArray(content?.meta?.hints)
|
|
984
|
+
? content.meta.hints.map((hint) => String(hint ?? '').trim()).filter(Boolean)
|
|
985
|
+
: [],
|
|
986
|
+
};
|
|
987
|
+
}
|
|
965
988
|
export function transformWordOrdering(questions) {
|
|
966
989
|
const transformed = questions.map((q) => {
|
|
967
990
|
const content = q.content;
|
|
@@ -1326,3 +1349,36 @@ export function transformFindWordsInMatrix(questions) {
|
|
|
1326
1349
|
explanation: question?.explanation,
|
|
1327
1350
|
};
|
|
1328
1351
|
}
|
|
1352
|
+
export function transformMatchingWithLinesGroup(questions) {
|
|
1353
|
+
const parent = questions[0];
|
|
1354
|
+
const content = (parent?.content || {});
|
|
1355
|
+
const items = Array.isArray(content.items) ? content.items : [];
|
|
1356
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
1357
|
+
? parent.correct_answer.answer
|
|
1358
|
+
: [];
|
|
1359
|
+
const rightItems = Array.isArray(content.rightItems)
|
|
1360
|
+
? content.rightItems.map((item, index) => ({
|
|
1361
|
+
id: typeof item?.id === 'string' ? item.id : `r${index + 1}`,
|
|
1362
|
+
text: typeof item?.text === 'string' ? item.text : '',
|
|
1363
|
+
}))
|
|
1364
|
+
: [];
|
|
1365
|
+
return {
|
|
1366
|
+
questions: items.map((item, index) => {
|
|
1367
|
+
const isExample = item.isExample === true;
|
|
1368
|
+
const fromItem = typeof item.correctAnswer?.answer === 'string' ? item.correctAnswer.answer : '';
|
|
1369
|
+
const fromParent = typeof parentCorrect[index] === 'string' ? parentCorrect[index] : '';
|
|
1370
|
+
return {
|
|
1371
|
+
id: `${parent?.id || 'q'}-item-${index}`,
|
|
1372
|
+
questionNumber: item.questionNumber || index + 1,
|
|
1373
|
+
imageUrl: item.content?.imageUrl || '',
|
|
1374
|
+
correctRightId: fromItem || fromParent,
|
|
1375
|
+
isExample,
|
|
1376
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
1377
|
+
};
|
|
1378
|
+
}),
|
|
1379
|
+
rightItems,
|
|
1380
|
+
title: content.meta?.title || '',
|
|
1381
|
+
instruction: content.meta?.instruction || 'Look and match.',
|
|
1382
|
+
explanation: parent?.explanation,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
@@ -177,6 +177,11 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
177
177
|
requiresContent: true,
|
|
178
178
|
hasDedicatedComponent: true,
|
|
179
179
|
},
|
|
180
|
+
FILL_IN_BLANK_GROUP: {
|
|
181
|
+
label: 'Điền chỗ trống (nhóm)',
|
|
182
|
+
requiresContent: true,
|
|
183
|
+
hasDedicatedComponent: true,
|
|
184
|
+
},
|
|
180
185
|
CROSS_OUT_WORD_GROUP: {
|
|
181
186
|
label: 'Cross out word (group)',
|
|
182
187
|
requiresContent: true,
|
|
@@ -237,6 +242,11 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
237
242
|
requiresContent: true,
|
|
238
243
|
hasDedicatedComponent: true,
|
|
239
244
|
},
|
|
245
|
+
MATCHING_WITH_LINES_GROUP: {
|
|
246
|
+
label: 'Nối bằng đường kẻ (nhóm)',
|
|
247
|
+
requiresContent: true,
|
|
248
|
+
hasDedicatedComponent: true,
|
|
249
|
+
},
|
|
240
250
|
WRITE_SENTENCES: {
|
|
241
251
|
label: 'Viết câu',
|
|
242
252
|
requiresContent: true,
|
|
@@ -354,6 +364,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
354
364
|
'CHOOSE_THE_CORRECT_ANSWER_GROUP',
|
|
355
365
|
'TRUE_FALSE_GROUP',
|
|
356
366
|
'TRUE_FALSE_CORRECT_GROUP',
|
|
367
|
+
'FILL_IN_BLANK_GROUP',
|
|
357
368
|
'CROSS_OUT_WORD_GROUP',
|
|
358
369
|
'WRITE_A_SHORT_LETTER',
|
|
359
370
|
'WRITE_CORRECT_VERB_FORM',
|
|
@@ -365,6 +376,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
365
376
|
'WORD_FILL_PARAGRAPH',
|
|
366
377
|
'MATCH_BY_WRITING_ANSWER',
|
|
367
378
|
'MATCH_WORD_TO_PICTURE',
|
|
379
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
368
380
|
'WRITE_SENTENCES',
|
|
369
381
|
'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP',
|
|
370
382
|
'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const FILL_IN_BLANK_ITEM_TYPE: "FILL_IN_BLANK";
|
|
3
|
+
export declare const FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION = "Complete the sentences.";
|
|
4
|
+
export interface FillInBlankGroupItemAnswer {
|
|
5
|
+
answers: string[][];
|
|
6
|
+
caseSensitive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface FillInBlankGroupStudentItemAnswer {
|
|
9
|
+
answers: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface FillInBlankGroupItemData {
|
|
12
|
+
question: string;
|
|
13
|
+
answers: string[][];
|
|
14
|
+
caseSensitive?: boolean;
|
|
15
|
+
imageUrl?: string;
|
|
16
|
+
audioUrl?: string;
|
|
17
|
+
showHints?: boolean;
|
|
18
|
+
hints?: string[];
|
|
19
|
+
isExample?: boolean;
|
|
20
|
+
points: number;
|
|
21
|
+
questionNumber: number;
|
|
22
|
+
}
|
|
23
|
+
export interface FillInBlankGroupData {
|
|
24
|
+
instruction: string;
|
|
25
|
+
audioUrl?: string;
|
|
26
|
+
imageUrl?: string;
|
|
27
|
+
showHints?: boolean;
|
|
28
|
+
hints?: string[];
|
|
29
|
+
items: FillInBlankGroupItemData[];
|
|
30
|
+
explanation?: string;
|
|
31
|
+
points?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface FillInBlankGroupCreatorProps {
|
|
34
|
+
initialData?: FillInBlankGroupData;
|
|
35
|
+
onSave?: (data: FillInBlankGroupData) => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
onChange?: (data: FillInBlankGroupData) => void;
|
|
38
|
+
externalErrors?: string[];
|
|
39
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
40
|
+
validationRef?: MutableRefObject<{
|
|
41
|
+
triggerValidation: () => Promise<boolean>;
|
|
42
|
+
getFormData?: () => FillInBlankGroupData;
|
|
43
|
+
} | null>;
|
|
44
|
+
}
|
|
45
|
+
export interface FillInBlankGroupClientProps {
|
|
46
|
+
questionData: FillInBlankGroupData;
|
|
47
|
+
isReviewMode?: boolean;
|
|
48
|
+
userAnswers?: FillInBlankGroupStudentItemAnswer[];
|
|
49
|
+
onAnswerChange?: (answers: FillInBlankGroupStudentItemAnswer[]) => void;
|
|
50
|
+
}
|
|
@@ -3,20 +3,32 @@ export type { ValidationRef };
|
|
|
3
3
|
export interface BlankItem {
|
|
4
4
|
id: string;
|
|
5
5
|
text: string;
|
|
6
|
+
/** @deprecated Prefer correctAnswers — kept for legacy load */
|
|
6
7
|
correctAnswer: string;
|
|
8
|
+
/** Equivalent correct answers for this blank */
|
|
9
|
+
correctAnswers?: string[];
|
|
7
10
|
}
|
|
8
11
|
export interface FillInBlankGroup {
|
|
9
12
|
number: number;
|
|
10
13
|
questionCount: number;
|
|
11
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Main data interface - supports multiple blanks in a single question
|
|
17
|
+
* Question format: "He {0} 10 years old and she {1} 12."
|
|
18
|
+
* Answers format (canonical): [["is", "'s"], ["are"]]
|
|
19
|
+
* - answers[i] = list of equivalent correct answers for placeholder {i}
|
|
20
|
+
* Legacy format still accepted when loading: ["is", "are"] → [["is"], ["are"]]
|
|
21
|
+
*/
|
|
12
22
|
export interface FillInBlankData {
|
|
13
23
|
question: string;
|
|
14
|
-
answers
|
|
24
|
+
/** Per-blank list of equivalent correct answers */
|
|
25
|
+
answers: string[][];
|
|
15
26
|
caseSensitive?: boolean;
|
|
16
27
|
explanation?: string;
|
|
17
28
|
points?: number;
|
|
18
29
|
isExample?: boolean;
|
|
19
30
|
imageUrl?: string;
|
|
31
|
+
audioUrl?: string;
|
|
20
32
|
showHints?: boolean;
|
|
21
33
|
hints?: string[];
|
|
22
34
|
isGroup?: boolean;
|
|
@@ -41,9 +53,12 @@ export interface FillInBlankCreatorProps {
|
|
|
41
53
|
groups?: FillInBlankGroup[];
|
|
42
54
|
questionIndexInPart?: number;
|
|
43
55
|
partId?: string;
|
|
56
|
+
/** Template questionConfig — controls which fields are shown (e.g. "audio") */
|
|
57
|
+
questionConfig?: string[];
|
|
44
58
|
}
|
|
45
59
|
export interface FillInBlankClientProps {
|
|
46
60
|
questionData: FillInBlankData;
|
|
61
|
+
questionConfig?: string[];
|
|
47
62
|
onSubmit?: (answers: string[]) => void;
|
|
48
63
|
isReviewMode?: boolean;
|
|
49
64
|
userAnswer?: string[];
|
|
@@ -12,5 +12,7 @@ export * from './fill-in-blank.type';
|
|
|
12
12
|
export * from './read-passage-and-answer-questions.type';
|
|
13
13
|
export * from './word-fill-structured-form.type';
|
|
14
14
|
export * from './true-false-correct-group.type';
|
|
15
|
+
export * from './fill-in-blank-group.type';
|
|
15
16
|
export * from './match-word-to-picture.type';
|
|
17
|
+
export * from './matching-with-lines-group.type';
|
|
16
18
|
export * from './spontaneous-qa-group.type';
|
|
@@ -12,5 +12,7 @@ export * from './fill-in-blank.type';
|
|
|
12
12
|
export * from './read-passage-and-answer-questions.type';
|
|
13
13
|
export * from './word-fill-structured-form.type';
|
|
14
14
|
export * from './true-false-correct-group.type';
|
|
15
|
+
export * from './fill-in-blank-group.type';
|
|
15
16
|
export * from './match-word-to-picture.type';
|
|
17
|
+
export * from './matching-with-lines-group.type';
|
|
16
18
|
export * from './spontaneous-qa-group.type';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export declare const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = "Look and match.";
|
|
3
|
+
export interface MatchingWithLinesRightItem {
|
|
4
|
+
id: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MatchingWithLinesGroupItemData {
|
|
8
|
+
imageUrl: string;
|
|
9
|
+
correctAnswer: string;
|
|
10
|
+
isExample?: boolean;
|
|
11
|
+
points: number;
|
|
12
|
+
questionNumber: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MatchingWithLinesGroupData {
|
|
15
|
+
title?: string;
|
|
16
|
+
instruction: string;
|
|
17
|
+
rightItems: MatchingWithLinesRightItem[];
|
|
18
|
+
items: MatchingWithLinesGroupItemData[];
|
|
19
|
+
explanation?: string;
|
|
20
|
+
points?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface MatchingWithLinesGroupCreatorProps {
|
|
23
|
+
initialData?: MatchingWithLinesGroupData;
|
|
24
|
+
onSave?: (data: MatchingWithLinesGroupData) => void;
|
|
25
|
+
onCancel?: () => void;
|
|
26
|
+
onChange?: (data: MatchingWithLinesGroupData) => void;
|
|
27
|
+
externalErrors?: string[];
|
|
28
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
29
|
+
validationRef?: MutableRefObject<{
|
|
30
|
+
triggerValidation: () => Promise<boolean>;
|
|
31
|
+
getFormData?: () => MatchingWithLinesGroupData;
|
|
32
|
+
} | null>;
|
|
33
|
+
}
|
|
34
|
+
export interface MatchingWithLinesGroupClientProps {
|
|
35
|
+
questionData: MatchingWithLinesGroupData;
|
|
36
|
+
isReviewMode?: boolean;
|
|
37
|
+
userAnswers?: string[];
|
|
38
|
+
onAnswerChange?: (index: number, rightId: string) => void;
|
|
39
|
+
onAnswersChange?: (next: string[]) => void;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION = 'Look and match.';
|
|
@@ -23,11 +23,13 @@ const IMAGE_BASED_TYPES = [
|
|
|
23
23
|
'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER',
|
|
24
24
|
'LOOK_PICTURE_FILL_WORD_HINT',
|
|
25
25
|
'FILL_IN_BLANK',
|
|
26
|
+
'FILL_IN_BLANK_GROUP',
|
|
26
27
|
'LABEL_THE_PICTURE',
|
|
27
28
|
'READ_AND_COLOR_OBJECTS',
|
|
28
29
|
'IMAGE_OBJECT_MATCHING',
|
|
29
30
|
'FILL_IN_BLANK',
|
|
30
31
|
'MATCH_WORD_TO_PICTURE',
|
|
32
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
31
33
|
];
|
|
32
34
|
// Passage-based question types
|
|
33
35
|
const PASSAGE_BASED_TYPES = [
|
|
@@ -62,8 +64,10 @@ const getQuestionTypeName = (type) => {
|
|
|
62
64
|
'IMAGE_OBJECT_MATCHING': 'Ghép hình ảnh - Đối tượng',
|
|
63
65
|
'WRITE_CORRECT_VERB_FORM': 'Viết dạng động từ',
|
|
64
66
|
'FILL_IN_BLANK': 'Điền vào chỗ trống',
|
|
67
|
+
'FILL_IN_BLANK_GROUP': 'Điền chỗ trống (nhóm)',
|
|
65
68
|
'READ_PASSAGE_AND_ANSWER_QUESTIONS': 'Đọc hiểu',
|
|
66
69
|
'MATCH_WORD_TO_PICTURE': 'Nối từ với tranh',
|
|
70
|
+
'MATCHING_WITH_LINES_GROUP': 'Nối bằng đường kẻ (nhóm)',
|
|
67
71
|
};
|
|
68
72
|
return type ? typeMap[type] || type.replace(/_/g, ' ').toLowerCase() : 'Chưa xác định';
|
|
69
73
|
};
|
|
@@ -27,7 +27,9 @@ export function QuestionTypeSelector({ value, onValueChange, disabled, }) {
|
|
|
27
27
|
'WORD_ORDERING',
|
|
28
28
|
'MATCH_BY_WRITING_ANSWER',
|
|
29
29
|
'MATCH_WORD_TO_PICTURE',
|
|
30
|
+
'MATCHING_WITH_LINES_GROUP',
|
|
30
31
|
'WRITE_SENTENCES',
|
|
32
|
+
'FILL_IN_BLANK_GROUP',
|
|
31
33
|
];
|
|
32
34
|
return (_jsxs(Select, { value: value, onValueChange: onValueChange, disabled: disabled, children: [_jsx(SelectTrigger, { id: "questionType", className: "mt-1", children: _jsx(SelectValue, { placeholder: "Ch\u1ECDn lo\u1EA1i c\u00E2u h\u1ECFi" }) }), _jsx(SelectContent, { children: questionTypes.map((type) => (_jsx(SelectItem, { value: type, children: getQuestionTypeLabel(type) }, type))) })] }));
|
|
33
35
|
}
|
|
@@ -14,6 +14,7 @@ import { chooseTheCorrectAnswerRegistration } from '../types/choose-the-correct-
|
|
|
14
14
|
import { chooseTheCorrectAnswerGroupRegistration } from '../types/choose-the-correct-answer-group/register';
|
|
15
15
|
import { trueFalseGroupRegistration } from '../types/true-false-group/register';
|
|
16
16
|
import { trueFalseCorrectGroupRegistration } from '../types/true-false-correct-group/register';
|
|
17
|
+
import { fillInBlankGroupRegistration } from '../types/fill-in-blank-group/register';
|
|
17
18
|
import { crossOutWordGroupRegistration } from '../types/cross-out-word-group/register';
|
|
18
19
|
import { writeAShortLetterRegistration } from '../types/write-a-short-letter/register';
|
|
19
20
|
import { writeCorrectVerbFormRegistration } from '../types/write-correct-verb-form/register';
|
|
@@ -25,6 +26,7 @@ import { wordOrderingRegistration } from '../types/word-ordering/register';
|
|
|
25
26
|
import { wordFillParagraphRegistration } from '../types/word-fill-paragraph/register';
|
|
26
27
|
import { matchByWritingAnswerRegistration } from '../types/match-by-writing-answer/register';
|
|
27
28
|
import { matchWordToPictureRegistration } from '../types/match-word-to-picture/register';
|
|
29
|
+
import { matchingWithLinesGroupRegistration } from '../types/matching-with-lines-group/register';
|
|
28
30
|
import { writeSentencesRegistration } from '../types/write-sentences/register';
|
|
29
31
|
import { listenAndChooseFromAnswerGroupRegistration } from '../types/listen-and-choose-from-answer-group/register';
|
|
30
32
|
import { listenAndChooseObjectsInSceneRegistration } from '../types/listen-and-choose-objects-in-scene/register';
|
|
@@ -53,6 +55,7 @@ export const questionTypeRegistry = {
|
|
|
53
55
|
CHOOSE_THE_CORRECT_ANSWER_GROUP: chooseTheCorrectAnswerGroupRegistration,
|
|
54
56
|
TRUE_FALSE_GROUP: trueFalseGroupRegistration,
|
|
55
57
|
TRUE_FALSE_CORRECT_GROUP: trueFalseCorrectGroupRegistration,
|
|
58
|
+
FILL_IN_BLANK_GROUP: fillInBlankGroupRegistration,
|
|
56
59
|
CROSS_OUT_WORD_GROUP: crossOutWordGroupRegistration,
|
|
57
60
|
WORD_ORDER_AND_MATCH_GROUP: wordOrderAndMatchGroupRegistration,
|
|
58
61
|
FILL_IN_BLANK: fillInBlankRegistration,
|
|
@@ -76,6 +79,7 @@ export const questionTypeRegistry = {
|
|
|
76
79
|
WORD_FILL_PARAGRAPH: wordFillParagraphRegistration, // Will need to handle viewMode mapping either here or in wrapper
|
|
77
80
|
MATCH_BY_WRITING_ANSWER: matchByWritingAnswerRegistration,
|
|
78
81
|
MATCH_WORD_TO_PICTURE: matchWordToPictureRegistration,
|
|
82
|
+
MATCHING_WITH_LINES_GROUP: matchingWithLinesGroupRegistration,
|
|
79
83
|
WRITE_SENTENCES: writeSentencesRegistration,
|
|
80
84
|
LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP: listenAndChooseFromAnswerGroupRegistration,
|
|
81
85
|
LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE: listenAndChooseObjectsInSceneRegistration,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { FillInBlankClientProps } from '../../_shared/types/fill-in-blank.type';
|
|
2
|
-
export declare function FillInBlankClient({ questionData, onSubmit, isReviewMode, userAnswer, autoFillCorrectAnswers, explanation: explanationProp, }: FillInBlankClientProps): import("react").JSX.Element;
|
|
2
|
+
export declare function FillInBlankClient({ questionData, questionConfig, onSubmit, isReviewMode, userAnswer, autoFillCorrectAnswers, explanation: explanationProp, }: FillInBlankClientProps): import("react").JSX.Element;
|