@tinyweb_dev/oe-exam-sdk 1.0.10 → 1.0.12
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 +6 -2
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.js +36 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +4 -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/utils/answer-transformers.js +1 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +27 -3
- package/dist/components/exams/take/utils/question-transformers.js +45 -2
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +5 -2
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +2 -2
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +14 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +2 -2
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +22 -33
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +16 -3
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +2 -1
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +25 -6
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +1 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +16 -5
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +60 -18
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +2 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +24 -5
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +8 -4
- package/dist/components/questions/types/matching-with-lines-group/transform.js +5 -4
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +19 -15
- package/dist/components/results/renderers/review-question-body-movers.js +6 -2
- package/dist/components/results/review-data.js +12 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +17 -2
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +2 -2
- package/package.json +1 -1
|
@@ -632,6 +632,12 @@ function ExamTakingPageContainerInner({ roomId, api, onNavigate, resultPath = (a
|
|
|
632
632
|
map.set(`${question.id}-item-${index}`, question.id);
|
|
633
633
|
});
|
|
634
634
|
}
|
|
635
|
+
if (part.questionType === 'MATCH_WORD_TO_PICTURE_GROUP' &&
|
|
636
|
+
Array.isArray(content?.items)) {
|
|
637
|
+
content.items.forEach((_, index) => {
|
|
638
|
+
map.set(`${question.id}-item-${index}`, question.id);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
635
641
|
if (part.questionType === 'CHOOSE_THEN_ANSWER_GROUP' &&
|
|
636
642
|
Array.isArray(content?.items)) {
|
|
637
643
|
content.items.forEach((_, index) => {
|
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* 2. Transforms data to renderer-specific props
|
|
9
9
|
* 3. Renders the correct component for each question type
|
|
10
10
|
*/
|
|
11
|
-
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, SortWordsIntoCategoriesRenderer, ChooseThenAnswerGroupRenderer, } from './question-renderers';
|
|
11
|
+
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversMatchWordToPictureGroupRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrosswordPuzzleRenderer, FindWordsInMatrixRenderer, SortWordsIntoCategoriesRenderer, ChooseThenAnswerGroupRenderer, } from './question-renderers';
|
|
12
12
|
import { SpeakingQuestionRenderer } from './speaking/renderers';
|
|
13
|
-
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformReadingPassage, transformLabelPicture, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, transformSortWordsIntoCategories, transformChooseThenAnswerGroup, } from '../utils/question-transformers';
|
|
13
|
+
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformReadingPassage, transformLabelPicture, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformWordOrdering, transformWordOrderingGroupTake, transformWordFillParagraph, transformMatchByWritingAnswer, transformMatchWordToPicture, transformMatchWordToPictureGroup, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformMatchingWithLinesGroup, transformCrosswordPuzzle, transformFindWordsInMatrix, transformSortWordsIntoCategories, transformChooseThenAnswerGroup, } from '../utils/question-transformers';
|
|
14
14
|
import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
|
|
15
15
|
import { normalizeColoringAssignments } from '../utils/coloring-assignments';
|
|
16
16
|
export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, compactLayout = false, }) {
|
|
@@ -200,6 +200,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
200
200
|
const data = transformMatchByWritingAnswer(questions);
|
|
201
201
|
return (_jsx(MoversMatchByWritingAnswerRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, audioUrl: part.audioUrl, options: data.options, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, viewMode: data.viewMode, groupImageUrl: data.groupImageUrl }));
|
|
202
202
|
}
|
|
203
|
+
case 'MATCH_WORD_TO_PICTURE_GROUP': {
|
|
204
|
+
const data = transformMatchWordToPictureGroup(questions);
|
|
205
|
+
return (_jsx(MoversMatchWordToPictureGroupRenderer, { partNumber: partNo, partName: part.name, questionCount: data.questions.filter((q) => !q.isExample).length, instruction: part.instructions || data.instruction, audioUrl: part.audioUrl, data: data, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
206
|
+
}
|
|
203
207
|
case 'MATCH_WORD_TO_PICTURE': {
|
|
204
208
|
const data = transformMatchWordToPicture(questions);
|
|
205
209
|
return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, wordBank: data.wordBank, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MatchWordToPictureGroupPartData } from '../../utils/question-transformers';
|
|
2
|
+
interface MoversMatchWordToPictureGroupRendererProps {
|
|
3
|
+
partNumber: number;
|
|
4
|
+
partName?: string;
|
|
5
|
+
questionCount: number;
|
|
6
|
+
instruction: string;
|
|
7
|
+
audioUrl?: string;
|
|
8
|
+
data: MatchWordToPictureGroupPartData;
|
|
9
|
+
answers: Record<string, string | undefined>;
|
|
10
|
+
onAnswerChange: (questionId: string, value: string) => void;
|
|
11
|
+
isReviewMode?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function MoversMatchWordToPictureGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, data, answers, onAnswerChange, isReviewMode, }: MoversMatchWordToPictureGroupRendererProps): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
4
|
+
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
5
|
+
import { MatchWordToPictureGroupClient } from '../../../../questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient';
|
|
6
|
+
export function MoversMatchWordToPictureGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, data, answers, onAnswerChange, isReviewMode = false, }) {
|
|
7
|
+
const userAnswers = data.questions.map((question) => {
|
|
8
|
+
if (question.isExample)
|
|
9
|
+
return question.correctAnswer;
|
|
10
|
+
return typeof answers[question.id] === 'string' ? answers[question.id] || '' : '';
|
|
11
|
+
});
|
|
12
|
+
const resolvedAudio = data.audioUrl || audioUrl || '';
|
|
13
|
+
return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(MatchWordToPictureGroupClient, { questionData: {
|
|
14
|
+
title: data.title,
|
|
15
|
+
instruction: data.instruction,
|
|
16
|
+
...(data.passage ? { passage: data.passage } : {}),
|
|
17
|
+
...(resolvedAudio ? { audioUrl: resolvedAudio } : {}),
|
|
18
|
+
...(data.imageUrl ? { imageUrl: data.imageUrl } : {}),
|
|
19
|
+
wordBank: data.wordBank,
|
|
20
|
+
items: data.questions.map((question) => ({
|
|
21
|
+
imageUrl: question.imageUrl,
|
|
22
|
+
correctAnswer: question.correctAnswer,
|
|
23
|
+
isExample: question.isExample,
|
|
24
|
+
points: question.points,
|
|
25
|
+
questionNumber: question.questionNumber,
|
|
26
|
+
})),
|
|
27
|
+
explanation: data.explanation,
|
|
28
|
+
}, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: (next) => {
|
|
29
|
+
next.forEach((value, index) => {
|
|
30
|
+
const target = data.questions[index];
|
|
31
|
+
if (target && !target.isExample) {
|
|
32
|
+
onAnswerChange(target.id, value);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
} }) })] }));
|
|
36
|
+
}
|
|
@@ -13,9 +13,13 @@ export function MoversMatchingWithLinesGroupRenderer({ partNumber, partName, que
|
|
|
13
13
|
return (_jsxs("div", { className: "w-full space-y-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, partName: partName, questionCount: questionCount }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction }), _jsx("div", { className: "mt-4", children: _jsx(MatchingWithLinesGroupClient, { questionData: {
|
|
14
14
|
title: data.title,
|
|
15
15
|
instruction: data.instruction,
|
|
16
|
+
...(data.passage ? { passage: data.passage } : {}),
|
|
17
|
+
...(data.audioUrl ? { audioUrl: data.audioUrl } : {}),
|
|
18
|
+
...(data.imageUrl ? { imageUrl: data.imageUrl } : {}),
|
|
16
19
|
rightItems: data.rightItems,
|
|
17
20
|
items: data.questions.map((question) => ({
|
|
18
21
|
imageUrl: question.imageUrl,
|
|
22
|
+
text: question.text,
|
|
19
23
|
correctAnswer: question.correctRightId,
|
|
20
24
|
isExample: question.isExample,
|
|
21
25
|
points: question.points,
|
|
@@ -17,6 +17,7 @@ export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRender
|
|
|
17
17
|
export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
|
|
18
18
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
19
19
|
export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
|
|
20
|
+
export { MoversMatchWordToPictureGroupRenderer } from './MoversMatchWordToPictureGroupRenderer';
|
|
20
21
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
21
22
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
22
23
|
export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
|
|
@@ -21,6 +21,7 @@ export { MoversWordOrderingGroupRenderer } from './MoversWordOrderingGroupRender
|
|
|
21
21
|
export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRenderer';
|
|
22
22
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
23
23
|
export { MoversMatchWordToPictureRenderer } from './MoversMatchWordToPictureRenderer';
|
|
24
|
+
export { MoversMatchWordToPictureGroupRenderer } from './MoversMatchWordToPictureGroupRenderer';
|
|
24
25
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
25
26
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
26
27
|
export { MoversMatchingWithLinesGroupRenderer } from './MoversMatchingWithLinesGroupRenderer';
|
|
@@ -90,6 +90,7 @@ function findParentQuestion(subQuestionId, parts) {
|
|
|
90
90
|
part.questionType === 'TRUE_FALSE_CORRECT_GROUP' ||
|
|
91
91
|
part.questionType === 'FILL_IN_BLANK_GROUP' ||
|
|
92
92
|
part.questionType === 'MATCHING_WITH_LINES_GROUP' ||
|
|
93
|
+
part.questionType === 'MATCH_WORD_TO_PICTURE_GROUP' ||
|
|
93
94
|
part.questionType === 'WORD_ORDER_AND_MATCH_GROUP' ||
|
|
94
95
|
part.questionType === 'SPONTANEOUS_QA_GROUP' ||
|
|
95
96
|
part.questionType === 'ANSWER_THE_QUESTION_GROUP' ||
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Each transformer handles specific question type data structure.
|
|
6
6
|
*/
|
|
7
7
|
import type { FillBlankQuestion, ChooseBestAnswerQuestion, ReadingPassageQuestion, LabelThePictureLabel, 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' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'SORT_WORDS_INTO_CATEGORIES' | 'CHOOSE_THEN_ANSWER_GROUP' | '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' | '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' | 'READ_PASSAGE_AND_ANSWER_QUESTIONS' | 'LABEL_THE_PICTURE' | 'FILL_MISSING_WORDS_IN_GRID' | 'IMAGE_OBJECT_MATCHING' | 'ANSWER_THE_QUESTION' | 'ANSWER_THE_QUESTION_GROUP' | 'TRUE_FALSE' | 'TRUE_FALSE_GROUP' | 'TRUE_FALSE_CORRECT_GROUP' | 'FILL_IN_BLANK_GROUP' | 'MATCHING_WITH_LINES_GROUP' | 'CROSS_OUT_WORD_GROUP' | 'WORD_ORDERING' | 'WORD_ORDERING_GROUP' | 'WORD_FILL_PARAGRAPH' | 'MATCH_BY_WRITING_ANSWER' | 'MATCH_WORD_TO_PICTURE' | 'MATCH_WORD_TO_PICTURE_GROUP' | 'WRITE_SENTENCES' | 'WORD_FILL_STRUCTURED_FORM' | 'CROSSWORD_PUZZLE' | 'FIND_WORDS_IN_MATRIX' | 'SORT_WORDS_INTO_CATEGORIES' | 'CHOOSE_THEN_ANSWER_GROUP' | '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' | 'SPEAKING_CUE_CARD';
|
|
9
9
|
export interface ApiQuestion {
|
|
10
10
|
id: string;
|
|
11
11
|
content: any;
|
|
@@ -372,6 +372,25 @@ export declare function transformMatchWordToPicture(questions: ApiQuestion[]): {
|
|
|
372
372
|
wordBank: string[];
|
|
373
373
|
instruction: string;
|
|
374
374
|
};
|
|
375
|
+
export interface MatchWordToPictureGroupQuestionItem {
|
|
376
|
+
id: string;
|
|
377
|
+
questionNumber: number;
|
|
378
|
+
imageUrl?: string | string[];
|
|
379
|
+
correctAnswer: string;
|
|
380
|
+
isExample: boolean;
|
|
381
|
+
points: number;
|
|
382
|
+
}
|
|
383
|
+
export interface MatchWordToPictureGroupPartData {
|
|
384
|
+
questions: MatchWordToPictureGroupQuestionItem[];
|
|
385
|
+
wordBank: string[];
|
|
386
|
+
title?: string;
|
|
387
|
+
instruction: string;
|
|
388
|
+
passage?: string;
|
|
389
|
+
audioUrl?: string;
|
|
390
|
+
imageUrl?: string | string[];
|
|
391
|
+
explanation?: string;
|
|
392
|
+
}
|
|
393
|
+
export declare function transformMatchWordToPictureGroup(questions: ApiQuestion[]): MatchWordToPictureGroupPartData;
|
|
375
394
|
export declare function transformWriteSentences(questions: ApiQuestion[]): {
|
|
376
395
|
questions: Array<{
|
|
377
396
|
id: string;
|
|
@@ -574,7 +593,8 @@ export declare function transformChooseThenAnswerGroup(questions: ApiQuestion[])
|
|
|
574
593
|
export interface MatchingWithLinesGroupQuestionItem {
|
|
575
594
|
id: string;
|
|
576
595
|
questionNumber: number;
|
|
577
|
-
imageUrl
|
|
596
|
+
imageUrl?: string;
|
|
597
|
+
text?: string;
|
|
578
598
|
correctRightId: string;
|
|
579
599
|
isExample: boolean;
|
|
580
600
|
points: number;
|
|
@@ -583,10 +603,14 @@ export interface MatchingWithLinesGroupPartData {
|
|
|
583
603
|
questions: MatchingWithLinesGroupQuestionItem[];
|
|
584
604
|
rightItems: Array<{
|
|
585
605
|
id: string;
|
|
586
|
-
text
|
|
606
|
+
text?: string;
|
|
607
|
+
imageUrl?: string;
|
|
587
608
|
}>;
|
|
588
609
|
title?: string;
|
|
589
610
|
instruction: string;
|
|
611
|
+
passage?: string;
|
|
612
|
+
audioUrl?: string | string[];
|
|
613
|
+
imageUrl?: string | string[];
|
|
590
614
|
explanation?: string;
|
|
591
615
|
}
|
|
592
616
|
export declare function transformMatchingWithLinesGroup(questions: ApiQuestion[]): MatchingWithLinesGroupPartData;
|
|
@@ -1045,6 +1045,39 @@ export function transformMatchWordToPicture(questions) {
|
|
|
1045
1045
|
instruction,
|
|
1046
1046
|
};
|
|
1047
1047
|
}
|
|
1048
|
+
export function transformMatchWordToPictureGroup(questions) {
|
|
1049
|
+
const parent = questions[0];
|
|
1050
|
+
const content = (parent?.content || {});
|
|
1051
|
+
const items = Array.isArray(content.items) ? content.items : [];
|
|
1052
|
+
const parentCorrect = Array.isArray(parent?.correct_answer?.answer)
|
|
1053
|
+
? parent.correct_answer.answer
|
|
1054
|
+
: [];
|
|
1055
|
+
const title = typeof content.meta?.title === 'string' ? content.meta.title.trim() : '';
|
|
1056
|
+
const passage = typeof content.meta?.passage === 'string' ? content.meta.passage.trim() : '';
|
|
1057
|
+
const audioUrl = typeof content.meta?.audioUrl === 'string' ? content.meta.audioUrl.trim() : '';
|
|
1058
|
+
return {
|
|
1059
|
+
questions: items.map((item, index) => {
|
|
1060
|
+
const isExample = item.isExample === true;
|
|
1061
|
+
const fromItem = typeof item.correctAnswer?.answer === 'string' ? item.correctAnswer.answer : '';
|
|
1062
|
+
const fromParent = typeof parentCorrect[index] === 'string' ? parentCorrect[index] : '';
|
|
1063
|
+
return {
|
|
1064
|
+
id: `${parent?.id || 'q'}-item-${index}`,
|
|
1065
|
+
questionNumber: item.questionNumber || index + 1,
|
|
1066
|
+
imageUrl: item.content?.imageUrl,
|
|
1067
|
+
correctAnswer: fromItem || fromParent,
|
|
1068
|
+
isExample,
|
|
1069
|
+
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
1070
|
+
};
|
|
1071
|
+
}),
|
|
1072
|
+
wordBank: Array.isArray(content.meta?.wordBank) ? content.meta.wordBank : [],
|
|
1073
|
+
...(title ? { title } : {}),
|
|
1074
|
+
instruction: content.meta?.instruction || 'Look and complete.',
|
|
1075
|
+
...(passage ? { passage } : {}),
|
|
1076
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
1077
|
+
...(content.meta?.imageUrl ? { imageUrl: content.meta.imageUrl } : {}),
|
|
1078
|
+
explanation: parent?.explanation,
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1048
1081
|
export function transformWriteSentences(questions) {
|
|
1049
1082
|
const firstQuestion = questions[0];
|
|
1050
1083
|
const content = firstQuestion?.content;
|
|
@@ -1388,18 +1421,25 @@ export function transformMatchingWithLinesGroup(questions) {
|
|
|
1388
1421
|
const rightItems = Array.isArray(content.rightItems)
|
|
1389
1422
|
? content.rightItems.map((item, index) => ({
|
|
1390
1423
|
id: typeof item?.id === 'string' ? item.id : `r${index + 1}`,
|
|
1391
|
-
|
|
1424
|
+
...(typeof item?.text === 'string' && item.text.trim() ? { text: item.text } : {}),
|
|
1425
|
+
...(typeof item?.imageUrl === 'string' && item.imageUrl.trim() ? { imageUrl: item.imageUrl } : {}),
|
|
1392
1426
|
}))
|
|
1393
1427
|
: [];
|
|
1428
|
+
const passage = content.meta?.passage?.trim() || '';
|
|
1429
|
+
const audioUrl = content.meta?.audioUrl;
|
|
1430
|
+
const imageUrl = content.meta?.imageUrl;
|
|
1394
1431
|
return {
|
|
1395
1432
|
questions: items.map((item, index) => {
|
|
1396
1433
|
const isExample = item.isExample === true;
|
|
1397
1434
|
const fromItem = typeof item.correctAnswer?.answer === 'string' ? item.correctAnswer.answer : '';
|
|
1398
1435
|
const fromParent = typeof parentCorrect[index] === 'string' ? parentCorrect[index] : '';
|
|
1436
|
+
const itemImage = item.content?.imageUrl?.trim() || '';
|
|
1437
|
+
const itemText = item.content?.text?.trim() || '';
|
|
1399
1438
|
return {
|
|
1400
1439
|
id: `${parent?.id || 'q'}-item-${index}`,
|
|
1401
1440
|
questionNumber: item.questionNumber || index + 1,
|
|
1402
|
-
imageUrl:
|
|
1441
|
+
...(itemImage ? { imageUrl: itemImage } : {}),
|
|
1442
|
+
...(itemText ? { text: itemText } : {}),
|
|
1403
1443
|
correctRightId: fromItem || fromParent,
|
|
1404
1444
|
isExample,
|
|
1405
1445
|
points: isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
@@ -1408,6 +1448,9 @@ export function transformMatchingWithLinesGroup(questions) {
|
|
|
1408
1448
|
rightItems,
|
|
1409
1449
|
title: content.meta?.title || '',
|
|
1410
1450
|
instruction: content.meta?.instruction || 'Look and match.',
|
|
1451
|
+
...(passage ? { passage } : {}),
|
|
1452
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
1453
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
1411
1454
|
explanation: parent?.explanation,
|
|
1412
1455
|
};
|
|
1413
1456
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
export declare function KidQuestionCard({ icon, title, points, isExample, review, headerExtra, children, className, contentClassName, }: {
|
|
2
|
+
export declare function KidQuestionCard({ icon, hideIcon, title, titleClassName, points, isExample, review, headerExtra, children, className, contentClassName, }: {
|
|
3
3
|
icon?: ReactNode;
|
|
4
|
-
|
|
4
|
+
hideIcon?: boolean;
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
/** Defaults to `truncate`; pass e.g. `whitespace-normal` to let long titles wrap. */
|
|
7
|
+
titleClassName?: string;
|
|
5
8
|
points?: number;
|
|
6
9
|
isExample?: boolean;
|
|
7
10
|
review?: 'correct' | 'wrong' | null;
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Check, CircleHelp, Star, X } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../../shared/lib/utils';
|
|
5
|
-
export function KidQuestionCard({ icon, title, points, isExample = false, review, headerExtra, children, className, contentClassName, }) {
|
|
6
|
-
return (_jsxs("div", { className: cn('overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className:
|
|
5
|
+
export function KidQuestionCard({ icon, hideIcon = false, title, titleClassName, points, isExample = false, review, headerExtra, children, className, contentClassName, }) {
|
|
6
|
+
return (_jsxs("div", { className: cn('overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [!hideIcon && (_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) })), _jsx("span", { className: cn('text-sm font-medium text-gray-700', titleClassName ?? 'truncate'), children: title }), isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "C\u00E2u v\u00ED d\u1EE5"] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsxs("span", { className: "text-xs text-gray-500", children: [points, " \u0111i\u1EC3m"] })), review === 'correct' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700", children: [_jsx(Check, { className: "h-3 w-3" }), "\u0110\u00FAng"] })), review === 'wrong' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: [_jsx(X, { className: "h-3 w-3" }), "Sai"] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ export declare const MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION = "Look and
|
|
|
3
3
|
export declare const MATCH_WORD_TO_PICTURE_ITEM_TYPE: "MATCH_WORD_TO_PICTURE";
|
|
4
4
|
export declare function toMatchWordToPictureImageUrls(value?: unknown): string[];
|
|
5
5
|
export declare function fromMatchWordToPictureImageUrls(value?: unknown): string | string[] | undefined;
|
|
6
|
+
export declare function toMatchWordToPictureStorageKey(value: string): string;
|
|
6
7
|
export interface MatchWordToPictureGroupItemData {
|
|
7
8
|
imageUrl?: string | string[];
|
|
8
9
|
correctAnswer: string;
|
|
@@ -19,3 +19,17 @@ export function fromMatchWordToPictureImageUrls(value) {
|
|
|
19
19
|
}
|
|
20
20
|
return urls.length === 1 ? urls[0] : urls;
|
|
21
21
|
}
|
|
22
|
+
export function toMatchWordToPictureStorageKey(value) {
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
if (!trimmed)
|
|
25
|
+
return '';
|
|
26
|
+
if (trimmed.startsWith('http://')
|
|
27
|
+
|| trimmed.startsWith('https://')
|
|
28
|
+
|| trimmed.startsWith('blob:')
|
|
29
|
+
|| trimmed.startsWith('data:')
|
|
30
|
+
|| trimmed.startsWith('/images/')
|
|
31
|
+
|| trimmed.startsWith('/_next/')) {
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
return trimmed.replace(/^\/+/, '');
|
|
35
|
+
}
|
|
@@ -18,9 +18,9 @@ export interface MatchingWithLinesGroupData {
|
|
|
18
18
|
instruction: string;
|
|
19
19
|
/** Shared reading passage (HTML) above the matching board. */
|
|
20
20
|
passage?: string;
|
|
21
|
-
audioUrl?: string;
|
|
21
|
+
audioUrl?: string | string[];
|
|
22
22
|
/** Shared cover / portrait image (not a left/right match item). */
|
|
23
|
-
imageUrl?: string;
|
|
23
|
+
imageUrl?: string | string[];
|
|
24
24
|
rightItems: MatchingWithLinesRightItem[];
|
|
25
25
|
items: MatchingWithLinesGroupItemData[];
|
|
26
26
|
explanation?: string;
|
package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js
CHANGED
|
@@ -1,48 +1,34 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, Lightbulb, Volume2 } from 'lucide-react';
|
|
3
|
+
import { BookOpen, Check, ChevronDown, Lightbulb, Volume2, X } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import { toMatchWordToPictureImageUrls } from '../../_shared/types/match-word-to-picture-group.type';
|
|
7
|
-
import {
|
|
6
|
+
import { toMatchWordToPictureImageUrls, toMatchWordToPictureStorageKey, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
7
|
+
import { KidInstruction, KidPrompt, KidQuestionCard, } from '../../_shared/components/compact';
|
|
8
8
|
function normalizeWord(value) {
|
|
9
9
|
return value.trim().toLowerCase();
|
|
10
10
|
}
|
|
11
|
-
function
|
|
11
|
+
function isPlayableUrl(value) {
|
|
12
12
|
return (value.startsWith('http://')
|
|
13
13
|
|| value.startsWith('https://')
|
|
14
14
|
|| value.startsWith('blob:')
|
|
15
15
|
|| value.startsWith('data:'));
|
|
16
16
|
}
|
|
17
|
-
function toPresignKey(value) {
|
|
18
|
-
const trimmed = value.trim();
|
|
19
|
-
if (isPlayableAudioUrl(trimmed)) {
|
|
20
|
-
return trimmed;
|
|
21
|
-
}
|
|
22
|
-
return trimmed.replace(/^\/+/, '');
|
|
23
|
-
}
|
|
24
17
|
function ClientAudio({ url }) {
|
|
25
|
-
const fileKey =
|
|
18
|
+
const fileKey = toMatchWordToPictureStorageKey(url);
|
|
26
19
|
const { previewUrl, isLoading } = usePresignedFileUrl(fileKey);
|
|
27
|
-
const src = previewUrl || (
|
|
20
|
+
const src = previewUrl || (isPlayableUrl(fileKey) ? fileKey : '');
|
|
28
21
|
if (isLoading) {
|
|
29
22
|
return (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx("div", { className: "h-8 w-full max-w-xs animate-pulse rounded-md bg-slate-100" })] }));
|
|
30
23
|
}
|
|
31
|
-
if (!src)
|
|
24
|
+
if (!src)
|
|
32
25
|
return null;
|
|
33
|
-
}
|
|
34
|
-
return (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx("audio", { controls: true, src: src, className: "h-8 max-w-full", preload: "metadata" })] }));
|
|
35
|
-
}
|
|
36
|
-
function isDisplayableImageUrl(value) {
|
|
37
|
-
return (value.startsWith('http://')
|
|
38
|
-
|| value.startsWith('https://')
|
|
39
|
-
|| value.startsWith('blob:')
|
|
40
|
-
|| value.startsWith('data:'));
|
|
26
|
+
return (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx("audio", { controls: true, src: src, className: "h-8 max-w-full", preload: "metadata", children: _jsx("track", { kind: "captions" }) })] }));
|
|
41
27
|
}
|
|
42
28
|
function ClientImageItem({ url, alt, large = false, }) {
|
|
43
|
-
const fileKey =
|
|
29
|
+
const fileKey = toMatchWordToPictureStorageKey(url);
|
|
44
30
|
const { previewUrl, isLoading } = usePresignedFileUrl(fileKey);
|
|
45
|
-
const src = previewUrl || (
|
|
31
|
+
const src = previewUrl || (isPlayableUrl(fileKey) ? fileKey : '');
|
|
46
32
|
if (!url) {
|
|
47
33
|
return (_jsx("div", { className: "flex h-28 w-28 items-center justify-center rounded-lg border border-dashed border-gray-300 bg-gray-50 text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 \u1EA3nh" }));
|
|
48
34
|
}
|
|
@@ -51,9 +37,16 @@ function ClientImageItem({ url, alt, large = false, }) {
|
|
|
51
37
|
}
|
|
52
38
|
return (_jsx("div", { className: cn('relative flex-shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-gray-50', large ? 'max-h-80 w-full max-w-xl' : 'h-28 w-28'), children: isLoading ? (_jsx("div", { className: "flex h-28 w-full items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-violet-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Picture', className: cn('object-contain', large ? 'mx-auto max-h-80 w-auto' : 'h-full w-full') })) }));
|
|
53
39
|
}
|
|
40
|
+
function WordDropdown({ value, wordBank, usedWords, disabled, onChange, }) {
|
|
41
|
+
return (_jsxs("div", { className: "group relative min-w-[160px] sm:min-w-[210px]", children: [_jsxs("select", { value: value, onChange: (event) => onChange(event.target.value), disabled: disabled, className: "h-12 w-full appearance-none rounded-2xl border border-slate-200 bg-white px-4 pr-11 text-sm font-semibold text-slate-700 shadow-sm outline-none transition-all hover:border-violet-300 focus:border-violet-500 focus:ring-4 focus:ring-violet-100/50 disabled:cursor-not-allowed disabled:opacity-60", "aria-label": "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p", children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, index) => {
|
|
42
|
+
const isUsedByOther = usedWords.has(normalizeWord(word)) && normalizeWord(word) !== normalizeWord(value);
|
|
43
|
+
return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${index}`));
|
|
44
|
+
})] }), _jsx("div", { className: "pointer-events-none absolute right-2.5 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full bg-slate-100 text-slate-500 group-focus-within:bg-violet-100 group-focus-within:text-violet-600", children: _jsx(ChevronDown, { className: "h-3.5 w-3.5", strokeWidth: 2.5 }) })] }));
|
|
45
|
+
}
|
|
54
46
|
export function MatchWordToPictureGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
55
47
|
const items = questionData.items || [];
|
|
56
48
|
const wordBank = questionData.wordBank || [];
|
|
49
|
+
const coverUrls = toMatchWordToPictureImageUrls(questionData.imageUrl);
|
|
57
50
|
const handleSelect = (itemIndex, value) => {
|
|
58
51
|
const item = items[itemIndex];
|
|
59
52
|
if (isReviewMode || item?.isExample || !onAnswerChange) {
|
|
@@ -70,7 +63,7 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
70
63
|
usedWords.add(normalizeWord(value));
|
|
71
64
|
}
|
|
72
65
|
});
|
|
73
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null,
|
|
66
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null, coverUrls.length > 0 ? (_jsx("div", { className: "flex flex-wrap justify-center gap-3 rounded-lg border border-slate-100 bg-slate-50 p-3", children: coverUrls.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh bài (${imageIndex + 1})`, large: true }, `mwtp-cover-${imageIndex}`))) })) : null, 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 } })] })) : null, wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-blue-100 bg-blue-50/60 p-3", children: [_jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wider text-blue-700", children: "Word bank" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
|
|
74
67
|
const isUsed = usedWords.has(normalizeWord(word));
|
|
75
68
|
return (_jsx("span", { className: cn('rounded-full border px-3 py-1 text-sm font-semibold', isUsed
|
|
76
69
|
? 'border-blue-500 bg-blue-50 text-blue-800 ring-2 ring-blue-100'
|
|
@@ -82,13 +75,9 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
82
75
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
83
76
|
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
84
77
|
Boolean(item.correctAnswer);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return (_jsx(KidChoiceRow, { letter: optionLetter(wordIndex), selected: isSelected, isReviewMode: isReviewMode, isCorrect: isReviewMode && isWordCorrect, disabled: !canSelect || isUsedByOther, onClick: canSelect && !isUsedByOther
|
|
90
|
-
? () => handleSelect(itemIndex, isSelected ? '' : word)
|
|
91
|
-
: undefined, children: word }, `${word}-${wordIndex}`));
|
|
92
|
-
})), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
78
|
+
const itemImages = toMatchWordToPictureImageUrls(item.imageUrl);
|
|
79
|
+
return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [itemImages.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: itemImages.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(WordDropdown, { value: selectedWord, wordBank: wordBank, usedWords: usedWords, disabled: !canSelect, onChange: (value) => handleSelect(itemIndex, value) }), isReviewMode && (isCorrect
|
|
80
|
+
? _jsx(Check, { className: "h-5 w-5 text-emerald-600" })
|
|
81
|
+
: _jsx(X, { className: "h-5 w-5 text-red-500" }))] })), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
93
82
|
}), 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] }))] }));
|
|
94
83
|
}
|
|
@@ -32,7 +32,7 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
32
32
|
const [title, setTitle] = useState(initialData?.title || '');
|
|
33
33
|
const [instruction, setInstruction] = useState(initialData?.instruction || seeded.instruction || MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION);
|
|
34
34
|
const [passage, setPassage] = useState(initialData?.passage || seeded.passage || '');
|
|
35
|
-
const [audioUrl] = useState(initialData?.audioUrl || seeded.audioUrl || '');
|
|
35
|
+
const [audioUrl, setAudioUrl] = useState(initialData?.audioUrl || seeded.audioUrl || '');
|
|
36
36
|
const [imageUrl, setImageUrl] = useState(toMatchWordToPictureImageUrls(initialData?.imageUrl)[0] || '');
|
|
37
37
|
const [wordBank, setWordBank] = useState(initialData?.wordBank?.length ? initialData.wordBank : seeded.wordBank);
|
|
38
38
|
const [items, setItems] = useState(() => (initialData?.items?.length ? initialData.items : seeded.items));
|
|
@@ -76,12 +76,25 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
76
76
|
nextErrors.push('Word bank cần ít nhất một từ.');
|
|
77
77
|
if (items.length < 1)
|
|
78
78
|
nextErrors.push('Cần ít nhất một câu hỏi.');
|
|
79
|
+
const wordSet = new Set(words.map((word) => word.trim().toLowerCase()));
|
|
80
|
+
const seenAnswers = new Set();
|
|
79
81
|
items.forEach((item, index) => {
|
|
80
82
|
if (!imageUrl.trim() && toMatchWordToPictureImageUrls(item.imageUrl).length === 0) {
|
|
81
83
|
nextErrors.push(`Câu ${index + 1}: cần ảnh (hoặc ảnh chung).`);
|
|
82
84
|
}
|
|
83
|
-
|
|
85
|
+
const answer = item.correctAnswer.trim();
|
|
86
|
+
if (!answer) {
|
|
84
87
|
nextErrors.push(`Câu ${index + 1}: cần chọn từ đúng.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const normalized = answer.toLowerCase();
|
|
91
|
+
if (!wordSet.has(normalized)) {
|
|
92
|
+
nextErrors.push(`Câu ${index + 1}: đáp án không có trong word bank.`);
|
|
93
|
+
}
|
|
94
|
+
if (seenAnswers.has(normalized)) {
|
|
95
|
+
nextErrors.push(`Câu ${index + 1}: từ "${answer}" đã được dùng (mỗi từ chỉ một lần).`);
|
|
96
|
+
}
|
|
97
|
+
seenAnswers.add(normalized);
|
|
85
98
|
});
|
|
86
99
|
setErrors(nextErrors);
|
|
87
100
|
return nextErrors.length === 0;
|
|
@@ -102,7 +115,7 @@ export function MatchWordToPictureGroupCreator({ initialData, onChange, external
|
|
|
102
115
|
if (isClientMode) {
|
|
103
116
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(MatchWordToPictureGroupClient, { questionData: currentPayload })] }));
|
|
104
117
|
}
|
|
105
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(ImageIcon, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i t\u1EEB v\u1EDBi tranh (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwtp-group-title", className: "h-10 border-gray-200 bg-white", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-group-instruction", className: "h-10 border-gray-200 bg-white", value: instruction, onChange: (e) => setInstruction(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsx(MatchWordToPictureGroupImageField, { id: "mwtp-group-
|
|
118
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => _jsx("li", { children: err }, i)) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(ImageIcon, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i t\u1EEB v\u1EDBi tranh (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwtp-group-title", className: "h-10 border-gray-200 bg-white", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwtp-group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "mwtp-group-instruction", className: "h-10 border-gray-200 bg-white", value: instruction, onChange: (e) => setInstruction(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (Passage)" }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsx(MatchWordToPictureGroupImageField, { id: "mwtp-group-audio", label: "Audio chung (optional)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl }), _jsx(MatchWordToPictureGroupImageField, { id: "mwtp-group-shared-image", label: "\u1EA2nh chung (optional)", value: imageUrl, onChange: setImageUrl })] }), 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}`, className: "h-10 border-gray-200 bg-white" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => setWordBank((prev) => prev.filter((_, i) => i !== index)), className: "h-7 w-7", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setWordBank((prev) => [...prev, '']), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm t\u1EEB"] })] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["Tranh (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setItems((prev) => [...prev, createEmptyMatchWordToPictureGroupItem(prev.length + 1)]), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm tranh"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("span", { className: "text-sm font-medium", children: ["C\u00E2u ", item.questionNumber || index + 1] }), _jsxs("div", { className: "flex items-center gap-2", 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, {
|
|
106
119
|
points: parsePointsValue(event.target.value, true),
|
|
107
120
|
}) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => setItems((prev) => prev.filter((_, i) => i !== index).map((entry, i) => ({ ...entry, questionNumber: i + 1 }))), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsx(MatchWordToPictureGroupImageField, { id: `mwtp-group-image-${index}`, label: "\u1EA2nh", value: toMatchWordToPictureImageUrls(item.imageUrl)[0] || '', onChange: (url) => updateItem(index, {
|
|
108
121
|
imageUrl: fromMatchWordToPictureImageUrls(url),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export declare function MatchWordToPictureGroupImageField({ id, label, value, onChange, }: {
|
|
1
|
+
export declare function MatchWordToPictureGroupImageField({ id, label, value, onChange, accept, }: {
|
|
2
2
|
id: string;
|
|
3
3
|
label: string;
|
|
4
4
|
value: string;
|
|
5
5
|
onChange: (url: string) => void;
|
|
6
|
+
accept?: 'image/*' | 'audio/*';
|
|
6
7
|
}): import("react").JSX.Element;
|
|
@@ -6,11 +6,30 @@ import { FileUpload } from '../../../../components/ui/file-upload';
|
|
|
6
6
|
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
7
7
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
8
|
import { Trash2 } from 'lucide-react';
|
|
9
|
-
|
|
9
|
+
import { toMatchWordToPictureStorageKey } from '../../_shared/types/match-word-to-picture-group.type';
|
|
10
|
+
function isDisplayableUrl(value) {
|
|
11
|
+
return (value.startsWith('http://') ||
|
|
12
|
+
value.startsWith('https://') ||
|
|
13
|
+
value.startsWith('blob:') ||
|
|
14
|
+
value.startsWith('data:'));
|
|
15
|
+
}
|
|
16
|
+
function resolveDisplayUrl(fileUrl, previewOverride, fetchedPreviewUrl) {
|
|
17
|
+
if (previewOverride)
|
|
18
|
+
return previewOverride;
|
|
19
|
+
if (fetchedPreviewUrl)
|
|
20
|
+
return fetchedPreviewUrl;
|
|
21
|
+
if (isDisplayableUrl(fileUrl))
|
|
22
|
+
return fileUrl;
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
export function MatchWordToPictureGroupImageField({ id, label, value, onChange, accept = 'image/*', }) {
|
|
10
26
|
const [previewOverride, setPreviewOverride] = useState('');
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept:
|
|
27
|
+
const storageKey = toMatchWordToPictureStorageKey(value);
|
|
28
|
+
const { previewUrl: fetchedPreviewUrl, isLoading } = usePresignedFileUrl(storageKey);
|
|
29
|
+
const displayUrl = resolveDisplayUrl(value, previewOverride, fetchedPreviewUrl);
|
|
30
|
+
const isImage = accept === 'image/*';
|
|
31
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: isImage ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (_jsxs("div", { className: "flex items-start gap-2", children: [isLoading && !displayUrl ? (_jsx("div", { className: "h-20 w-20 animate-pulse rounded-md bg-slate-100" })) : (_jsx(ImagePreview, { src: displayUrl || value, alt: label, className: "h-20 w-20 rounded-md object-contain" })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
32
|
+
setPreviewOverride('');
|
|
33
|
+
onChange('');
|
|
34
|
+
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })), value && !isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-8 w-48 animate-pulse rounded-md bg-slate-100" })) : (_jsx("audio", { controls: true, src: displayUrl, className: "h-8 max-w-full", children: _jsx("track", { kind: "captions" }) })))] }));
|
|
16
35
|
}
|
|
@@ -104,6 +104,6 @@ export function MatchingWithLinesBoard({ leftItems, rightItems, values, correctV
|
|
|
104
104
|
const used = values.includes(item.id);
|
|
105
105
|
return (_jsxs("button", { type: "button", onClick: () => handleRightClick(item.id), disabled: locked, "aria-label": `Connect right item ${item.text || index + 1}`, className: cn('box-border flex h-auto min-h-14 w-max items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1 text-left shadow-sm', selectedLeft !== null && !locked && 'cursor-pointer hover:border-teal-400', used && 'border-teal-200'), children: [_jsx("span", { ref: (node) => {
|
|
106
106
|
rightDotRefs.current[index] = node;
|
|
107
|
-
}, className: cn('h-4 w-4 shrink-0 rounded-full border-2 bg-white', used || selectedLeft !== null ? 'border-teal-600' : 'border-gray-400', selectedLeft !== null && 'bg-teal-100'), "aria-hidden": true }), item.imageUrl ? (_jsx("div", { className: "relative size-11 shrink-0 overflow-hidden rounded-md bg-gray-50", children: _jsx(ResolvedImage, { src: item.imageUrl, alt: item.text || `Match option ${index + 1}`, className: "h-full w-full object-contain" }) })) : (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: item.text
|
|
107
|
+
}, className: cn('h-4 w-4 shrink-0 rounded-full border-2 bg-white', used || selectedLeft !== null ? 'border-teal-600' : 'border-gray-400', selectedLeft !== null && 'bg-teal-100'), "aria-hidden": true }), item.imageUrl ? (_jsx("div", { className: "relative size-11 shrink-0 overflow-hidden rounded-md bg-gray-50", children: _jsx(ResolvedImage, { src: item.imageUrl, alt: item.text || `Match option ${index + 1}`, className: "h-full w-full object-contain" }) })) : null, item.text?.trim() ? (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: item.text })) : !item.imageUrl ? (_jsx("span", { className: "whitespace-nowrap text-sm font-medium text-gray-800", children: "\u2014" })) : null] }, item.id || `right-${index}`));
|
|
108
108
|
}) })] })] }));
|
|
109
109
|
}
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, Lightbulb } from 'lucide-react';
|
|
3
|
+
import { BookOpen, Lightbulb, Volume2 } from 'lucide-react';
|
|
4
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
|
-
import {
|
|
5
|
+
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
6
6
|
import { MatchingWithLinesBoard } from './MatchingWithLinesBoard';
|
|
7
7
|
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
|
+
import { toChooseAnswerGroupImageUrls } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
9
|
+
function ClientImageItem({ url, alt }) {
|
|
10
|
+
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
11
|
+
const src = previewUrl ||
|
|
12
|
+
(typeof url === 'string' && (url.startsWith('http') || url.startsWith('blob:')) ? url : '');
|
|
13
|
+
if (!src && !isLoading) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return (_jsx("div", { className: "max-w-sm overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: isLoading ? (_jsx("div", { className: "flex h-36 w-48 items-center justify-center", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-teal-500" }) })) : (_jsx("img", { src: src, alt: alt || 'Image', className: "max-h-56 w-auto object-contain p-2" })) }));
|
|
17
|
+
}
|
|
8
18
|
function ClientAudio({ url }) {
|
|
9
19
|
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
10
20
|
const src = previewUrl ||
|
|
11
21
|
(typeof url === 'string' && (url.startsWith('http') || url.startsWith('blob:')) ? url : '');
|
|
12
|
-
if (!src && !isLoading)
|
|
22
|
+
if (!src && !isLoading) {
|
|
13
23
|
return null;
|
|
14
|
-
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-indigo-100 bg-indigo-50/70 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-indigo-600" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-indigo-100" })) : (_jsx("audio", { controls: true, className: "h-9 w-full", src: src, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
|
|
15
26
|
}
|
|
16
27
|
export function MatchingWithLinesGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, onAnswersChange, }) {
|
|
17
28
|
const items = questionData.items || [];
|
|
@@ -33,7 +44,7 @@ export function MatchingWithLinesGroupClient({ questionData, isReviewMode = fals
|
|
|
33
44
|
}
|
|
34
45
|
});
|
|
35
46
|
};
|
|
36
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { 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: "
|
|
47
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), toChooseAnswerGroupImageUrls(questionData.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `group-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), 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: "overflow-x-auto", children: _jsx(RichTextHtml, { html: questionData.passage, className: "text-gray-800 leading-relaxed" }) })] })) : null, _jsx(KidQuestionCard, { title: "N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB", children: _jsx(MatchingWithLinesBoard, { leftItems: items.map((item) => ({
|
|
37
48
|
imageUrl: item.imageUrl,
|
|
38
49
|
text: item.text,
|
|
39
50
|
isExample: item.isExample,
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js
CHANGED
|
@@ -4,17 +4,28 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { Label } from '../../../../components/ui/label';
|
|
6
6
|
import { Input } from '../../../../components/ui/input';
|
|
7
|
+
import { Textarea } from '../../../../components/ui/textarea';
|
|
7
8
|
import { PointsInput } from '../../../../components/ui/points-input';
|
|
8
9
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
9
10
|
import { Switch } from '../../../../components/ui/switch';
|
|
10
11
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
11
|
-
import { Link2, Plus, Star, Trash2 } from 'lucide-react';
|
|
12
|
+
import { BookOpen, ImageIcon, Link2, Plus, Star, Trash2, Volume2 } from 'lucide-react';
|
|
13
|
+
import { ChooseAnswerGroupAudioUploadItem, ChooseAnswerGroupImageUploadItem, } from '../choose-the-correct-answer-group/ChooseAnswerGroupImageUpload';
|
|
14
|
+
import { fromChooseAnswerGroupImageUrls, toEditableChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
12
15
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
13
16
|
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
14
17
|
import { MatchingWithLinesGroupClient } from './MatchingWithLinesGroupClient';
|
|
15
18
|
import { MatchingWithLinesImageField } from './MatchingWithLinesImageField';
|
|
16
19
|
import { createEmptyMatchingPair, nextRightId } from './map-matching-with-lines-group-data';
|
|
17
20
|
import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
|
|
21
|
+
function rightOptionLabel(item) {
|
|
22
|
+
const text = item.text?.trim();
|
|
23
|
+
if (text)
|
|
24
|
+
return `${text} (${item.id})`;
|
|
25
|
+
if (item.imageUrl?.trim())
|
|
26
|
+
return `Ảnh (${item.id})`;
|
|
27
|
+
return item.id;
|
|
28
|
+
}
|
|
18
29
|
function parsePointsValue(raw, allowZero = false) {
|
|
19
30
|
if (raw === '')
|
|
20
31
|
return allowZero ? 0 : 1;
|
|
@@ -66,27 +77,36 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
66
77
|
const [rightItems, setRightItems] = useState(() => normalizeRightItems(initialData?.rightItems));
|
|
67
78
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items, normalizeRightItems(initialData?.rightItems)));
|
|
68
79
|
const [passage, setPassage] = useState(initialData?.passage || '');
|
|
69
|
-
const [
|
|
70
|
-
const [
|
|
80
|
+
const [imageUrls, setImageUrls] = useState(() => (toEditableChooseAnswerGroupImageUrls(initialData?.imageUrl)));
|
|
81
|
+
const [audioUrls, setAudioUrls] = useState(() => (toEditableChooseAnswerGroupImageUrls(initialData?.audioUrl)));
|
|
71
82
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
72
83
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
84
|
+
const [isPassageVisible, setIsPassageVisible] = useState(() => {
|
|
85
|
+
const html = initialData?.passage;
|
|
86
|
+
return Boolean(html && html !== '<p></p>' && html !== '<p><br></p>' && html.trim());
|
|
87
|
+
});
|
|
73
88
|
const [errors, setErrors] = useState([]);
|
|
74
89
|
const onChangeRef = useRef(onChange);
|
|
75
90
|
const previousPayloadRef = useRef('');
|
|
76
91
|
useEffect(() => {
|
|
77
92
|
onChangeRef.current = onChange;
|
|
78
93
|
}, [onChange]);
|
|
79
|
-
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextPassage = passage, nextRightItems = rightItems, nextItems = items,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextPassage = passage, nextRightItems = rightItems, nextItems = items, nextAudioUrls = audioUrls, nextImageUrls = imageUrls, nextExplanation = explanation) => {
|
|
95
|
+
const nextAudioUrl = fromChooseAnswerGroupImageUrls(nextAudioUrls);
|
|
96
|
+
const nextImageUrl = fromChooseAnswerGroupImageUrls(nextImageUrls);
|
|
97
|
+
const isContentEmpty = (html) => !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
98
|
+
return {
|
|
99
|
+
title: nextTitle,
|
|
100
|
+
instruction: nextInstruction,
|
|
101
|
+
...(!isContentEmpty(nextPassage) ? { passage: nextPassage.trim() } : {}),
|
|
102
|
+
...(nextAudioUrl !== undefined ? { audioUrl: nextAudioUrl } : {}),
|
|
103
|
+
...(nextImageUrl !== undefined ? { imageUrl: nextImageUrl } : {}),
|
|
104
|
+
rightItems: nextRightItems,
|
|
105
|
+
items: nextItems,
|
|
106
|
+
explanation: nextExplanation,
|
|
107
|
+
points: sumPoints(nextItems),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
90
110
|
const debouncedOnChange = useDebouncedCallback((payload) => {
|
|
91
111
|
onChangeRef.current?.(payload);
|
|
92
112
|
}, 300);
|
|
@@ -98,7 +118,7 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
98
118
|
debouncedOnChange(payload);
|
|
99
119
|
onUnsavedChangesChange?.(true);
|
|
100
120
|
}
|
|
101
|
-
}, [title, instruction, passage,
|
|
121
|
+
}, [title, instruction, passage, audioUrls, imageUrls, rightItems, items, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
102
122
|
const validate = () => {
|
|
103
123
|
const nextErrors = [];
|
|
104
124
|
if (!instruction.trim()) {
|
|
@@ -144,7 +164,7 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
144
164
|
getFormData: () => buildPayload(),
|
|
145
165
|
};
|
|
146
166
|
}
|
|
147
|
-
}, [validationRef, title, instruction, passage,
|
|
167
|
+
}, [validationRef, title, instruction, passage, audioUrls, imageUrls, rightItems, items, explanation]);
|
|
148
168
|
const updateItem = (index, patch) => {
|
|
149
169
|
setItems((prev) => {
|
|
150
170
|
const next = [...prev];
|
|
@@ -189,7 +209,29 @@ export function MatchingWithLinesGroupCreator({ initialData, onChange, externalE
|
|
|
189
209
|
if (isClientMode) {
|
|
190
210
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(MatchingWithLinesGroupClient, { questionData: currentPayload, userAnswers: currentPayload.items.map((item) => item.correctAnswer) })] }));
|
|
191
211
|
}
|
|
192
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(Link2, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwl-title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY", className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }),
|
|
212
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(Link2, { className: "h-3.5 w-3.5" }), title: "N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "mwl-title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "VOCABULARY", className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "mwl-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { id: "mwl-instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION, rows: 2, className: "min-h-[56px] border-gray-200 pb-11" }), _jsxs("div", { className: "absolute bottom-1.5 right-1.5 z-10 flex items-center gap-1 rounded-md border border-indigo-100 bg-white/95 p-0.5 shadow-sm", children: [_jsx("button", { type: "button", title: "Th\u00EAm h\u00ECnh \u1EA3nh", onClick: () => {
|
|
213
|
+
const hasEmpty = imageUrls.some((url) => !url.trim());
|
|
214
|
+
if (imageUrls.length === 0 || !hasEmpty) {
|
|
215
|
+
setImageUrls([...imageUrls, '']);
|
|
216
|
+
}
|
|
217
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${imageUrls.length > 0
|
|
218
|
+
? 'bg-indigo-600 text-white shadow-sm'
|
|
219
|
+
: 'bg-indigo-50 text-indigo-600 hover:bg-indigo-100'}`, children: _jsx(ImageIcon, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", title: "Th\u00EAm audio", onClick: () => {
|
|
220
|
+
const hasEmpty = audioUrls.some((url) => !url.trim());
|
|
221
|
+
if (audioUrls.length === 0 || !hasEmpty) {
|
|
222
|
+
setAudioUrls([...audioUrls, '']);
|
|
223
|
+
}
|
|
224
|
+
}, className: `inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${audioUrls.length > 0
|
|
225
|
+
? 'bg-violet-600 text-white shadow-sm'
|
|
226
|
+
: 'bg-violet-50 text-violet-600 hover:bg-violet-100'}`, children: _jsx(Volume2, { className: "h-4 w-4" }) })] })] })] }), imageUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [imageUrls.map((url, imageIndex) => (_jsx(ChooseAnswerGroupImageUploadItem, { index: imageIndex, itemIndex: -1, imageUrl: url, onChange: (nextUrl) => {
|
|
227
|
+
setImageUrls((current) => current.map((item, idx) => (idx === imageIndex ? nextUrl : item)));
|
|
228
|
+
}, onRemove: () => {
|
|
229
|
+
setImageUrls((current) => current.filter((_, idx) => idx !== imageIndex));
|
|
230
|
+
} }, `mwl-group-image-${imageIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setImageUrls((current) => (current.length ? [...current, ''] : [''])), className: "h-8 gap-1.5 border-dashed border-indigo-300 text-indigo-600 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm \u1EA3nh"] })] })), audioUrls.length > 0 && (_jsxs("div", { className: "space-y-2 rounded-md border border-gray-200 bg-slate-50/80 p-2", children: [audioUrls.map((url, audioIndex) => (_jsx(ChooseAnswerGroupAudioUploadItem, { index: audioIndex, itemIndex: -1, audioUrl: url, onChange: (nextUrl) => {
|
|
231
|
+
setAudioUrls((current) => current.map((item, idx) => (idx === audioIndex ? nextUrl : item)));
|
|
232
|
+
}, onRemove: () => {
|
|
233
|
+
setAudioUrls((current) => current.filter((_, idx) => idx !== audioIndex));
|
|
234
|
+
} }, `mwl-group-audio-${audioIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setAudioUrls((current) => (current.length ? [...current, ''] : [''])), className: "h-8 gap-1.5 border-dashed border-violet-300 text-violet-600 hover:bg-violet-50 hover:text-violet-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm audio"] })] })), !isPassageVisible ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsPassageVisible(true), className: "h-8 gap-1.5 border-gray-200 px-2.5 text-xs text-gray-600 hover:bg-gray-50", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5" }), "Th\u00EAm n\u1ED9i dung b\u00E0i \u0111\u1ECDc"] })) : (_jsxs("div", { className: "relative space-y-1.5 pr-10", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsPassageVisible(false), className: "absolute right-0 top-0 z-10 h-7 px-2 text-xs text-gray-500 hover:text-gray-700", children: "\u1EA8n" }), _jsxs(Label, { className: "text-sm font-semibold text-gray-800", children: ["N\u1ED9i dung b\u00E0i \u0111\u1ECDc ", _jsx("span", { className: "text-xs font-normal text-gray-500", children: "(tu\u1EF3 ch\u1ECDn)" })] }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, variant: "compact", minHeightClassName: "min-h-[72px]" })] }))] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["C\u1EB7p n\u1ED1i (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addPair, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u1EB7p"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u1EB7p ", index + 1] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `mwl-example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `mwl-example-${index}`, className: "flex cursor-pointer 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, {
|
|
193
235
|
points: parsePointsValue(event.target.value, true),
|
|
194
|
-
}) }) })), items.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removePair(index), className: "h-7 w-7 text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-image-${index}`, label: "\u1EA2nh c\u1ED9t tr\u00E1i", value: item.imageUrl || '', onChange: (url) => updateItem(index, { imageUrl: url }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-left-text-${index}`, children: "Ch\u1EEF c\u1ED9t tr\u00E1i (text-to-text)" }), _jsx(Input, { id: `mwl-left-text-${index}`, value: item.text || '', onChange: (e) => updateItem(index, { text: e.target.value }), placeholder: "ball", className: "mt-1 bg-white" })] })] }), _jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-right-image-${index}`, label: "\u1EA2nh c\u1ED9t ph\u1EA3i (image-to-image)", value: rightItems[index]?.imageUrl || '', onChange: (url) => updateRightItem(index, { imageUrl: url || undefined }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-right-text-${index}`, children: "T\u1EEB c\u1ED9t ph\u1EA3i" }), _jsx(Input, { id: `mwl-right-text-${index}`, value: rightItems[index]?.text || '', onChange: (e) => updateRightItem(index, { text: e.target.value }), placeholder: "paintbrush", className: "mt-1 bg-white" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-answer-${index}`, children: "\u0110\u00E1p \u00E1n \u0111\u00FAng (id t\u1EEB)" }), _jsxs("select", { id: `mwl-answer-${index}`, value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), className: "mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), rightItems.map((right) => (
|
|
236
|
+
}) }) })), items.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removePair(index), className: "h-7 w-7 text-red-500 hover:bg-red-50 hover:text-red-600", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-image-${index}`, label: "\u1EA2nh c\u1ED9t tr\u00E1i", value: item.imageUrl || '', onChange: (url) => updateItem(index, { imageUrl: url }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-left-text-${index}`, children: "Ch\u1EEF c\u1ED9t tr\u00E1i (text-to-text)" }), _jsx(Input, { id: `mwl-left-text-${index}`, value: item.text || '', onChange: (e) => updateItem(index, { text: e.target.value }), placeholder: "ball", className: "mt-1 bg-white" })] })] }), _jsxs("div", { className: "space-y-2.5", children: [_jsx(MatchingWithLinesImageField, { id: `mwl-right-image-${index}`, label: "\u1EA2nh c\u1ED9t ph\u1EA3i (image-to-image)", value: rightItems[index]?.imageUrl || '', onChange: (url) => updateRightItem(index, { imageUrl: url || undefined }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-right-text-${index}`, children: "T\u1EEB c\u1ED9t ph\u1EA3i" }), _jsx(Input, { id: `mwl-right-text-${index}`, value: rightItems[index]?.text || '', onChange: (e) => updateRightItem(index, { text: e.target.value }), placeholder: "paintbrush", className: "mt-1 bg-white" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `mwl-answer-${index}`, children: "\u0110\u00E1p \u00E1n \u0111\u00FAng (id t\u1EEB)" }), _jsxs("select", { id: `mwl-answer-${index}`, value: item.correctAnswer, onChange: (e) => updateItem(index, { correctAnswer: e.target.value }), className: "mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm", children: [_jsx("option", { value: "", children: "Ch\u1ECDn t\u1EEB \u0111\u00FAng" }), rightItems.map((right) => (_jsx("option", { value: right.id, children: rightOptionLabel(right) }, right.id)))] })] })] })] })] }, `${rightItems[index]?.id || index}`))), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) })] })] })] }));
|
|
195
237
|
}
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export declare function MatchingWithLinesImageField({ id, label, value, onChange, }: {
|
|
1
|
+
export declare function MatchingWithLinesImageField({ id, label, value, onChange, accept, }: {
|
|
2
2
|
id: string;
|
|
3
3
|
label: string;
|
|
4
4
|
value: string;
|
|
5
5
|
onChange: (url: string) => void;
|
|
6
|
+
accept?: 'image/*' | 'audio/*';
|
|
6
7
|
}): import("react").JSX.Element;
|
package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js
CHANGED
|
@@ -6,21 +6,40 @@ import { FileUpload } from '../../../../components/ui/file-upload';
|
|
|
6
6
|
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
7
7
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
8
|
import { Trash2 } from 'lucide-react';
|
|
9
|
+
function isDisplayableUrl(value) {
|
|
10
|
+
return (value.startsWith('http://') ||
|
|
11
|
+
value.startsWith('https://') ||
|
|
12
|
+
value.startsWith('blob:') ||
|
|
13
|
+
value.startsWith('data:'));
|
|
14
|
+
}
|
|
15
|
+
function toStorageKey(value) {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
return '';
|
|
19
|
+
if (isDisplayableUrl(trimmed))
|
|
20
|
+
return trimmed;
|
|
21
|
+
if (trimmed.startsWith('/images/') || trimmed.startsWith('/_next/')) {
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
return trimmed.replace(/^\/+/, '');
|
|
25
|
+
}
|
|
9
26
|
function resolveDisplayUrl(fileUrl, previewOverride, fetchedPreviewUrl) {
|
|
10
27
|
if (previewOverride)
|
|
11
28
|
return previewOverride;
|
|
12
29
|
if (fetchedPreviewUrl)
|
|
13
30
|
return fetchedPreviewUrl;
|
|
14
|
-
if (
|
|
31
|
+
if (isDisplayableUrl(fileUrl))
|
|
15
32
|
return fileUrl;
|
|
16
33
|
return '';
|
|
17
34
|
}
|
|
18
|
-
export function MatchingWithLinesImageField({ id, label, value, onChange, }) {
|
|
35
|
+
export function MatchingWithLinesImageField({ id, label, value, onChange, accept = 'image/*', }) {
|
|
19
36
|
const [previewOverride, setPreviewOverride] = useState('');
|
|
20
|
-
const
|
|
37
|
+
const storageKey = toStorageKey(value);
|
|
38
|
+
const { previewUrl: fetchedPreviewUrl, isLoading } = usePresignedFileUrl(storageKey);
|
|
21
39
|
const displayUrl = resolveDisplayUrl(value, previewOverride, fetchedPreviewUrl);
|
|
22
|
-
|
|
40
|
+
const isImage = accept === 'image/*';
|
|
41
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: isImage ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (_jsxs("div", { className: "flex items-start gap-2", children: [isLoading && !displayUrl ? (_jsx("div", { className: "h-20 w-20 animate-pulse rounded-md bg-slate-100" })) : (_jsx(ImagePreview, { src: displayUrl || value, alt: label, className: "h-20 w-20 rounded-md object-contain" })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
23
42
|
setPreviewOverride('');
|
|
24
43
|
onChange('');
|
|
25
|
-
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }))] }));
|
|
44
|
+
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })), value && !isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-8 w-48 animate-pulse rounded-md bg-slate-100" })) : (_jsx("audio", { controls: true, src: displayUrl, className: "h-8 max-w-full", children: _jsx("track", { kind: "captions" }) })))] }));
|
|
26
45
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
|
|
2
|
+
import { fromChooseAnswerGroupImageUrls, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
3
|
+
function readSharedMedia(value) {
|
|
4
|
+
return fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(value));
|
|
5
|
+
}
|
|
2
6
|
function isRecord(value) {
|
|
3
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
8
|
}
|
|
@@ -72,9 +76,9 @@ export function mapQuestionToMatchingWithLinesGroupData(question) {
|
|
|
72
76
|
const meta = asRecord(content.meta ?? answer.meta);
|
|
73
77
|
if (Array.isArray(content.items)) {
|
|
74
78
|
const items = content.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
75
|
-
const audioUrl =
|
|
79
|
+
const audioUrl = readSharedMedia(meta.audioUrl) ?? readSharedMedia(answer.audioUrl);
|
|
76
80
|
const passage = asString(meta.passage).trim() || asString(answer.passage).trim();
|
|
77
|
-
const imageUrl =
|
|
81
|
+
const imageUrl = readSharedMedia(meta.imageUrl) ?? readSharedMedia(answer.imageUrl);
|
|
78
82
|
return {
|
|
79
83
|
title: asString(meta.title) || asString(answer.title),
|
|
80
84
|
instruction: asString(meta.instruction) ||
|
|
@@ -91,9 +95,9 @@ export function mapQuestionToMatchingWithLinesGroupData(question) {
|
|
|
91
95
|
}
|
|
92
96
|
if (Array.isArray(answer.items)) {
|
|
93
97
|
const items = answer.items.map((item, index) => mapMatchingWithLinesGroupItem(item, index, fallbackAnswers[index]));
|
|
94
|
-
const audioUrl =
|
|
98
|
+
const audioUrl = readSharedMedia(answer.audioUrl);
|
|
95
99
|
const passage = asString(answer.passage).trim();
|
|
96
|
-
const imageUrl =
|
|
100
|
+
const imageUrl = readSharedMedia(answer.imageUrl);
|
|
97
101
|
return {
|
|
98
102
|
title: asString(answer.title),
|
|
99
103
|
instruction: asString(answer.instruction) || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/matching-with-lines-group.type';
|
|
2
|
+
import { fromChooseAnswerGroupImageUrls, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
2
3
|
function sumGradablePoints(items) {
|
|
3
4
|
return items.reduce((total, item) => {
|
|
4
5
|
if (item.isExample)
|
|
@@ -30,16 +31,16 @@ export const transformMatchingWithLinesGroup = (question) => {
|
|
|
30
31
|
});
|
|
31
32
|
const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
|
|
32
33
|
const passage = typeof answerData?.passage === 'string' ? answerData.passage.trim() : '';
|
|
33
|
-
const audioUrl =
|
|
34
|
-
const imageUrl =
|
|
34
|
+
const audioUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answerData?.audioUrl));
|
|
35
|
+
const imageUrl = fromChooseAnswerGroupImageUrls(toChooseAnswerGroupImageUrls(answerData?.imageUrl));
|
|
35
36
|
return {
|
|
36
37
|
apiContent: {
|
|
37
38
|
meta: {
|
|
38
39
|
instruction: answerData?.instruction || MATCHING_WITH_LINES_GROUP_DEFAULT_INSTRUCTION,
|
|
39
40
|
...(title ? { title } : {}),
|
|
40
41
|
...(passage ? { passage } : {}),
|
|
41
|
-
...(audioUrl ? { audioUrl } : {}),
|
|
42
|
-
...(imageUrl ? { imageUrl } : {}),
|
|
42
|
+
...(audioUrl !== undefined ? { audioUrl } : {}),
|
|
43
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
43
44
|
},
|
|
44
45
|
rightItems: rightItems.map((item) => {
|
|
45
46
|
const imageUrl = item.imageUrl?.trim() || '';
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
|
-
import { Check, X,
|
|
4
|
+
import { Check, X, Library } from 'lucide-react';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
|
-
import {
|
|
7
|
+
import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
8
|
import { detectWfsfBankMode, isOptionsWordEntry, normalizeSharedWordBank, } from './wfsf-word-bank.utils';
|
|
9
9
|
import { expandWfsfCorrectAlts, formatWfsfCorrectLabel, isWfsfBlankCorrect, primaryWfsfAnswers, } from '../../../../shared/lib/utils/wfsf-blank-match';
|
|
10
10
|
function normalizeWfsfBlankStyle(raw) {
|
|
@@ -138,9 +138,13 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
138
138
|
// Header = page rubric. Pearson preview maps draft.instruction → explanation.
|
|
139
139
|
const rawExplanation = String(explanation || questionData.explanation || '').trim();
|
|
140
140
|
const headerInstruction = String(instruction || dataInstruction || rawExplanation || '').trim();
|
|
141
|
-
|
|
141
|
+
// Header reads "Bài x: rubric" — the question type name is never shown.
|
|
142
|
+
const numberLabel = typeof questionNumber === 'number' && questionNumber > 0
|
|
143
|
+
? `Bài ${questionNumber}`
|
|
144
|
+
: '';
|
|
145
|
+
const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction || DEFAULT_WFSF_HEADER);
|
|
142
146
|
// Do not re-show the same rubric under "Giải thích".
|
|
143
|
-
const displayExplanation = rawExplanation && rawExplanation !==
|
|
147
|
+
const displayExplanation = rawExplanation && rawExplanation !== headerInstruction ? rawExplanation : '';
|
|
144
148
|
const handleInputChange = useCallback((key, value) => {
|
|
145
149
|
if (isReviewMode)
|
|
146
150
|
return;
|
|
@@ -178,8 +182,8 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
178
182
|
: undefined, size: isLetterBlank ? undefined : isPhraseBlank ? 28 : 10, className: cn('inline-block bg-slate-50/40 text-center text-sm font-semibold outline-none transition-all align-baseline hover:bg-slate-100/60 focus:bg-teal-50/60 focus:ring-0', isLetterBlank
|
|
179
183
|
? 'h-7 w-7 rounded-md border px-0 uppercase'
|
|
180
184
|
: isPhraseBlank
|
|
181
|
-
? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-
|
|
182
|
-
: 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-
|
|
185
|
+
? 'min-w-[16rem] max-w-[36rem] w-[28ch] rounded-none border-0 border-b-2 bg-transparent px-1 py-0.5'
|
|
186
|
+
: 'min-w-[4.5rem] max-w-[8rem] w-[10ch] rounded-none border-0 border-b-2 bg-transparent px-1 py-0.5', isReviewMode
|
|
183
187
|
? isCorrectFill
|
|
184
188
|
? 'border-green-400 bg-green-50 text-green-800'
|
|
185
189
|
: isWrongFill
|
|
@@ -278,12 +282,12 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
278
282
|
.map((node, index) => renderNode(node, `root-${index}`))
|
|
279
283
|
.filter((node) => node !== null);
|
|
280
284
|
}, [content, renderBlank]);
|
|
281
|
-
return (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
return (_jsx("div", { className: "space-y-4", children: _jsxs(KidQuestionCard, { hideIcon: true, title: headerTitle, titleClassName: "whitespace-normal break-words text-base font-normal text-gray-800", review: isReviewMode && allFilled ? (isAllCorrect ? 'correct' : 'wrong') : null, children: [title && (_jsx(KidPrompt, { children: _jsx("h2", { className: "text-sm font-semibold text-gray-800", children: title }) })), allAudios.length > 0 && (_jsx("div", { className: "flex flex-col items-center gap-2", children: allAudios.map((audio, idx) => (_jsx(ClientAudioItem, { url: audio }, idx))) })), allImages.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: allImages.map((img, idx) => (_jsx(ClientImageItem, { url: img, alt: title ? `${title} (${idx + 1})` : undefined }, idx))) })), bankMode === 'WITH_WORD_BANK' && sharedWords.length > 0 && (_jsxs("div", { className: "rounded-lg border border-purple-200 bg-gradient-to-r from-purple-50 to-pink-50 p-3", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx(Library, { className: "h-4 w-4 text-purple-600" }), _jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-purple-700", children: "Word Bank" })] }), _jsx("div", { className: "flex flex-wrap gap-2", children: sharedWords.map((item, index) => {
|
|
286
|
+
const used = usedSharedWords.has(item.word.trim().toLowerCase());
|
|
287
|
+
return (_jsxs("span", { className: cn('rounded-full border px-3 py-1 text-sm font-medium', item.isExample
|
|
288
|
+
? 'border-amber-300 bg-amber-100 text-amber-800'
|
|
289
|
+
: used
|
|
290
|
+
? 'border-purple-200 bg-white/60 text-purple-400 line-through'
|
|
291
|
+
: 'border-purple-300 bg-white text-purple-800'), children: [item.word, item.isExample ? ' (ex)' : ''] }, `${item.word}-${index}`));
|
|
292
|
+
}) })] })), _jsx("div", { className: "structured-form-content space-y-2 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5 text-base leading-loose text-gray-800", children: renderedContent }), isReviewMode && !isAllCorrect && (_jsxs("div", { className: "rounded-lg border border-green-200 bg-green-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-green-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: expectedBlankKeys.map((key) => (_jsxs("span", { className: "rounded-lg border border-green-300 bg-green-100 px-3 py-1.5 text-sm font-semibold text-green-800", children: [`{${key}}`, ": ", formatWfsfCorrectLabel(answers[key])] }, key))) })] })), isReviewMode && displayExplanation && (_jsxs("div", { className: "rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-blue-600", children: "Gi\u1EA3i th\u00EDch" }), _jsx("p", { className: "mt-1 text-sm text-blue-800", children: displayExplanation })] }))] }) }));
|
|
289
293
|
}
|
|
@@ -4,8 +4,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
* Movers fallback review cases (isReviewMode=true).
|
|
5
5
|
* Source: oe-exam-fe ReviewQuestionRenderer.tsx
|
|
6
6
|
*/
|
|
7
|
-
import { MoversAnswerTheQuestionRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, ChooseThenAnswerGroupRenderer, SortWordsIntoCategoriesRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
-
import { transformAnswerTheQuestion, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, transformChooseThenAnswerGroup, transformSortWordsIntoCategories, } from '../../../components/exams/take/utils/question-transformers';
|
|
7
|
+
import { MoversAnswerTheQuestionRenderer, MoversGridFillRenderer, MoversLabelThePictureRenderer, MoversMatchByWritingAnswerRenderer, MoversMatchWordToPictureRenderer, MoversMatchWordToPictureGroupRenderer, MoversReadingPassageRenderer, MoversTrueFalseRenderer, MoversTrueFalseCorrectGroupRenderer, MoversFillInBlankGroupRenderer, MoversMatchingWithLinesGroupRenderer, MoversCrossOutWordGroupRenderer, MoversWordFillParagraphRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderingRenderer, MoversWordOrderingGroupRenderer, MoversWriteSentencesRenderer, MoversWritingRenderer, MoversCrosswordPuzzleRenderer, ChooseThenAnswerGroupRenderer, SortWordsIntoCategoriesRenderer, } from '../../../components/exams/take/components/question-renderers';
|
|
8
|
+
import { transformAnswerTheQuestion, transformGridFill, transformLabelPicture, transformMatchByWritingAnswer, transformMatchWordToPicture, transformMatchWordToPictureGroup, transformReadingPassage, transformTrueFalse, transformTrueFalseCorrectGroup, transformFillInBlankGroup, transformMatchingWithLinesGroup, transformWordFillParagraph, transformWordFillStructuredForm, transformWordOrdering, transformWordOrderingGroupTake, transformWriteSentences, transformWriteShortLetter, transformCrosswordPuzzle, transformChooseThenAnswerGroup, transformSortWordsIntoCategories, } from '../../../components/exams/take/utils/question-transformers';
|
|
9
9
|
import { normalizeCrossOutWordGroupAnswer, transformCrossOutWordGroup, } from '../../../components/exams/take/utils/cross-out-word-group.transformer';
|
|
10
10
|
function asApiQuestions(questions) {
|
|
11
11
|
return questions;
|
|
@@ -183,6 +183,10 @@ export function renderMoversReviewBody(ctx) {
|
|
|
183
183
|
const data = transformMatchByWritingAnswer(apiQuestions);
|
|
184
184
|
return (_jsx(MoversMatchByWritingAnswerRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, audioUrl: part.audioUrl, options: data.options, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, viewMode: data.viewMode, groupImageUrl: data.groupImageUrl, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
|
|
185
185
|
}
|
|
186
|
+
case 'MATCH_WORD_TO_PICTURE_GROUP': {
|
|
187
|
+
const data = transformMatchWordToPictureGroup(apiQuestions);
|
|
188
|
+
return (_jsx(MoversMatchWordToPictureGroupRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: data.questions.filter((q) => !q.isExample).length, audioUrl: part.audioUrl, data: data, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true }));
|
|
189
|
+
}
|
|
186
190
|
case 'MATCH_WORD_TO_PICTURE': {
|
|
187
191
|
const data = transformMatchWordToPicture(apiQuestions);
|
|
188
192
|
return (_jsx(MoversMatchWordToPictureRenderer, { partNumber: partNo, partName: part.name, instruction: part.instructions || data.instruction, questionCount: questionCount, wordBank: data.wordBank, questions: data.questions, answers: answers, onAnswerChange: handleAnswerChange, isReviewMode: true, correctAnswerTextMap: correctAnswerTextMap, isCorrectMap: isCorrectMap }));
|
|
@@ -11,6 +11,7 @@ const FLATTENED_ANSWER_TYPES = new Set([
|
|
|
11
11
|
'CHOOSE_THEN_ANSWER_GROUP',
|
|
12
12
|
'FILL_IN_BLANK_GROUP',
|
|
13
13
|
'MATCHING_WITH_LINES_GROUP',
|
|
14
|
+
'MATCH_WORD_TO_PICTURE_GROUP',
|
|
14
15
|
]);
|
|
15
16
|
export function transformResultAnswersForReview(answers, templateParts = []) {
|
|
16
17
|
const groups = groupAnswers(answers);
|
|
@@ -282,6 +283,17 @@ function transformStudentAnswer(questionId, questionType, value) {
|
|
|
282
283
|
typeof item === 'string' ? item : '',
|
|
283
284
|
]));
|
|
284
285
|
}
|
|
286
|
+
if (questionType === 'MATCH_WORD_TO_PICTURE_GROUP') {
|
|
287
|
+
const answer = Array.isArray(record.answer)
|
|
288
|
+
? record.answer
|
|
289
|
+
: Array.isArray(value)
|
|
290
|
+
? value
|
|
291
|
+
: [];
|
|
292
|
+
return Object.fromEntries(answer.map((item, index) => [
|
|
293
|
+
`${questionId}-item-${index}`,
|
|
294
|
+
typeof item === 'string' ? item : '',
|
|
295
|
+
]));
|
|
296
|
+
}
|
|
285
297
|
if (questionType === 'CHOOSE_THE_CORRECT_ANSWER' || questionType === 'CHOOSE_CORRECT_ADJECTIVE') {
|
|
286
298
|
const rawAns = record.answer ?? record.optionId ?? record.optionIds;
|
|
287
299
|
const ids = getOptionIds(questionId, rawAns);
|
|
@@ -511,14 +511,29 @@ const reverseMatchingWithLinesGroup = (apiQuestion) => {
|
|
|
511
511
|
questionNumber: asNumber(item.questionNumber, index + 1),
|
|
512
512
|
};
|
|
513
513
|
});
|
|
514
|
+
const readMatchingMedia = (value) => {
|
|
515
|
+
if (Array.isArray(value)) {
|
|
516
|
+
const cleaned = value
|
|
517
|
+
.filter((item) => typeof item === 'string')
|
|
518
|
+
.map((item) => item.trim())
|
|
519
|
+
.filter((item) => item !== '');
|
|
520
|
+
if (cleaned.length === 0)
|
|
521
|
+
return undefined;
|
|
522
|
+
return cleaned.length === 1 ? cleaned[0] : cleaned;
|
|
523
|
+
}
|
|
524
|
+
const single = asString(value).trim();
|
|
525
|
+
return single || undefined;
|
|
526
|
+
};
|
|
527
|
+
const audioUrl = readMatchingMedia(meta.audioUrl);
|
|
528
|
+
const imageUrl = readMatchingMedia(meta.imageUrl);
|
|
514
529
|
return {
|
|
515
530
|
content: '',
|
|
516
531
|
answer: {
|
|
517
532
|
title: asString(meta.title),
|
|
518
533
|
instruction: asString(meta.instruction, 'Look and match.'),
|
|
519
534
|
...(asString(meta.passage).trim() ? { passage: asString(meta.passage).trim() } : {}),
|
|
520
|
-
...(
|
|
521
|
-
...(
|
|
535
|
+
...(audioUrl !== undefined ? { audioUrl } : {}),
|
|
536
|
+
...(imageUrl !== undefined ? { imageUrl } : {}),
|
|
522
537
|
rightItems,
|
|
523
538
|
items,
|
|
524
539
|
explanation: asString(apiQuestion.explanation),
|
|
@@ -20,9 +20,9 @@ export interface MatchingWithLinesGroupContent {
|
|
|
20
20
|
meta?: {
|
|
21
21
|
instruction?: string;
|
|
22
22
|
title?: string;
|
|
23
|
-
audioUrl?: string;
|
|
23
|
+
audioUrl?: string | string[];
|
|
24
24
|
passage?: string;
|
|
25
|
-
imageUrl?: string;
|
|
25
|
+
imageUrl?: string | string[];
|
|
26
26
|
};
|
|
27
27
|
rightItems: MatchingWithLinesRightItem[];
|
|
28
28
|
items: MatchingWithLinesGroupItem[];
|