@tinyweb_dev/oe-exam-sdk 0.1.30 → 0.1.32
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/ExamPreviewDialog.d.ts +9 -3
- package/dist/components/exams/ExamPreviewDialog.js +23 -5
- package/dist/components/exams/ExamPreviewPartContent.d.ts +2 -1
- package/dist/components/exams/ExamPreviewPartContent.js +3 -2
- package/dist/components/exams/ExamPreviewSidebar.d.ts +3 -1
- package/dist/components/exams/ExamPreviewSidebar.js +7 -2
- package/dist/components/exams/exam-preview.mode.d.ts +4 -0
- package/dist/components/exams/exam-preview.mode.js +5 -0
- package/dist/components/exams/index.d.ts +1 -0
- package/dist/components/exams/index.js +1 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts +3 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +285 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/types.d.ts +30 -0
- package/dist/components/exams/take/utils/answer-transformers.js +37 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +28 -1
- package/dist/components/exams/take/utils/question-transformers.js +69 -0
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +32 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +1 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +16 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -1
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
- package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +183 -0
- package/dist/components/questions/types/answer-the-question-group/index.d.ts +5 -0
- package/dist/components/questions/types/answer-the-question-group/index.js +5 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.d.ts +4 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +83 -0
- package/dist/components/questions/types/answer-the-question-group/register.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/register.js +36 -0
- package/dist/components/questions/types/answer-the-question-group/transform.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +72 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +11 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +69 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.d.ts +8 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +213 -0
- package/dist/components/questions/types/crossword-puzzle/index.d.ts +4 -0
- package/dist/components/questions/types/crossword-puzzle/index.js +4 -0
- package/dist/components/questions/types/crossword-puzzle/register.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/register.js +29 -0
- package/dist/components/questions/types/crossword-puzzle/transform.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/transform.js +75 -0
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -1
- package/dist/components/questions/viewer/QuestionViewer.js +9 -6
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -4
- package/dist/components/results/ReviewQuestionRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +19 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +27 -0
- package/dist/components/results/renderers/review-question-body-dedicated.d.ts +1 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +47 -2
- package/dist/components/results/renderers/review-question-body-movers.js +7 -2
- package/dist/components/results/review-data.js +20 -4
- 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/i18n/messages/en/admin.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/en/admin.js +5 -0
- package/dist/shared/lib/i18n/messages/en.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/vi/admin.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/vi/admin.js +5 -0
- package/dist/shared/lib/i18n/messages/vi.d.ts +5 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +93 -0
- package/dist/shared/lib/utils/question-transform-validation.js +12 -0
- 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/answer-the-question-group.d.ts +39 -0
- package/dist/shared/types/questions/answer-the-question-group.js +1 -0
- package/dist/shared/types/questions/crossword-puzzle.d.ts +50 -0
- package/dist/shared/types/questions/crossword-puzzle.js +10 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +4 -0
- package/package.json +1 -1
|
@@ -88,7 +88,8 @@ function findParentQuestion(subQuestionId, parts) {
|
|
|
88
88
|
part.questionType === 'CROSS_OUT_WORD_GROUP' ||
|
|
89
89
|
part.questionType === 'TRUE_FALSE_GROUP' ||
|
|
90
90
|
part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
|
|
91
|
-
part.questionType === 'SPONTANEOUS_QA_GROUP'
|
|
91
|
+
part.questionType === 'SPONTANEOUS_QA_GROUP' ||
|
|
92
|
+
part.questionType === 'ANSWER_THE_QUESTION_GROUP') {
|
|
92
93
|
const parent = part.questions.find((question) => subQuestionId.startsWith(`${question.id}-item-`));
|
|
93
94
|
if (parent) {
|
|
94
95
|
return {
|
|
@@ -105,7 +106,7 @@ function findParentQuestion(subQuestionId, parts) {
|
|
|
105
106
|
*/
|
|
106
107
|
function findGridParentQuestion(parts) {
|
|
107
108
|
for (const part of parts) {
|
|
108
|
-
if (part.questionType === 'FILL_MISSING_WORDS_IN_GRID') {
|
|
109
|
+
if (part.questionType === 'FILL_MISSING_WORDS_IN_GRID' || part.questionType === 'CROSSWORD_PUZZLE') {
|
|
109
110
|
if (part.questions.length > 0) {
|
|
110
111
|
return {
|
|
111
112
|
parentId: part.questions[0].id,
|
|
@@ -416,10 +417,40 @@ export function transformAnswersForSubmission(answers, parts) {
|
|
|
416
417
|
}
|
|
417
418
|
// Add grid cell answers (grouped under parent question)
|
|
418
419
|
for (const [parentId, cellAnswers] of gridCellAnswers) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
420
|
+
const parentPart = parts.find((p) => p.questions.some((q) => q.id === parentId));
|
|
421
|
+
if (parentPart?.questionType === 'CROSSWORD_PUZZLE') {
|
|
422
|
+
const q = parentPart.questions.find((x) => x.id === parentId);
|
|
423
|
+
const content = q?.content;
|
|
424
|
+
const words = {};
|
|
425
|
+
if (content?.clues) {
|
|
426
|
+
const across = content.clues.across || [];
|
|
427
|
+
const down = content.clues.down || [];
|
|
428
|
+
for (const c of across) {
|
|
429
|
+
let word = '';
|
|
430
|
+
for (let i = 0; i < (c.length || 1); i++) {
|
|
431
|
+
word += (cellAnswers[`${c.startRow}-${c.startCol + i}`] || '').trim();
|
|
432
|
+
}
|
|
433
|
+
words[`across-${c.number}`] = word;
|
|
434
|
+
}
|
|
435
|
+
for (const c of down) {
|
|
436
|
+
let word = '';
|
|
437
|
+
for (let i = 0; i < (c.length || 1); i++) {
|
|
438
|
+
word += (cellAnswers[`${c.startRow + i}-${c.startCol}`] || '').trim();
|
|
439
|
+
}
|
|
440
|
+
words[`down-${c.number}`] = word;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
result.push({
|
|
444
|
+
questionId: parentId,
|
|
445
|
+
answer: { answers: words, cells: cellAnswers },
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
result.push({
|
|
450
|
+
questionId: parentId,
|
|
451
|
+
answer: { answers: cellAnswers },
|
|
452
|
+
});
|
|
453
|
+
}
|
|
423
454
|
}
|
|
424
455
|
// Add label answers (grouped under parent question)
|
|
425
456
|
for (const [parentId, labels] of labelAnswers) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Each transformer handles specific question type data structure.
|
|
6
6
|
*/
|
|
7
7
|
import type { FillBlankQuestion, ChooseBestAnswerQuestion, PictureChooseSubQuestion, PictureFillBlankSubQuestion, ReadingPassageQuestion, LabelThePictureLabel, FillWordHintSubQuestion, ColoringHintItem, ColoringRegion } from '../types';
|
|
8
|
-
export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
8
|
+
export type QuestionType = 'FILL_IN_BLANK' | 'WRITE_CORRECT_VERB_FORM' | 'CHOOSE_THE_CORRECT_ANSWER' | 'CHOOSE_THE_CORRECT_ANSWER_GROUP' | 'CHOOSE_CORRECT_ADJECTIVE' | 'WRITE_A_SHORT_LETTER' | 'READ_AND_COLOR_OBJECTS' | 'LOOK_PICTURE_CHOOSE_CORRECT_ANSWER' | 'LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'LOOK_PICTURE_FILL_WORD_HINT' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'WORD_ORDER_AND_MATCH_GROUP' | 'LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE' | 'LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP' | 'LISTEN_AND_DRAG_OBJECTS_INTO_SCENE' | 'LISTEN_AND_SPEAK_ANSWER' | 'LISTEN_AND_SPEAK_COMPARE_IMAGES' | 'LISTEN_AND_SPEAK_WITH_STORY_IMAGES' | 'LISTEN_AND_SPEAK_IMAGE_GROUP' | 'LISTEN_AND_SPEAK_ODD_ONE_OUT' | 'LISTEN_AND_SPEAK_QUESTION_LIST' | 'LISTEN_AND_SPEAK_INFO_EXCHANGE' | 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | 'SPONTANEOUS_QA_GROUP' | 'SPEAKING_CONVERSATION' | 'GN_SPEAKING_INTERVIEW' | 'SPEAKING_CUE_CARD';
|
|
9
9
|
export interface ApiQuestion {
|
|
10
10
|
id: string;
|
|
11
11
|
content: any;
|
|
@@ -244,6 +244,11 @@ export declare function transformAnswerTheQuestion(questions: ApiQuestion[]): {
|
|
|
244
244
|
title?: string;
|
|
245
245
|
groupContent?: string;
|
|
246
246
|
};
|
|
247
|
+
export declare function transformAnswerTheQuestionGroup(questions: ApiQuestion[]): {
|
|
248
|
+
questions: AnswerTheQuestionQuestion[];
|
|
249
|
+
instruction: string;
|
|
250
|
+
groupContent?: string;
|
|
251
|
+
};
|
|
247
252
|
export interface TrueFalseQuestion {
|
|
248
253
|
id: string;
|
|
249
254
|
questionNumber: number;
|
|
@@ -410,3 +415,25 @@ export interface WordOrderAndMatchGroupPartData {
|
|
|
410
415
|
explanation?: string;
|
|
411
416
|
}
|
|
412
417
|
export declare function transformWordOrderAndMatchGroup(questions: ApiQuestion[]): WordOrderAndMatchGroupPartData;
|
|
418
|
+
export interface CrosswordClueItem {
|
|
419
|
+
number: number;
|
|
420
|
+
clue: string;
|
|
421
|
+
startRow: number;
|
|
422
|
+
startCol: number;
|
|
423
|
+
length: number;
|
|
424
|
+
}
|
|
425
|
+
export interface CrosswordPuzzlePartData {
|
|
426
|
+
title?: string;
|
|
427
|
+
instruction: string;
|
|
428
|
+
gridSize: {
|
|
429
|
+
rows: number;
|
|
430
|
+
cols: number;
|
|
431
|
+
};
|
|
432
|
+
clues: {
|
|
433
|
+
across: CrosswordClueItem[];
|
|
434
|
+
down: CrosswordClueItem[];
|
|
435
|
+
};
|
|
436
|
+
correctAnswers: Record<string, string>;
|
|
437
|
+
explanation?: string;
|
|
438
|
+
}
|
|
439
|
+
export declare function transformCrosswordPuzzle(questions: ApiQuestion[]): CrosswordPuzzlePartData;
|
|
@@ -804,6 +804,39 @@ export function transformAnswerTheQuestion(questions) {
|
|
|
804
804
|
groupContent: groupPayload?.groupContent || firstContent?.groupContent,
|
|
805
805
|
};
|
|
806
806
|
}
|
|
807
|
+
// ============================================
|
|
808
|
+
// ANSWER_THE_QUESTION_GROUP Transformer
|
|
809
|
+
// ============================================
|
|
810
|
+
export function transformAnswerTheQuestionGroup(questions) {
|
|
811
|
+
const parent = questions[0];
|
|
812
|
+
const content = parent?.content;
|
|
813
|
+
const items = Array.isArray(content?.items) ? content.items : [];
|
|
814
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
815
|
+
? parent.correct_answer.answer
|
|
816
|
+
: [];
|
|
817
|
+
const transformed = items.map((item, itemIndex) => {
|
|
818
|
+
const isExample = item.isExample === true;
|
|
819
|
+
const fallbackAnswers = Array.isArray(parentCorrect[itemIndex]?.expectedAnswers)
|
|
820
|
+
? parentCorrect[itemIndex].expectedAnswers
|
|
821
|
+
: [];
|
|
822
|
+
const nestedAnswers = Array.isArray(item.correctAnswer?.expectedAnswers)
|
|
823
|
+
? item.correctAnswer.expectedAnswers
|
|
824
|
+
: [];
|
|
825
|
+
const exampleAnswer = (nestedAnswers[0] || fallbackAnswers[0] || '');
|
|
826
|
+
return {
|
|
827
|
+
id: `${parent.id}-item-${itemIndex}`,
|
|
828
|
+
questionNumber: item.questionNumber || itemIndex + 1,
|
|
829
|
+
question: item.content?.question || '',
|
|
830
|
+
isExample,
|
|
831
|
+
exampleAnswer: isExample ? exampleAnswer : undefined,
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
return {
|
|
835
|
+
questions: transformed,
|
|
836
|
+
instruction: content?.meta?.instruction || 'Read the text again and answer the questions.',
|
|
837
|
+
groupContent: content?.meta?.passage,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
807
840
|
export function transformTrueFalse(questions) {
|
|
808
841
|
const groupPayload = questions[0]?.questionGroup?.payload;
|
|
809
842
|
const transformed = questions.map((q) => {
|
|
@@ -1135,3 +1168,39 @@ export function transformWordOrderAndMatchGroup(questions) {
|
|
|
1135
1168
|
explanation: parent?.explanation,
|
|
1136
1169
|
};
|
|
1137
1170
|
}
|
|
1171
|
+
export function transformCrosswordPuzzle(questions) {
|
|
1172
|
+
const question = questions[0];
|
|
1173
|
+
const content = (question?.content || {});
|
|
1174
|
+
const correctAnswer = (question?.correct_answer || question?.correctAnswer || {});
|
|
1175
|
+
const rows = content?.gridSize?.rows || content?.rows || 12;
|
|
1176
|
+
const cols = content?.gridSize?.cols || content?.cols || 12;
|
|
1177
|
+
const across = Array.isArray(content?.clues?.across)
|
|
1178
|
+
? content.clues.across.map((c) => ({
|
|
1179
|
+
number: Number(c.number),
|
|
1180
|
+
clue: String(c.clue || '').trim(),
|
|
1181
|
+
startRow: Number(c.startRow) || 1,
|
|
1182
|
+
startCol: Number(c.startCol) || 1,
|
|
1183
|
+
length: Number(c.length) || 1,
|
|
1184
|
+
}))
|
|
1185
|
+
: [];
|
|
1186
|
+
const down = Array.isArray(content?.clues?.down)
|
|
1187
|
+
? content.clues.down.map((c) => ({
|
|
1188
|
+
number: Number(c.number),
|
|
1189
|
+
clue: String(c.clue || '').trim(),
|
|
1190
|
+
startRow: Number(c.startRow) || 1,
|
|
1191
|
+
startCol: Number(c.startCol) || 1,
|
|
1192
|
+
length: Number(c.length) || 1,
|
|
1193
|
+
}))
|
|
1194
|
+
: [];
|
|
1195
|
+
const correctAnswers = correctAnswer?.answers && typeof correctAnswer.answers === 'object'
|
|
1196
|
+
? correctAnswer.answers
|
|
1197
|
+
: {};
|
|
1198
|
+
return {
|
|
1199
|
+
title: content?.title || '',
|
|
1200
|
+
instruction: question?.instruction || content?.instruction || 'Complete the crossword.',
|
|
1201
|
+
gridSize: { rows, cols },
|
|
1202
|
+
clues: { across, down },
|
|
1203
|
+
correctAnswers,
|
|
1204
|
+
explanation: question?.explanation,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
@@ -207,6 +207,11 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
207
207
|
requiresContent: true,
|
|
208
208
|
hasDedicatedComponent: true,
|
|
209
209
|
},
|
|
210
|
+
ANSWER_THE_QUESTION_GROUP: {
|
|
211
|
+
label: 'Đọc và trả lời câu hỏi (AI)',
|
|
212
|
+
requiresContent: true,
|
|
213
|
+
hasDedicatedComponent: true,
|
|
214
|
+
},
|
|
210
215
|
WORD_ORDERING: {
|
|
211
216
|
label: 'Sắp xếp từ',
|
|
212
217
|
requiresContent: true,
|
|
@@ -312,6 +317,11 @@ export const QUESTION_TYPE_CONFIG = {
|
|
|
312
317
|
requiresContent: true,
|
|
313
318
|
hasDedicatedComponent: true,
|
|
314
319
|
},
|
|
320
|
+
CROSSWORD_PUZZLE: {
|
|
321
|
+
label: 'Giải ô chữ (Crossword)',
|
|
322
|
+
requiresContent: true,
|
|
323
|
+
hasDedicatedComponent: true,
|
|
324
|
+
},
|
|
315
325
|
};
|
|
316
326
|
export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
317
327
|
'FILL_IN_BLANK',
|
|
@@ -333,6 +343,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
333
343
|
'WRITE_CORRECT_VERB_FORM',
|
|
334
344
|
'CHOOSE_CORRECT_ADJECTIVE',
|
|
335
345
|
'ANSWER_THE_QUESTION',
|
|
346
|
+
'ANSWER_THE_QUESTION_GROUP',
|
|
336
347
|
'TRUE_FALSE',
|
|
337
348
|
'WORD_ORDERING',
|
|
338
349
|
'WORD_FILL_PARAGRAPH',
|
|
@@ -356,6 +367,7 @@ export const QUESTION_TYPES_WITH_DEDICATED_COMPONENTS = [
|
|
|
356
367
|
'GN_SPEAKING_INTERVIEW',
|
|
357
368
|
'SPEAKING_CUE_CARD',
|
|
358
369
|
'WORD_FILL_STRUCTURED_FORM',
|
|
370
|
+
'CROSSWORD_PUZZLE',
|
|
359
371
|
];
|
|
360
372
|
export function getQuestionTypeLabel(type) {
|
|
361
373
|
return QUESTION_TYPE_CONFIG[type]?.label || type;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
export interface AnswerTheQuestionGroupItemData {
|
|
3
|
+
question: string;
|
|
4
|
+
expectedAnswers: string[];
|
|
5
|
+
isExample?: boolean;
|
|
6
|
+
points: number;
|
|
7
|
+
questionNumber: number;
|
|
8
|
+
}
|
|
9
|
+
export interface AnswerTheQuestionGroupData {
|
|
10
|
+
instruction: string;
|
|
11
|
+
passage?: string;
|
|
12
|
+
items: AnswerTheQuestionGroupItemData[];
|
|
13
|
+
explanation?: string;
|
|
14
|
+
points?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface AnswerTheQuestionGroupCreatorProps {
|
|
17
|
+
initialData?: AnswerTheQuestionGroupData;
|
|
18
|
+
onSave?: (data: AnswerTheQuestionGroupData) => void;
|
|
19
|
+
onCancel?: () => void;
|
|
20
|
+
onChange?: (data: AnswerTheQuestionGroupData) => void;
|
|
21
|
+
externalErrors?: string[];
|
|
22
|
+
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
|
|
23
|
+
validationRef?: MutableRefObject<{
|
|
24
|
+
triggerValidation: () => Promise<boolean>;
|
|
25
|
+
getFormData?: () => AnswerTheQuestionGroupData;
|
|
26
|
+
} | null>;
|
|
27
|
+
}
|
|
28
|
+
export interface AnswerTheQuestionGroupClientProps {
|
|
29
|
+
questionData: AnswerTheQuestionGroupData;
|
|
30
|
+
isReviewMode?: boolean;
|
|
31
|
+
userAnswers?: string[];
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer } from '../../../../shared/types/questions/crossword-puzzle';
|
|
2
|
+
export interface CrosswordClueFormItem extends CrosswordClue {
|
|
3
|
+
id: string;
|
|
4
|
+
answer: string;
|
|
5
|
+
direction: 'across' | 'down';
|
|
6
|
+
}
|
|
7
|
+
export interface CrosswordPuzzleFormState {
|
|
8
|
+
title?: string;
|
|
9
|
+
rows: number;
|
|
10
|
+
cols: number;
|
|
11
|
+
clues: CrosswordClueFormItem[];
|
|
12
|
+
caseSensitive: boolean;
|
|
13
|
+
points: number;
|
|
14
|
+
explanation?: string;
|
|
15
|
+
}
|
|
16
|
+
export type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -34,7 +34,7 @@ const PASSAGE_BASED_TYPES = [
|
|
|
34
34
|
'READ_PASSAGE_AND_COMPLETE_STORY',
|
|
35
35
|
];
|
|
36
36
|
// Grid-based question types
|
|
37
|
-
const GRID_BASED_TYPES = ['FILL_MISSING_WORDS_IN_GRID'];
|
|
37
|
+
const GRID_BASED_TYPES = ['FILL_MISSING_WORDS_IN_GRID', 'CROSSWORD_PUZZLE'];
|
|
38
38
|
// Get display icon for question type
|
|
39
39
|
const getQuestionTypeIcon = (type) => {
|
|
40
40
|
if (IMAGE_BASED_TYPES.includes(type)) {
|
|
@@ -18,6 +18,7 @@ import { writeAShortLetterRegistration } from '../types/write-a-short-letter/reg
|
|
|
18
18
|
import { writeCorrectVerbFormRegistration } from '../types/write-correct-verb-form/register';
|
|
19
19
|
import { chooseCorrectAdjectiveRegistration } from '../types/choose-correct-adjective/register';
|
|
20
20
|
import { answerTheQuestionRegistration } from '../types/answer-the-question/register';
|
|
21
|
+
import { answerTheQuestionGroupRegistration } from '../types/answer-the-question-group/register';
|
|
21
22
|
import { trueFalseRegistration } from '../types/true-false/register';
|
|
22
23
|
import { wordOrderingRegistration } from '../types/word-ordering/register';
|
|
23
24
|
import { wordFillParagraphRegistration } from '../types/word-fill-paragraph/register';
|
|
@@ -42,6 +43,7 @@ import { gnSpeakingInterviewRegistration } from '../types/gn-speaking-interview/
|
|
|
42
43
|
import { speakingCueCardRegistration } from '../types/speaking-cue-card/register';
|
|
43
44
|
import { wordFillStructuredFormRegistration } from '../types/word-fill-structured-form/register';
|
|
44
45
|
import { wordOrderAndMatchGroupRegistration } from '../types/word-order-and-match-group/register';
|
|
46
|
+
import { crosswordPuzzleRegistration } from '../types/crossword-puzzle/register';
|
|
45
47
|
// ─── Registry map ───────────────────────────────────────────────────────────
|
|
46
48
|
export const questionTypeRegistry = {
|
|
47
49
|
CHOOSE_THE_CORRECT_ANSWER: chooseTheCorrectAnswerRegistration,
|
|
@@ -64,6 +66,7 @@ export const questionTypeRegistry = {
|
|
|
64
66
|
WRITE_CORRECT_VERB_FORM: writeCorrectVerbFormRegistration,
|
|
65
67
|
CHOOSE_CORRECT_ADJECTIVE: chooseCorrectAdjectiveRegistration,
|
|
66
68
|
ANSWER_THE_QUESTION: answerTheQuestionRegistration,
|
|
69
|
+
ANSWER_THE_QUESTION_GROUP: answerTheQuestionGroupRegistration,
|
|
67
70
|
TRUE_FALSE: trueFalseRegistration,
|
|
68
71
|
WORD_ORDERING: wordOrderingRegistration,
|
|
69
72
|
WORD_FILL_PARAGRAPH: wordFillParagraphRegistration, // Will need to handle viewMode mapping either here or in wrapper
|
|
@@ -87,4 +90,5 @@ export const questionTypeRegistry = {
|
|
|
87
90
|
GN_SPEAKING_INTERVIEW: gnSpeakingInterviewRegistration,
|
|
88
91
|
SPEAKING_CUE_CARD: speakingCueCardRegistration,
|
|
89
92
|
WORD_FILL_STRUCTURED_FORM: wordFillStructuredFormRegistration,
|
|
93
|
+
CROSSWORD_PUZZLE: crosswordPuzzleRegistration,
|
|
90
94
|
};
|
|
@@ -14,7 +14,8 @@ export declare enum QuestionBankEditorType {
|
|
|
14
14
|
WRITE_A_SHORT_LETTER = "WRITE_A_SHORT_LETTER",
|
|
15
15
|
WRITE_CORRECT_VERB_FORM = "WRITE_CORRECT_VERB_FORM",
|
|
16
16
|
FILL_IN_BLANK = "FILL_IN_BLANK",
|
|
17
|
-
CHOOSE_CORRECT_ADJECTIVE = "CHOOSE_CORRECT_ADJECTIVE"
|
|
17
|
+
CHOOSE_CORRECT_ADJECTIVE = "CHOOSE_CORRECT_ADJECTIVE",
|
|
18
|
+
CROSSWORD_PUZZLE = "CROSSWORD_PUZZLE"
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Renders the appropriate Creator for a question-bank item.
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { t } from '../../../shared/lib/i18n';
|
|
5
5
|
import { renderChooseCorrectAdjectiveEditor, renderChooseTheCorrectAnswerEditor, renderFillInBlankEditor, renderFillMissingWordsInGridEditor, renderImageObjectMatchingEditor, renderLabelThePictureEditor, renderLookPictureChooseCorrectAnswerEditor, renderLookPictureFillBlankChooseAnswerEditor, renderLookPictureFillWordHintEditor, renderReadAndColorObjectsEditor, renderReadPassageAndAnswerQuestionsEditor, renderWriteAShortLetterEditor, renderWriteCorrectVerbFormEditor, UnsupportedEditorFallback, } from './editor-adapters';
|
|
6
|
+
import { CrosswordPuzzleCreator } from '../types/crossword-puzzle/CrosswordPuzzleCreator';
|
|
6
7
|
/** Marketing question types supported by the question-bank editor. */
|
|
7
8
|
export var QuestionBankEditorType;
|
|
8
9
|
(function (QuestionBankEditorType) {
|
|
@@ -19,6 +20,7 @@ export var QuestionBankEditorType;
|
|
|
19
20
|
QuestionBankEditorType["WRITE_CORRECT_VERB_FORM"] = "WRITE_CORRECT_VERB_FORM";
|
|
20
21
|
QuestionBankEditorType["FILL_IN_BLANK"] = "FILL_IN_BLANK";
|
|
21
22
|
QuestionBankEditorType["CHOOSE_CORRECT_ADJECTIVE"] = "CHOOSE_CORRECT_ADJECTIVE";
|
|
23
|
+
QuestionBankEditorType["CROSSWORD_PUZZLE"] = "CROSSWORD_PUZZLE";
|
|
22
24
|
})(QuestionBankEditorType || (QuestionBankEditorType = {}));
|
|
23
25
|
/**
|
|
24
26
|
* Renders the appropriate Creator for a question-bank item.
|
|
@@ -69,6 +71,20 @@ export function QuestionEditorRenderer({ questionType, content, correctAnswer, e
|
|
|
69
71
|
return renderFillInBlankEditor(adapterProps);
|
|
70
72
|
case QuestionBankEditorType.CHOOSE_CORRECT_ADJECTIVE:
|
|
71
73
|
return renderChooseCorrectAdjectiveEditor(adapterProps);
|
|
74
|
+
case QuestionBankEditorType.CROSSWORD_PUZZLE:
|
|
75
|
+
return (_jsx(CrosswordPuzzleCreator, { initialData: {
|
|
76
|
+
content,
|
|
77
|
+
answer: correctAnswer,
|
|
78
|
+
explanation,
|
|
79
|
+
points,
|
|
80
|
+
}, onChange: (data) => {
|
|
81
|
+
onChange?.({
|
|
82
|
+
content: data.content,
|
|
83
|
+
answer: data.answer,
|
|
84
|
+
explanation: data.explanation || (explanation ?? undefined),
|
|
85
|
+
points: data.points ?? points,
|
|
86
|
+
});
|
|
87
|
+
} }));
|
|
72
88
|
default:
|
|
73
89
|
return (_jsx(UnsupportedEditorFallback, { questionType: questionType, content: content, correctAnswer: correctAnswer, explanation: explanation }));
|
|
74
90
|
}
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { BookOpen, CircleHelp, Lightbulb, Sparkles, Star } from 'lucide-react';
|
|
4
|
+
export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
|
|
5
|
+
const items = questionData.items || [];
|
|
6
|
+
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 })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
|
|
7
|
+
const userValue = userAnswers[itemIndex] || '';
|
|
8
|
+
const expectedAnswers = Array.isArray(item.expectedAnswers)
|
|
9
|
+
? item.expectedAnswers.filter((answer) => answer.trim() !== '')
|
|
10
|
+
: [];
|
|
11
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-4 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-4", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700", children: userValue || (isReviewMode ? '(Chưa trả lời)' : 'Học sinh sẽ nhập câu trả lời tại đây') }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n m\u1EABu (AI)" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] }))] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
12
|
+
}), 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] }))] }));
|
|
13
|
+
}
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { AnswerTheQuestionGroupCreatorProps } from '../../_shared/types/answer-the-question-group.type';
|
|
2
|
+
export declare function AnswerTheQuestionGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: AnswerTheQuestionGroupCreatorProps): import("react").JSX.Element;
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
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 { Textarea } from '../../../../components/ui/textarea';
|
|
8
|
+
import { PointsInput } from '../../../../components/ui/points-input';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
10
|
+
import { Switch } from '../../../../components/ui/switch';
|
|
11
|
+
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
12
|
+
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
13
|
+
import { Eye, Pencil, Plus, Sparkles, Star, Trash2 } from 'lucide-react';
|
|
14
|
+
import { AnswerTheQuestionGroupClient } from './AnswerTheQuestionGroupClient';
|
|
15
|
+
const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
|
|
16
|
+
function createEmptyItem(questionNumber) {
|
|
17
|
+
return {
|
|
18
|
+
question: '',
|
|
19
|
+
expectedAnswers: [''],
|
|
20
|
+
isExample: false,
|
|
21
|
+
points: 1,
|
|
22
|
+
questionNumber,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function normalizeExpectedAnswers(value) {
|
|
26
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
27
|
+
return [''];
|
|
28
|
+
}
|
|
29
|
+
return value.map((item) => (typeof item === 'string' ? item : ''));
|
|
30
|
+
}
|
|
31
|
+
function normalizeItems(rawItems) {
|
|
32
|
+
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
33
|
+
return [createEmptyItem(1)];
|
|
34
|
+
}
|
|
35
|
+
return rawItems.map((item, index) => ({
|
|
36
|
+
question: item.question || '',
|
|
37
|
+
expectedAnswers: normalizeExpectedAnswers(item.expectedAnswers),
|
|
38
|
+
isExample: item.isExample === true,
|
|
39
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
40
|
+
questionNumber: item.questionNumber || index + 1,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function sumPoints(items) {
|
|
44
|
+
return items.reduce((total, item) => (item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1)), 0);
|
|
45
|
+
}
|
|
46
|
+
function isContentEmpty(html) {
|
|
47
|
+
return !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
48
|
+
}
|
|
49
|
+
export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
50
|
+
const [instruction, setInstruction] = useState(initialData?.instruction || DEFAULT_INSTRUCTION);
|
|
51
|
+
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
52
|
+
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
53
|
+
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
54
|
+
const [isClientMode, setIsClientMode] = useState(false);
|
|
55
|
+
const [errors, setErrors] = useState([]);
|
|
56
|
+
const onChangeRef = useRef(onChange);
|
|
57
|
+
const previousPayloadRef = useRef('');
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
onChangeRef.current = onChange;
|
|
60
|
+
}, [onChange]);
|
|
61
|
+
const buildPayload = (nextInstruction = instruction, nextPassage = passage, nextItems = items, nextExplanation = explanation) => ({
|
|
62
|
+
instruction: nextInstruction,
|
|
63
|
+
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
64
|
+
items: nextItems,
|
|
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
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
previousPayloadRef.current = serialized;
|
|
78
|
+
onUnsavedChangesChange?.(true);
|
|
79
|
+
debouncedOnChange(payload);
|
|
80
|
+
}, [instruction, passage, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
81
|
+
const validate = () => {
|
|
82
|
+
const nextErrors = [];
|
|
83
|
+
if (!instruction.trim()) {
|
|
84
|
+
nextErrors.push('Vui lòng nhập hướng dẫn.');
|
|
85
|
+
}
|
|
86
|
+
if (items.length < 1) {
|
|
87
|
+
nextErrors.push('Cần ít nhất một câu hỏi.');
|
|
88
|
+
}
|
|
89
|
+
items.forEach((item, index) => {
|
|
90
|
+
if (!item.question.trim()) {
|
|
91
|
+
nextErrors.push(`Câu ${index + 1}: nội dung câu hỏi là bắt buộc.`);
|
|
92
|
+
}
|
|
93
|
+
const filledAnswers = item.expectedAnswers
|
|
94
|
+
.map((answer) => answer.trim())
|
|
95
|
+
.filter((answer) => answer !== '');
|
|
96
|
+
if (!item.isExample && filledAnswers.length === 0) {
|
|
97
|
+
nextErrors.push(`Câu ${index + 1}: cần ít nhất một đáp án mẫu.`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return nextErrors;
|
|
101
|
+
};
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!validationRef) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
validationRef.current = {
|
|
107
|
+
triggerValidation: async () => {
|
|
108
|
+
const nextErrors = validate();
|
|
109
|
+
setErrors(nextErrors);
|
|
110
|
+
return nextErrors.length === 0;
|
|
111
|
+
},
|
|
112
|
+
getFormData: () => buildPayload(),
|
|
113
|
+
};
|
|
114
|
+
}, [validationRef, instruction, passage, items, explanation]);
|
|
115
|
+
const updateItem = (index, patch) => {
|
|
116
|
+
setItems((prev) => {
|
|
117
|
+
const next = [...prev];
|
|
118
|
+
const current = next[index];
|
|
119
|
+
if (!current)
|
|
120
|
+
return prev;
|
|
121
|
+
next[index] = { ...current, ...patch };
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const updateExpectedAnswer = (itemIndex, answerIndex, value) => {
|
|
126
|
+
setItems((prev) => {
|
|
127
|
+
const next = [...prev];
|
|
128
|
+
const current = next[itemIndex];
|
|
129
|
+
if (!current)
|
|
130
|
+
return prev;
|
|
131
|
+
const expectedAnswers = [...current.expectedAnswers];
|
|
132
|
+
expectedAnswers[answerIndex] = value;
|
|
133
|
+
next[itemIndex] = { ...current, expectedAnswers };
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
const addExpectedAnswer = (itemIndex) => {
|
|
138
|
+
setItems((prev) => {
|
|
139
|
+
const next = [...prev];
|
|
140
|
+
const current = next[itemIndex];
|
|
141
|
+
if (!current)
|
|
142
|
+
return prev;
|
|
143
|
+
next[itemIndex] = {
|
|
144
|
+
...current,
|
|
145
|
+
expectedAnswers: [...current.expectedAnswers, ''],
|
|
146
|
+
};
|
|
147
|
+
return next;
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const removeExpectedAnswer = (itemIndex, answerIndex) => {
|
|
151
|
+
setItems((prev) => {
|
|
152
|
+
const next = [...prev];
|
|
153
|
+
const current = next[itemIndex];
|
|
154
|
+
if (!current || current.expectedAnswers.length <= 1)
|
|
155
|
+
return prev;
|
|
156
|
+
next[itemIndex] = {
|
|
157
|
+
...current,
|
|
158
|
+
expectedAnswers: current.expectedAnswers.filter((_, index) => index !== answerIndex),
|
|
159
|
+
};
|
|
160
|
+
return next;
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
const addItem = () => {
|
|
164
|
+
setItems((prev) => [...prev, createEmptyItem(prev.length + 1)]);
|
|
165
|
+
};
|
|
166
|
+
const removeItem = (index) => {
|
|
167
|
+
setItems((prev) => {
|
|
168
|
+
if (prev.length <= 1)
|
|
169
|
+
return prev;
|
|
170
|
+
return prev
|
|
171
|
+
.filter((_, itemIndex) => itemIndex !== index)
|
|
172
|
+
.map((item, itemIndex) => ({ ...item, questionNumber: itemIndex + 1 }));
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const displayErrors = externalErrors && externalErrors.length > 0 ? externalErrors : errors;
|
|
176
|
+
if (isClientMode) {
|
|
177
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(false), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Ch\u1EBF \u0111\u1ED9 ch\u1EC9nh s\u1EEDa"] }) }), _jsx(AnswerTheQuestionGroupClient, { questionData: buildPayload(), isReviewMode: true })] }));
|
|
178
|
+
}
|
|
179
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-medium text-gray-900", children: "\u0110\u1ECDc v\u00E0 tr\u1EA3 l\u1EDDi c\u00E2u h\u1ECFi (AI)" }), _jsx("p", { className: "text-sm text-gray-500", children: "Nh\u00F3m c\u00E2u \u0111\u1ECDc hi\u1EC3u l\u01B0u 1 row. AI ch\u1EA5m theo question + expectedAnswers." })] }), _jsxs(Button, { type: "button", variant: "outline", onClick: () => setIsClientMode(true), children: [_jsx(Eye, { className: "mr-2 h-4 w-4" }), "Xem tr\u01B0\u1EDBc"] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Nh\u00F3m \u0111\u1ECDc hi\u1EC3u" }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "group-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: DEFAULT_INSTRUCTION })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "rounded-xl border border-violet-100 bg-violet-50 p-4 text-sm text-violet-800", children: [_jsxs("div", { className: "flex items-center gap-2 font-medium", children: [_jsx(Sparkles, { className: "h-4 w-4" }), "Ch\u1EA5m \u0111i\u1EC3m b\u1EB1ng AI"] }), _jsx("p", { className: "mt-1 text-xs text-violet-700", children: "H\u1ECDc sinh vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi. AI so v\u1EDBi b\u1EA5t k\u1EF3 \u0111\u00E1p \u00E1n m\u1EABu n\u00E0o c\u1EE7a t\u1EEBng c\u00E2u (binary 0/1)." })] }), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gray-50 p-4", children: [_jsx(PointsInput, { value: sumPoints(items), allowZero: true, readOnly: true }), _jsx("p", { className: "mt-2 text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB c\u00E1c c\u00E2u kh\u00F4ng ph\u1EA3i example." })] })] })] }), items.map((item, itemIndex) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsxs(CardTitle, { className: "text-base", children: ["C\u00E2u ", itemIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeItem(itemIndex), disabled: items.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-800", children: [_jsx(Star, { className: "h-4 w-4" }), "C\u00E2u v\u00ED d\u1EE5"] }), _jsx(Switch, { checked: item.isExample === true, onCheckedChange: (checked) => updateItem(itemIndex, {
|
|
180
|
+
isExample: checked,
|
|
181
|
+
points: checked ? 0 : (item.points || 1),
|
|
182
|
+
}) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: item.question, onChange: (event) => updateItem(itemIndex, { question: event.target.value }), rows: 2, placeholder: "What is autism?" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "\u0110\u00E1p \u00E1n m\u1EABu cho AI" }), item.expectedAnswers.map((answer, answerIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: answer, onChange: (event) => updateExpectedAnswer(itemIndex, answerIndex, event.target.value), placeholder: `Đáp án mẫu ${answerIndex + 1}` }), item.expectedAnswers.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeExpectedAnswer(itemIndex, answerIndex), children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }, `expected-${itemIndex}-${answerIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => addExpectedAnswer(itemIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm \u0111\u00E1p \u00E1n m\u1EABu"] })] }), !item.isExample && (_jsx(PointsInput, { value: item.points, allowZero: true, onChange: (e) => updateItem(itemIndex, { points: Number(e.target.value) || 0 }) }))] })] }, `item-${itemIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: addItem, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm c\u00E2u h\u1ECFi"] }), displayErrors.length > 0 && (_jsx("div", { className: "rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: displayErrors.map((error) => (_jsx("p", { children: error }, error))) }))] }));
|
|
183
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnswerTheQuestionGroupData, AnswerTheQuestionGroupItemData } from '../../_shared/types/answer-the-question-group.type';
|
|
2
|
+
export declare function createEmptyAnswerTheQuestionGroupItem(questionNumber: number): AnswerTheQuestionGroupItemData;
|
|
3
|
+
export declare function mapAnswerTheQuestionGroupItem(item: unknown, index: number, fallbackAnswers?: string[]): AnswerTheQuestionGroupItemData;
|
|
4
|
+
export declare function mapQuestionToAnswerTheQuestionGroupData(question: Record<string, unknown> | null | undefined): AnswerTheQuestionGroupData;
|