@tinyweb_dev/oe-exam-sdk 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
  2. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
  3. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
  4. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
  5. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
  6. package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
  7. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
  8. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
  9. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
  10. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
  11. package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
  12. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +18 -20
  13. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
  14. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
  15. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
  16. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
  17. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
  18. package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
  19. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
  20. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
  21. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
  22. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
  23. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
  24. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
  25. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
  26. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
  27. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
  28. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
  29. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
  30. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
  31. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
  32. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
  33. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
  34. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
  35. package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
  36. package/dist/components/questions/viewer/QuestionViewer.js +22 -9
  37. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
  38. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
  39. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
  40. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
  41. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
  42. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
  43. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
  44. package/dist/shared/lib/utils/question-reverse-transform.js +8 -0
  45. package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
  46. package/dist/shared/types/questions/answer-the-question-group.js +5 -0
  47. package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
  48. package/package.json +1 -1
@@ -2,18 +2,28 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
- export function TrueFalseGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
5
+ export function TrueFalseGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
6
6
  const items = questionData.items || [];
7
+ const handleSelect = (itemIndex, value) => {
8
+ const item = items[itemIndex];
9
+ if (isReviewMode || item?.isExample || !onAnswerChange) {
10
+ return;
11
+ }
12
+ const next = items.map((_, index) => userAnswers[index]);
13
+ next[itemIndex] = value;
14
+ onAnswerChange(next);
15
+ };
7
16
  return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => {
8
- const userValue = userAnswers[itemIndex];
17
+ const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
9
18
  const correctAnswer = item.correctAnswer;
19
+ const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
10
20
  return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "flex items-center gap-3", children: [
11
21
  { value: true, label: 'True' },
12
22
  { value: false, label: 'False' },
13
23
  ].map((opt) => {
14
24
  const isCorrect = opt.value === correctAnswer;
15
25
  const isSelected = opt.value === userValue;
16
- return (_jsxs("div", { className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', isReviewMode && isCorrect
26
+ return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, opt.value), className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', canSelect ? 'cursor-pointer hover:border-blue-300' : 'cursor-default', isReviewMode && isCorrect
17
27
  ? 'border-green-500 bg-green-50 text-green-700'
18
28
  : isReviewMode && isSelected && !isCorrect
19
29
  ? 'border-red-500 bg-red-50 text-red-700'
@@ -2,7 +2,8 @@ import type { Question } from '../../../shared/types/entities/question.types';
2
2
  type AnswerTheQuestionGroupViewerProps = {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
- userAnswer?: string[];
5
+ userAnswer?: unknown;
6
+ onSubmitAnswer?: (answer: string[]) => void;
6
7
  };
7
- export declare function AnswerTheQuestionGroupViewer({ question, isReviewMode, userAnswer, }: AnswerTheQuestionGroupViewerProps): import("react").JSX.Element;
8
+ export declare function AnswerTheQuestionGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: AnswerTheQuestionGroupViewerProps): import("react").JSX.Element;
8
9
  export {};
@@ -2,10 +2,17 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { AnswerTheQuestionGroupClient } from '../types/answer-the-question-group/AnswerTheQuestionGroupClient';
4
4
  import { mapQuestionToAnswerTheQuestionGroupData } from '../types/answer-the-question-group/map-answer-the-question-group-data';
5
- export function AnswerTheQuestionGroupViewer({ question, isReviewMode = false, userAnswer, }) {
5
+ function readStringAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value.map((item) => String(item ?? ''));
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer.map((item) => String(item ?? ''));
11
+ }
12
+ return undefined;
13
+ }
14
+ export function AnswerTheQuestionGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToAnswerTheQuestionGroupData(question);
7
- const reviewAnswers = isReviewMode && !userAnswer
8
- ? questionData.items.map((item) => item.expectedAnswers[0] || '')
9
- : userAnswer;
10
- return (_jsx(AnswerTheQuestionGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
16
+ const storedAnswers = readStringAnswers(userAnswer);
17
+ return (_jsx(AnswerTheQuestionGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: storedAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
11
18
  }
@@ -1,9 +1,10 @@
1
1
  import type { Question } from '../../../shared/types/entities/question.types';
2
- import type { ChooseThenAnswerStudentItemAnswer } from '../_shared/types/choose-then-answer-group.type';
3
- export declare function ChooseThenAnswerGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
2
+ import type { ChooseThenAnswerGradeDetail, ChooseThenAnswerStudentItemAnswer } from '../_shared/types/choose-then-answer-group.type';
3
+ export declare function ChooseThenAnswerGroupViewer({ question, isReviewMode, userAnswer, gradeDetails, onSubmitAnswer, }: {
4
4
  question: Question;
5
5
  isReviewMode?: boolean;
6
6
  userAnswer?: unknown;
7
+ gradeDetails?: ChooseThenAnswerGradeDetail[];
7
8
  onSubmitAnswer?: (answer: {
8
9
  answer: ChooseThenAnswerStudentItemAnswer[];
9
10
  }) => void;
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { ChooseThenAnswerGroupClient } from '../types/choose-then-answer-group/ChooseThenAnswerGroupClient';
5
- import { mapQuestionToChooseThenAnswerGroupData, parseStudentAnswer, } from '../types/choose-then-answer-group/map-choose-then-answer-group-data';
5
+ import { mapQuestionToChooseThenAnswerGroupData, parseStudentAnswer, toCtaItemResults, } from '../types/choose-then-answer-group/map-choose-then-answer-group-data';
6
6
  function readStudentAnswers(value, itemCount) {
7
7
  const record = value && typeof value === 'object' && !Array.isArray(value)
8
8
  ? value
@@ -16,7 +16,7 @@ function readStudentAnswers(value, itemCount) {
16
16
  return undefined;
17
17
  return Array.from({ length: itemCount }, (_, index) => parseStudentAnswer(raw[index]));
18
18
  }
19
- export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
19
+ export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, userAnswer, gradeDetails, onSubmitAnswer, }) {
20
20
  const questionData = mapQuestionToChooseThenAnswerGroupData(question);
21
21
  const seed = questionData.items.map((item) => ({
22
22
  optionId: item.isExample ? item.optionId : '',
@@ -35,5 +35,6 @@ export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, us
35
35
  setLocalAnswers(updated);
36
36
  onSubmitAnswer?.({ answer: updated });
37
37
  };
38
- return (_jsx(ChooseThenAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localAnswers), onAnswerChange: isReviewMode ? undefined : handleChange }));
38
+ const displayedAnswers = isReviewMode && !parsedUser ? [] : (parsedUser ?? localAnswers);
39
+ return (_jsx(ChooseThenAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode && !parsedUser ? undefined : displayedAnswers, itemResults: toCtaItemResults(gradeDetails, questionData.items, displayedAnswers), onAnswerChange: isReviewMode ? undefined : handleChange }));
39
40
  }
@@ -2,6 +2,8 @@ import type { Question } from '../../../shared/types/entities/question.types';
2
2
  type CrossOutWordGroupViewerProps = {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
+ userAnswer?: unknown;
6
+ onSubmitAnswer?: (answer: unknown[]) => void;
5
7
  };
6
- export declare function CrossOutWordGroupViewer({ question, isReviewMode, }: CrossOutWordGroupViewerProps): import("react").JSX.Element;
8
+ export declare function CrossOutWordGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: CrossOutWordGroupViewerProps): import("react").JSX.Element;
7
9
  export {};
@@ -2,7 +2,20 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { CrossOutWordGroupClient } from '../types/cross-out-word-group/CrossOutWordGroupClient';
4
4
  import { mapQuestionToCrossOutWordGroupData } from '../types/cross-out-word-group/map-cross-out-word-group-data';
5
- export function CrossOutWordGroupViewer({ question, isReviewMode = false, }) {
5
+ function readCrossOutAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value;
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer;
11
+ }
12
+ return undefined;
13
+ }
14
+ export function CrossOutWordGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToCrossOutWordGroupData(question);
7
- return (_jsx(CrossOutWordGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode ? questionData.items.map((item) => item.correctAnswer) : [] }));
16
+ const storedAnswers = readCrossOutAnswers(userAnswer);
17
+ const userAnswers = isReviewMode && !storedAnswers
18
+ ? questionData.items.map((item) => item.correctAnswer)
19
+ : storedAnswers;
20
+ return (_jsx(CrossOutWordGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: userAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
8
21
  }
@@ -4,6 +4,14 @@ interface QuestionViewerProps {
4
4
  onSubmitAnswer?: (answer: any) => void;
5
5
  isReviewMode?: boolean;
6
6
  userAnswer?: any;
7
+ gradeDetails?: Array<{
8
+ itemId: string;
9
+ isCorrect?: boolean;
10
+ userAnswer?: unknown;
11
+ correctAnswer?: unknown;
12
+ partialScore?: number;
13
+ feedback?: string;
14
+ }>;
7
15
  }
8
- export declare function QuestionViewer({ question, onSubmitAnswer, isReviewMode, userAnswer, }: QuestionViewerProps): import("react").JSX.Element;
16
+ export declare function QuestionViewer({ question, onSubmitAnswer, isReviewMode, userAnswer, gradeDetails, }: QuestionViewerProps): import("react").JSX.Element;
9
17
  export {};
@@ -38,7 +38,7 @@ import { MatchWordToPictureViewer } from './MatchWordToPictureViewer';
38
38
  import { MatchWordToPictureGroupViewer } from './MatchWordToPictureGroupViewer';
39
39
  import { MatchColumnsToMakeSentencesViewer } from './MatchColumnsToMakeSentencesViewer';
40
40
  import { WordOrderingGroupViewer } from './WordOrderingGroupViewer';
41
- export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, }) {
41
+ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false, userAnswer, gradeDetails, }) {
42
42
  const [selectedAnswer, setSelectedAnswer] = useState(userAnswer || null);
43
43
  const [playCount, setPlayCount] = useState(0);
44
44
  const audioRef = useRef(null);
@@ -477,6 +477,19 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
477
477
  }
478
478
  return (_jsxs("div", { className: "space-y-3", children: [content.title ? _jsx("h3", { className: "text-center text-lg font-bold uppercase text-slate-800", children: content.title }) : null, content.imageUrl ? (_jsx("div", { className: "flex justify-center", children: _jsx("img", { src: String(content.imageUrl), alt: "", className: "max-h-[180px] rounded-lg object-contain" }) })) : null, _jsx("div", { className: "space-y-2 text-base leading-relaxed text-slate-800", children: paragraph.split('\n').map((line, li) => (_jsx("div", { className: "flex flex-wrap items-baseline", children: renderLine(line, `wfp-${li}`) }, `wfp-l${li}`))) }), isReviewMode && correctAnswers.length > 0 ? (_jsxs("div", { className: "rounded-lg border border-emerald-200 bg-emerald-50/80 p-2.5 text-sm text-emerald-800", children: [_jsx("span", { className: "font-semibold", children: "\u0110\u00E1p \u00E1n: " }), correctAnswers.map((a, i) => (_jsxs("span", { className: "mr-2", children: ["(", i + 1, ") ", _jsx("span", { className: "font-bold underline", children: a || '—' })] }, `wfp-a${i}`)))] })) : null] }));
479
479
  };
480
+ const renderCrosswordPuzzle = () => {
481
+ const raw = userAnswer && typeof userAnswer === 'object' && !Array.isArray(userAnswer)
482
+ ? userAnswer
483
+ : {};
484
+ const answers = raw.answers && typeof raw.answers === 'object' && !Array.isArray(raw.answers)
485
+ ? raw.answers
486
+ : raw;
487
+ return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.correctAnswer ?? question.answer, isReviewMode: isReviewMode, userAnswers: answers, onAnswerChange: isReviewMode
488
+ ? undefined
489
+ : (key, value) => {
490
+ onSubmitAnswer?.({ answers: { ...answers, [key]: value } });
491
+ } }));
492
+ };
480
493
  const renderQuestion = () => {
481
494
  const resolvedType = String(question.type ||
482
495
  question.questionType ||
@@ -488,9 +501,9 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
488
501
  case 'CHOOSE_THE_CORRECT_ANSWER_GROUP':
489
502
  return (_jsx(ChooseTheCorrectAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
490
503
  case 'TRUE_FALSE_GROUP':
491
- return (_jsx(TrueFalseGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
504
+ return (_jsx(TrueFalseGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
492
505
  case 'TRUE_FALSE_CORRECT_GROUP':
493
- return (_jsx(TrueFalseCorrectGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
506
+ return (_jsx(TrueFalseCorrectGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
494
507
  case 'FILL_IN_BLANK_GROUP':
495
508
  return (_jsx(FillInBlankGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
496
509
  case 'MATCHING_WITH_LINES_GROUP':
@@ -498,11 +511,11 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
498
511
  case 'MATCH_COLUMNS_TO_MAKE_SENTENCES':
499
512
  return (_jsx(MatchColumnsToMakeSentencesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
500
513
  case 'CROSS_OUT_WORD_GROUP':
501
- return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode }));
514
+ return (_jsx(CrossOutWordGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
502
515
  case 'ANSWER_THE_QUESTION':
503
516
  return (_jsx(AnswerTheQuestionViewer, { question: question, isReviewMode: isReviewMode, userAnswer: typeof userAnswer === 'string' ? userAnswer : undefined, onSubmitAnswer: onSubmitAnswer }));
504
517
  case 'ANSWER_THE_QUESTION_GROUP':
505
- return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
518
+ return (_jsx(AnswerTheQuestionGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
506
519
  case 'SPEAKING_CUE_CARD':
507
520
  return (_jsx(SpeakingCueCardClient, { questionData: mapQuestionToSpeakingCueCardData(question), isReviewMode: isReviewMode }));
508
521
  case 'SPONTANEOUS_QA':
@@ -511,7 +524,7 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
511
524
  return (_jsx(SpontaneousQaGroupViewer, { question: question, isReviewMode: isReviewMode }));
512
525
  case 'WORD_ORDER_AND_MATCH_GROUP':
513
526
  case 'WORD_ORDER_AND_MATCH':
514
- return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer }));
527
+ return (_jsx(WordOrderAndMatchGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
515
528
  case 'WORD_ORDERING_GROUP':
516
529
  return (_jsx(WordOrderingGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
517
530
  case 'MATCH_WORD_TO_PICTURE_GROUP':
@@ -551,13 +564,13 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
551
564
  case 'WORD_FILL_STRUCTURED_FORM':
552
565
  return (_jsx(WordFillStructuredFormViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
553
566
  case 'CROSSWORD_PUZZLE':
554
- return (_jsx(CrosswordPuzzleClient, { content: question.content, correctAnswer: question.answer, isReviewMode: isReviewMode, userAnswers: userAnswer }));
567
+ return renderCrosswordPuzzle();
555
568
  case 'FIND_WORDS_IN_MATRIX':
556
- return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.answer, showSolution: isReviewMode }));
569
+ return (_jsx(FindWordsInMatrixClient, { content: question.content, correctAnswer: question.correctAnswer ?? question.answer, showSolution: isReviewMode, userAnswer: userAnswer, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
557
570
  case 'SORT_WORDS_INTO_CATEGORIES':
558
571
  return (_jsx(SortWordsIntoCategoriesViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
559
572
  case 'CHOOSE_THEN_ANSWER_GROUP':
560
- return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, onSubmitAnswer: onSubmitAnswer }));
573
+ return (_jsx(ChooseThenAnswerGroupViewer, { question: question, isReviewMode: isReviewMode, userAnswer: userAnswer, gradeDetails: gradeDetails, onSubmitAnswer: onSubmitAnswer }));
561
574
  default:
562
575
  return _jsxs("div", { children: ["Lo\u1EA1i c\u00E2u h\u1ECFi kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3: ", question.type] });
563
576
  }
@@ -3,7 +3,8 @@ import type { TrueFalseCorrectStudentItemAnswer } from '../_shared/types/true-fa
3
3
  type TrueFalseCorrectGroupViewerProps = {
4
4
  question: Question;
5
5
  isReviewMode?: boolean;
6
- userAnswer?: TrueFalseCorrectStudentItemAnswer[];
6
+ userAnswer?: unknown;
7
+ onSubmitAnswer?: (answer: TrueFalseCorrectStudentItemAnswer[]) => void;
7
8
  };
8
- export declare function TrueFalseCorrectGroupViewer({ question, isReviewMode, userAnswer, }: TrueFalseCorrectGroupViewerProps): import("react").JSX.Element;
9
+ export declare function TrueFalseCorrectGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: TrueFalseCorrectGroupViewerProps): import("react").JSX.Element;
9
10
  export {};
@@ -2,13 +2,23 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { TrueFalseCorrectGroupClient } from '../types/true-false-correct-group/TrueFalseCorrectGroupClient';
4
4
  import { mapQuestionToTrueFalseCorrectGroupData } from '../types/true-false-correct-group/map-true-false-correct-group-data';
5
- export function TrueFalseCorrectGroupViewer({ question, isReviewMode = false, userAnswer, }) {
5
+ function readStudentAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value;
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer;
11
+ }
12
+ return undefined;
13
+ }
14
+ export function TrueFalseCorrectGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToTrueFalseCorrectGroupData(question);
7
- const reviewAnswers = isReviewMode && !userAnswer
16
+ const storedAnswers = readStudentAnswers(userAnswer);
17
+ const reviewAnswers = isReviewMode && !storedAnswers
8
18
  ? questionData.items.map((item) => ({
9
19
  isTrue: item.isTrue,
10
20
  correction: item.correction,
11
21
  }))
12
- : userAnswer;
13
- return (_jsx(TrueFalseCorrectGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
22
+ : storedAnswers;
23
+ return (_jsx(TrueFalseCorrectGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
14
24
  }
@@ -2,7 +2,10 @@ import type { Question } from '../../../shared/types/entities/question.types';
2
2
  type TrueFalseGroupViewerProps = {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
- userAnswer?: boolean[];
5
+ userAnswer?: boolean[] | {
6
+ answer?: boolean[];
7
+ };
8
+ onSubmitAnswer?: (answer: boolean[]) => void;
6
9
  };
7
- export declare function TrueFalseGroupViewer({ question, isReviewMode, userAnswer, }: TrueFalseGroupViewerProps): import("react").JSX.Element;
10
+ export declare function TrueFalseGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: TrueFalseGroupViewerProps): import("react").JSX.Element;
8
11
  export {};
@@ -2,10 +2,20 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { TrueFalseGroupClient } from '../types/true-false-group/TrueFalseGroupClient';
4
4
  import { mapQuestionToTrueFalseGroupData } from '../types/true-false-group/map-true-false-group-data';
5
- export function TrueFalseGroupViewer({ question, isReviewMode = false, userAnswer, }) {
5
+ function readBooleanAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value;
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer;
11
+ }
12
+ return undefined;
13
+ }
14
+ export function TrueFalseGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToTrueFalseGroupData(question);
7
- const reviewAnswers = isReviewMode && !userAnswer
16
+ const storedAnswers = readBooleanAnswers(userAnswer);
17
+ const reviewAnswers = isReviewMode && !storedAnswers
8
18
  ? questionData.items.map((item) => item.correctAnswer)
9
- : userAnswer;
10
- return (_jsx(TrueFalseGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
19
+ : storedAnswers;
20
+ return (_jsx(TrueFalseGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
11
21
  }
@@ -1,6 +1,10 @@
1
1
  import type { Question } from '../../../shared/types/entities/question.types';
2
- export declare function WordOrderAndMatchGroupViewer({ question, isReviewMode, userAnswer, }: {
2
+ export declare function WordOrderAndMatchGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
- userAnswer?: any;
5
+ userAnswer?: unknown;
6
+ onSubmitAnswer?: (answer: Array<{
7
+ orderedPhrase: string;
8
+ matchLabel: string;
9
+ }>) => void;
6
10
  }): import("react").JSX.Element;
@@ -76,20 +76,40 @@ function resolveGroup(question) {
76
76
  items: itemsRaw.map((item, idx) => mapItem(item, idx, answersRaw[idx])),
77
77
  };
78
78
  }
79
- export function WordOrderAndMatchGroupViewer({ question, isReviewMode = false, userAnswer, }) {
79
+ function readMatchAnswers(value) {
80
+ if (Array.isArray(value)) {
81
+ return value;
82
+ }
83
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
84
+ return value.answer;
85
+ }
86
+ return undefined;
87
+ }
88
+ export function WordOrderAndMatchGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
80
89
  const { instruction, matchOptions, items } = resolveGroup(question);
81
90
  const questionData = {
82
91
  instruction,
83
92
  matchOptions,
84
93
  items,
85
94
  };
86
- const autoUserAnswers = isReviewMode
95
+ const storedAnswers = readMatchAnswers(userAnswer);
96
+ const autoUserAnswers = isReviewMode && !storedAnswers
87
97
  ? items.map((item) => ({
88
98
  orderedPhrase: item.orderedPhrase,
89
99
  matchLabel: item.matchLabel,
90
100
  }))
91
- : Array.isArray(userAnswer)
92
- ? userAnswer
93
- : undefined;
94
- return (_jsx(WordOrderAndMatchGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: autoUserAnswers }));
101
+ : storedAnswers;
102
+ return (_jsx(WordOrderAndMatchGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: autoUserAnswers, onAnswerChange: isReviewMode
103
+ ? undefined
104
+ : (itemIndex, next) => {
105
+ const current = items.map((_, index) => {
106
+ const existing = storedAnswers?.[index];
107
+ return {
108
+ orderedPhrase: existing?.orderedPhrase || '',
109
+ matchLabel: existing?.matchLabel || '',
110
+ };
111
+ });
112
+ current[itemIndex] = next;
113
+ onSubmitAnswer?.(current);
114
+ } }));
95
115
  }
@@ -31,6 +31,6 @@ export function ReviewAnswerTheQuestionGroupRenderer({ partNumber, partName, que
31
31
  ? 'border-red-200 bg-red-50 text-red-800'
32
32
  : 'border-gray-200 bg-gray-50 text-gray-700'}`, children: question.isExample
33
33
  ? (question.exampleAnswer || '(Câu mẫu)')
34
- : (studentAnswer || '(Chưa trả lời)') }), expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n m\u1EABu (AI)" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })), feedback && (_jsx("p", { className: "text-xs text-violet-700", children: feedback }))] })] }, question.id));
34
+ : (studentAnswer || '(Chưa trả lời)') }), expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })), feedback && (_jsx("p", { className: "text-xs text-violet-700", children: feedback }))] })] }, question.id));
35
35
  })] }));
36
36
  }
@@ -315,6 +315,7 @@ const reverseAnswerTheQuestionGroup = (apiQuestion) => {
315
315
  items,
316
316
  explanation: asString(apiQuestion.explanation),
317
317
  points: items.reduce((total, item) => (item.isExample ? total : total + item.points), 0),
318
+ ...(meta.gradingType === 'MAPPING' ? { gradingType: 'MAPPING' } : {}),
318
319
  },
319
320
  };
320
321
  };
@@ -404,6 +405,13 @@ const reverseChooseThenAnswerGroup = (apiQuestion) => {
404
405
  question: asString(itemContent.question),
405
406
  options,
406
407
  optionId: asString(rawAnswer.optionId) || options[0]?.id || '',
408
+ expectedAnswers: (() => {
409
+ const raw = asArray(rawAnswer.expectedAnswers)
410
+ .filter((value) => typeof value === 'string')
411
+ .map((value) => value.trim())
412
+ .filter((value) => value !== '');
413
+ return raw.length > 0 ? raw : [''];
414
+ })(),
407
415
  isExample,
408
416
  points: isExample ? 0 : asNumber(item.points, 1),
409
417
  questionNumber: asNumber(item.questionNumber, index + 1),
@@ -1,4 +1,8 @@
1
- export declare const ANSWER_THE_QUESTION_GROUP_GRADING_TYPE: "AI";
1
+ export declare const ANSWER_THE_QUESTION_GROUP_GRADING_TYPES: readonly ["AI", "MAPPING"];
2
+ export type AnswerTheQuestionGroupGradingType = (typeof ANSWER_THE_QUESTION_GROUP_GRADING_TYPES)[number];
3
+ /** Default for newly created groups. */
4
+ export declare const ANSWER_THE_QUESTION_GROUP_GRADING_TYPE: AnswerTheQuestionGroupGradingType;
5
+ export declare function resolveAnswerTheQuestionGroupGradingType(value: unknown): AnswerTheQuestionGroupGradingType;
2
6
  export interface AnswerTheQuestionGroupItemContent {
3
7
  question: string;
4
8
  imageUrl?: string | string[];
@@ -20,8 +24,9 @@ export interface AnswerTheQuestionGroupContent {
20
24
  passage?: string;
21
25
  audioUrl?: string;
22
26
  imageUrl?: string | string[];
23
- gradingType: typeof ANSWER_THE_QUESTION_GROUP_GRADING_TYPE;
24
- isAiGraded: true;
27
+ /** AI = LLM short-answer; MAPPING = exact-match expectedAnswers. */
28
+ gradingType: AnswerTheQuestionGroupGradingType;
29
+ isAiGraded: boolean;
25
30
  };
26
31
  items: AnswerTheQuestionGroupItem[];
27
32
  }
@@ -1 +1,6 @@
1
+ export const ANSWER_THE_QUESTION_GROUP_GRADING_TYPES = ['AI', 'MAPPING'];
2
+ /** Default for newly created groups. */
1
3
  export const ANSWER_THE_QUESTION_GROUP_GRADING_TYPE = 'AI';
4
+ export function resolveAnswerTheQuestionGroupGradingType(value) {
5
+ return value === 'MAPPING' ? 'MAPPING' : 'AI';
6
+ }
@@ -4,6 +4,7 @@ export interface ChooseThenAnswerOption {
4
4
  }
5
5
  export interface ChooseThenAnswerItemAnswer {
6
6
  optionId: string;
7
+ expectedAnswers?: string[];
7
8
  }
8
9
  export interface ChooseThenAnswerStudentItemAnswer {
9
10
  optionId: string;
@@ -27,6 +28,8 @@ export interface ChooseThenAnswerGroupContent {
27
28
  meta?: {
28
29
  instruction?: string;
29
30
  title?: string;
31
+ gradingType?: 'AI';
32
+ isAiGraded?: boolean;
30
33
  };
31
34
  items: ChooseThenAnswerGroupItem[];
32
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Reusable OceanEdu question components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",