@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
@@ -13,7 +13,7 @@ export function ChooseThenAnswerGroupRenderer({ partNumber, partName, questionCo
13
13
  });
14
14
  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(ChooseThenAnswerGroupClient, { questionData: {
15
15
  title: data.title,
16
- instruction: data.instruction,
16
+ instruction: '',
17
17
  items: data.questions.map((item) => ({
18
18
  question: item.question,
19
19
  options: item.options,
@@ -19,6 +19,8 @@ export interface AnswerTheQuestionGroupData {
19
19
  items: AnswerTheQuestionGroupItemData[];
20
20
  explanation?: string;
21
21
  points?: number;
22
+ /** Preserved from content.meta; AI | MAPPING. */
23
+ gradingType?: 'AI' | 'MAPPING';
22
24
  }
23
25
  export interface AnswerTheQuestionGroupCreatorProps {
24
26
  initialData?: AnswerTheQuestionGroupData;
@@ -36,4 +38,5 @@ export interface AnswerTheQuestionGroupClientProps {
36
38
  questionData: AnswerTheQuestionGroupData;
37
39
  isReviewMode?: boolean;
38
40
  userAnswers?: string[];
41
+ onAnswerChange?: (answers: string[]) => void;
39
42
  }
@@ -7,6 +7,7 @@ export interface ChooseThenAnswerOption {
7
7
  }
8
8
  export interface ChooseThenAnswerItemAnswer {
9
9
  optionId: string;
10
+ expectedAnswers?: string[];
10
11
  }
11
12
  export interface ChooseThenAnswerStudentItemAnswer {
12
13
  optionId: string;
@@ -16,6 +17,7 @@ export interface ChooseThenAnswerGroupItemData {
16
17
  question: string;
17
18
  options: ChooseThenAnswerOption[];
18
19
  optionId: string;
20
+ expectedAnswers?: string[];
19
21
  isExample?: boolean;
20
22
  points: number;
21
23
  questionNumber: number;
@@ -39,9 +41,24 @@ export interface ChooseThenAnswerGroupCreatorProps {
39
41
  getFormData?: () => ChooseThenAnswerGroupData;
40
42
  } | null>;
41
43
  }
44
+ export interface ChooseThenAnswerItemResult {
45
+ mappingCorrect?: boolean;
46
+ aiCorrect?: boolean;
47
+ partialScore?: number;
48
+ feedback?: string;
49
+ }
50
+ export interface ChooseThenAnswerGradeDetail {
51
+ itemId: string;
52
+ isCorrect?: boolean;
53
+ userAnswer?: unknown;
54
+ correctAnswer?: unknown;
55
+ partialScore?: number;
56
+ feedback?: string;
57
+ }
42
58
  export interface ChooseThenAnswerGroupClientProps {
43
59
  questionData: ChooseThenAnswerGroupData;
44
60
  isReviewMode?: boolean;
45
61
  userAnswers?: ChooseThenAnswerStudentItemAnswer[];
62
+ itemResults?: ChooseThenAnswerItemResult[];
46
63
  onAnswerChange?: (index: number, next: ChooseThenAnswerStudentItemAnswer) => void;
47
64
  }
@@ -39,4 +39,5 @@ export interface CrossOutWordGroupClientProps {
39
39
  questionData: CrossOutWordGroupData;
40
40
  isReviewMode?: boolean;
41
41
  userAnswers?: Array<CrossOutWordGroupItemAnswer | null | undefined>;
42
+ onAnswerChange?: (answers: Array<CrossOutWordGroupItemAnswer | null>) => void;
42
43
  }
@@ -39,4 +39,5 @@ export interface TrueFalseCorrectGroupClientProps {
39
39
  questionData: TrueFalseCorrectGroupData;
40
40
  isReviewMode?: boolean;
41
41
  userAnswers?: TrueFalseCorrectStudentItemAnswer[];
42
+ onAnswerChange?: (answers: TrueFalseCorrectStudentItemAnswer[]) => void;
42
43
  }
@@ -28,4 +28,5 @@ export interface TrueFalseGroupClientProps {
28
28
  questionData: TrueFalseGroupData;
29
29
  isReviewMode?: boolean;
30
30
  userAnswers?: boolean[];
31
+ onAnswerChange?: (answers: boolean[]) => void;
31
32
  }
@@ -1,2 +1,2 @@
1
1
  import type { AnswerTheQuestionGroupClientProps } from '../../_shared/types/answer-the-question-group.type';
2
- export declare function AnswerTheQuestionGroupClient({ questionData, isReviewMode, userAnswers, }: AnswerTheQuestionGroupClientProps): import("react").JSX.Element;
2
+ export declare function AnswerTheQuestionGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: AnswerTheQuestionGroupClientProps): import("react").JSX.Element;
@@ -3,36 +3,30 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { BookOpen, CircleHelp, Lightbulb, Sparkles, Star, Volume2 } from 'lucide-react';
4
4
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
5
  import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
6
- function isDisplayableMediaUrl(value) {
7
- return (value.startsWith('http://')
8
- || value.startsWith('https://')
9
- || value.startsWith('blob:')
10
- || value.startsWith('data:')
11
- || value.startsWith('/'));
12
- }
13
- function isPlayableAudioUrl(value) {
6
+ function isAbsoluteMediaUrl(value) {
14
7
  return (value.startsWith('http://')
15
8
  || value.startsWith('https://')
16
9
  || value.startsWith('blob:')
17
10
  || value.startsWith('data:'));
18
11
  }
12
+ function isPublicAssetPath(value) {
13
+ return value.startsWith('/images/') || value.startsWith('/_next/');
14
+ }
19
15
  function toPresignKey(value) {
20
16
  const trimmed = value.trim();
21
- if (isPlayableAudioUrl(trimmed)) {
17
+ if (isAbsoluteMediaUrl(trimmed) || isPublicAssetPath(trimmed)) {
22
18
  return trimmed;
23
19
  }
24
20
  return trimmed.replace(/^\/+/, '');
25
21
  }
26
22
  function MediaPreview({ src, kind = 'image', alt }) {
27
- const fileKey = kind === 'audio' ? toPresignKey(src) : src;
23
+ const fileKey = toPresignKey(src);
28
24
  const { previewUrl, isLoading } = usePresignedFileUrl(fileKey);
29
25
  if (isLoading) {
30
26
  return _jsx("div", { className: "h-16 w-16 animate-pulse rounded-md bg-slate-100" });
31
27
  }
32
28
  const url = previewUrl
33
- || (kind === 'audio'
34
- ? (isPlayableAudioUrl(fileKey) ? fileKey : '')
35
- : (isDisplayableMediaUrl(src) ? src : ''));
29
+ || (isAbsoluteMediaUrl(fileKey) || isPublicAssetPath(fileKey) ? fileKey : '');
36
30
  if (!url) {
37
31
  return null;
38
32
  }
@@ -45,13 +39,17 @@ function MediaPreview({ src, kind = 'image', alt }) {
45
39
  target.src = '/images/placeholder-image.svg';
46
40
  } }) }));
47
41
  }
48
- export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
42
+ export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
49
43
  const items = questionData.items || [];
50
44
  return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.audioUrl && (_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(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: `Hình ảnh (${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: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
51
45
  const userValue = userAnswers[itemIndex] || '';
52
46
  const expectedAnswers = Array.isArray(item.expectedAnswers)
53
47
  ? item.expectedAnswers.filter((answer) => answer.trim() !== '')
54
48
  : [];
55
- 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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700", children: userValue || (isReviewMode ? '(Chưa trả lời)' : 'Học sinh sẽ nhập câu trả lời tại đây') }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n m\u1EABu (AI)" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] }))] })] }, `${item.questionNumber}-${itemIndex}`));
49
+ 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: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), isReviewMode || item.isExample || !onAnswerChange ? (userValue ? (_jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700", children: userValue })) : null) : (_jsx("textarea", { value: userValue, onChange: (event) => {
50
+ const next = items.map((_, index) => userAnswers[index] || '');
51
+ next[itemIndex] = event.target.value;
52
+ onAnswerChange(next);
53
+ }, placeholder: "Nh\u1EADp c\u00E2u tr\u1EA3 l\u1EDDi...", rows: 2, className: "w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })), isReviewMode && expectedAnswers.length > 0 ? (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n" }), _jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm text-gray-700", children: expectedAnswers.map((answer) => (_jsx("li", { children: answer }, answer))) })] })) : isReviewMode && !userValue ? (_jsx("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 \u0111\u00E1p \u00E1n m\u1EABu" })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
56
54
  }), 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] }))] }));
57
55
  }
@@ -60,6 +60,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
60
60
  const [errors, setErrors] = useState([]);
61
61
  const onChangeRef = useRef(onChange);
62
62
  const previousPayloadRef = useRef('');
63
+ const gradingTypeRef = useRef(initialData?.gradingType);
63
64
  useEffect(() => {
64
65
  onChangeRef.current = onChange;
65
66
  }, [onChange]);
@@ -71,6 +72,7 @@ export function AnswerTheQuestionGroupCreator({ initialData, onChange, externalE
71
72
  items: nextItems,
72
73
  explanation: nextExplanation,
73
74
  points: sumPoints(nextItems),
75
+ ...(gradingTypeRef.current ? { gradingType: gradingTypeRef.current } : {}),
74
76
  });
75
77
  const debouncedOnChange = useDebouncedCallback((payload) => {
76
78
  onChangeRef.current?.(payload);
@@ -77,6 +77,7 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
77
77
  items,
78
78
  explanation,
79
79
  points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
80
+ ...(meta.gradingType === 'MAPPING' ? { gradingType: 'MAPPING' } : {}),
80
81
  };
81
82
  }
82
83
  if (Array.isArray(answer.items)) {
@@ -90,6 +91,7 @@ export function mapQuestionToAnswerTheQuestionGroupData(question) {
90
91
  items: answer.items,
91
92
  explanation,
92
93
  points: typeof answer.points === 'number' ? answer.points : 1,
94
+ ...(answer.gradingType === 'MAPPING' ? { gradingType: 'MAPPING' } : {}),
93
95
  };
94
96
  }
95
97
  return {
@@ -1,7 +1,12 @@
1
1
  import { fromAnswerTheQuestionGroupImageUrls, toAnswerTheQuestionGroupImageUrls, } from '../../_shared/types/answer-the-question-group.type';
2
+ import { resolveAnswerTheQuestionGroupGradingType } from '../../../../shared/types/questions/answer-the-question-group';
2
3
  const CHILD_QUESTION_TYPE = 'ANSWER_THE_QUESTION';
3
- const GRADING_TYPE = 'AI';
4
4
  const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
5
+ function readGradingType(question) {
6
+ const answer = question.answer;
7
+ const content = question.content;
8
+ return resolveAnswerTheQuestionGroupGradingType(answer?.gradingType ?? content?.meta?.gradingType);
9
+ }
5
10
  function normalizeExpectedAnswers(value) {
6
11
  if (!Array.isArray(value)) {
7
12
  return [];
@@ -26,6 +31,8 @@ function isContentEmpty(html) {
26
31
  export const transformAnswerTheQuestionGroup = (question) => {
27
32
  const answerData = question.answer;
28
33
  const items = Array.isArray(answerData?.items) ? answerData.items : [];
34
+ const gradingType = readGradingType(question);
35
+ const isAiGraded = gradingType === 'AI';
29
36
  if (items.length === 0) {
30
37
  const emptyAudioUrl = typeof answerData?.audioUrl === 'string' ? answerData.audioUrl.trim() : '';
31
38
  return {
@@ -33,8 +40,8 @@ export const transformAnswerTheQuestionGroup = (question) => {
33
40
  meta: {
34
41
  instruction: answerData?.instruction || DEFAULT_INSTRUCTION,
35
42
  ...(emptyAudioUrl ? { audioUrl: emptyAudioUrl } : {}),
36
- gradingType: GRADING_TYPE,
37
- isAiGraded: true,
43
+ gradingType,
44
+ isAiGraded,
38
45
  },
39
46
  items: [],
40
47
  },
@@ -66,8 +73,8 @@ export const transformAnswerTheQuestionGroup = (question) => {
66
73
  ...(!isContentEmpty(passage) ? { passage } : {}),
67
74
  ...(audioUrl ? { audioUrl } : {}),
68
75
  ...(imageUrl.length ? { imageUrl } : {}),
69
- gradingType: GRADING_TYPE,
70
- isAiGraded: true,
76
+ gradingType,
77
+ isAiGraded,
71
78
  },
72
79
  items: apiItems,
73
80
  },
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { BookOpen, Check, CircleHelp, Lightbulb, Star, Volume2 } from 'lucide-react';
3
+ import { BookOpen, Check, CircleHelp, Lightbulb, Star, Volume2, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
6
  import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
@@ -27,6 +27,18 @@ function ClientAudio({ url }) {
27
27
  function optionLetter(index) {
28
28
  return String.fromCharCode(65 + index);
29
29
  }
30
+ function reviewOptionTone(isReviewMode, isCorrect, isSelected) {
31
+ if (isReviewMode && isCorrect) {
32
+ return { border: 'border-green-400 bg-green-50', badge: 'bg-green-500 text-white' };
33
+ }
34
+ if (isReviewMode && isSelected) {
35
+ return { border: 'border-red-400 bg-red-50', badge: 'bg-red-500 text-white' };
36
+ }
37
+ if (isSelected) {
38
+ return { border: 'border-blue-400 bg-blue-50', badge: 'bg-blue-500 text-white' };
39
+ }
40
+ return { border: 'border-gray-200 bg-white', badge: 'bg-gray-100 text-gray-600' };
41
+ }
30
42
  export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
31
43
  const items = questionData.items || [];
32
44
  const handleSelect = (itemIndex, optionId) => {
@@ -38,34 +50,20 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
38
50
  next[itemIndex] = optionId;
39
51
  onAnswerChange(next);
40
52
  };
41
- 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 })), 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: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), items.map((item, itemIndex) => {
53
+ 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 })), 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" }) })] })), items.map((item, itemIndex) => {
42
54
  const selectedId = userAnswers[itemIndex];
43
55
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
44
56
  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: [!isRichTextEmpty(item.question) && (_jsx(RichTextHtml, { html: item.question, className: "text-sm font-semibold leading-relaxed text-gray-800" })), toChooseAnswerGroupImageUrls(item.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `${item.questionNumber}-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), item.optionType === 'image' ? (_jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: item.options.map((option, optionIndex) => {
45
57
  const isCorrect = option.id === item.correctAnswer;
46
58
  const isSelected = option.id === selectedId;
59
+ const tone = reviewOptionTone(isReviewMode, isCorrect, isSelected);
47
60
  const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
48
- return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, option.id), className: cn('flex flex-col items-center gap-2 rounded-lg border-2 px-3 py-2.5 text-sm text-left', isReviewMode && isCorrect
49
- ? 'border-green-400 bg-green-50'
50
- : isSelected
51
- ? 'border-blue-400 bg-blue-50'
52
- : 'border-gray-200 bg-white', canSelect && 'cursor-pointer hover:border-blue-300', !canSelect && 'cursor-default'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', isReviewMode && isCorrect
53
- ? 'bg-green-500 text-white'
54
- : isSelected
55
- ? 'bg-blue-500 text-white'
56
- : 'bg-gray-100 text-gray-600'), children: optionLetter(optionIndex) }), optionImageSrc ? (_jsx(ClientImageItem, { url: optionImageSrc, alt: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx("span", { className: "text-xs text-gray-500", children: `Đáp án ${optionLetter(optionIndex)}` })), isReviewMode && isCorrect && _jsx(Check, { className: "h-4 w-4 text-green-600" })] }, option.id || optionIndex));
61
+ return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, option.id), className: cn('flex flex-col items-center gap-2 rounded-lg border-2 px-3 py-2.5 text-sm text-left', tone.border, canSelect && 'cursor-pointer hover:border-blue-300', !canSelect && 'cursor-default'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', tone.badge), children: optionLetter(optionIndex) }), optionImageSrc ? (_jsx(ClientImageItem, { url: optionImageSrc, alt: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx("span", { className: "text-xs text-gray-500", children: `Đáp án ${optionLetter(optionIndex)}` })), isReviewMode && isCorrect ? (_jsx(Check, { className: "h-4 w-4 text-green-600" })) : isReviewMode && isSelected ? (_jsx(X, { className: "h-4 w-4 text-red-600" })) : null] }, option.id || optionIndex));
57
62
  }) })) : (_jsx("div", { className: "space-y-2", children: item.options.map((option, optionIndex) => {
58
63
  const isCorrect = option.id === item.correctAnswer;
59
64
  const isSelected = option.id === selectedId;
60
- return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, option.id), className: cn('flex w-full items-center gap-3 rounded-lg border-2 px-3 py-2.5 text-sm text-left', isReviewMode && isCorrect
61
- ? 'border-green-400 bg-green-50'
62
- : isSelected
63
- ? 'border-blue-400 bg-blue-50'
64
- : 'border-gray-200 bg-white', canSelect && 'cursor-pointer hover:border-blue-300', !canSelect && 'cursor-default'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', isReviewMode && isCorrect
65
- ? 'bg-green-500 text-white'
66
- : isSelected
67
- ? 'bg-blue-500 text-white'
68
- : 'bg-gray-100 text-gray-600'), children: optionLetter(optionIndex) }), isRichTextEmpty(option.text) ? (_jsx("span", { className: "flex-1 text-gray-800", children: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx(RichTextHtml, { html: option.text, className: "flex-1 text-gray-800" })), isReviewMode && isCorrect && _jsx(Check, { className: "h-4 w-4 text-green-600" })] }, option.id || optionIndex));
65
+ const tone = reviewOptionTone(isReviewMode, isCorrect, isSelected);
66
+ return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, option.id), className: cn('flex w-full items-center gap-3 rounded-lg border-2 px-3 py-2.5 text-sm text-left', tone.border, canSelect && 'cursor-pointer hover:border-blue-300', !canSelect && 'cursor-default'), children: [_jsx("span", { className: cn('flex h-7 w-7 items-center justify-center rounded-md text-xs font-bold', tone.badge), children: optionLetter(optionIndex) }), isRichTextEmpty(option.text) ? (_jsx("span", { className: "flex-1 text-gray-800", children: `Đáp án ${optionLetter(optionIndex)}` })) : (_jsx(RichTextHtml, { html: option.text, className: "flex-1 text-gray-800" })), isReviewMode && isCorrect ? (_jsx(Check, { className: "h-4 w-4 text-green-600" })) : isReviewMode && isSelected ? (_jsx(X, { className: "h-4 w-4 text-red-600" })) : null] }, option.id || optionIndex));
69
67
  }) }))] })] }, `${item.questionNumber}-${itemIndex}`));
70
68
  }), 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] }))] }));
71
69
  }
@@ -1,2 +1,2 @@
1
1
  import type { ChooseThenAnswerGroupClientProps } from '../../_shared/types/choose-then-answer-group.type';
2
- export declare function ChooseThenAnswerGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: ChooseThenAnswerGroupClientProps): import("react").JSX.Element;
2
+ export declare function ChooseThenAnswerGroupClient({ questionData, isReviewMode, userAnswers, itemResults, onAnswerChange, }: ChooseThenAnswerGroupClientProps): import("react").JSX.Element;
@@ -1,20 +1,58 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Lightbulb, Star } from 'lucide-react';
3
+ import { Check, Lightbulb, Star, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  function emptyAnswer() {
6
6
  return { optionId: '', text: '' };
7
7
  }
8
- export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
8
+ function formatHalf(points) {
9
+ const half = points * 0.5;
10
+ return Number.isInteger(half) ? String(half) : half.toFixed(1);
11
+ }
12
+ function badgeClass(params) {
13
+ if (params.isExample) {
14
+ return 'bg-amber-500';
15
+ }
16
+ if (!params.isReviewMode || !params.hasAttempt) {
17
+ return 'bg-indigo-600';
18
+ }
19
+ if (params.partialScore != null) {
20
+ if (params.partialScore >= params.points - 1e-9) {
21
+ return 'bg-emerald-500';
22
+ }
23
+ if (params.partialScore > 0) {
24
+ return 'bg-amber-500';
25
+ }
26
+ return 'bg-red-500';
27
+ }
28
+ if (params.mappingCorrect || params.hasText) {
29
+ return 'bg-amber-500';
30
+ }
31
+ return 'bg-red-500';
32
+ }
33
+ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], itemResults, onAnswerChange, }) {
9
34
  const canEdit = Boolean(onAnswerChange) && !isReviewMode;
10
35
  const items = questionData.items || [];
11
- return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-slate-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-slate-800", children: questionData.instruction }))] })), items.map((item, itemIndex) => {
36
+ const hasAttempt = Boolean(itemResults?.length) ||
37
+ userAnswers.some((answer) => Boolean(answer?.optionId || answer?.text?.trim()));
38
+ return (_jsxs("div", { className: "space-y-2", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-lg border border-indigo-100 bg-indigo-50/80 px-3 py-2", children: [questionData.title && (_jsx("p", { className: "text-[11px] font-semibold text-indigo-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-indigo-900", children: questionData.instruction }))] })), items.map((item, itemIndex) => {
12
39
  const stored = userAnswers[itemIndex] || emptyAnswer();
13
40
  const current = item.isExample
14
41
  ? { optionId: item.optionId, text: stored.text }
15
42
  : stored;
16
43
  const expectedId = item.optionId;
44
+ const hasStudentText = current.text.trim() !== '';
45
+ const result = itemResults?.[itemIndex];
46
+ const mappingCorrect = Boolean(expectedId) && current.optionId === expectedId;
47
+ const points = typeof item.points === 'number' ? item.points : 1;
48
+ const halfLabel = formatHalf(points);
49
+ const sampleAnswers = (item.expectedAnswers || [])
50
+ .map((answer) => answer.trim())
51
+ .filter((answer) => answer !== '');
17
52
  const parts = (item.question || '').split('{0}');
53
+ const showReview = isReviewMode && !item.isExample && Boolean(expectedId);
54
+ const showAnswerKey = showReview && !hasAttempt;
55
+ const showGradedReview = showReview && hasAttempt;
18
56
  const setAnswer = (next) => {
19
57
  if (!canEdit || item.isExample)
20
58
  return;
@@ -23,12 +61,38 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
23
61
  text: next.text ?? current.text,
24
62
  });
25
63
  };
26
- return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-slate-800 text-xs font-bold text-white", children: 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-[10px] font-semibold uppercase text-amber-800", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsxs("p", { className: "flex flex-wrap items-center gap-1.5 text-[15px] leading-relaxed text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "inline-flex items-center gap-1", children: item.options.map((option, optionIndex) => {
27
- const selected = current.optionId === option.id;
28
- const showReview = isReviewMode && !item.isExample && Boolean(expectedId);
29
- const isCorrect = showReview && option.id === expectedId;
30
- const isWrong = showReview && selected && option.id !== expectedId;
31
- return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [optionIndex > 0 && _jsx("span", { className: "text-slate-400", children: "|" }), _jsx("button", { type: "button", disabled: !canEdit || item.isExample, onClick: () => setAnswer({ optionId: option.id }), className: cn('rounded-md border px-2.5 py-1 text-sm transition-colors', selected && !isCorrect && !isWrong && 'border-black bg-black text-white', !selected && !isCorrect && !isWrong && 'border-gray-300 bg-white text-gray-900', isCorrect && 'border-emerald-500 bg-emerald-50 text-emerald-900', isWrong && 'border-red-500 bg-red-50 text-red-900', (!canEdit || item.isExample) && 'cursor-default'), children: option.text || option.id })] }, option.id));
32
- }) }), _jsx("span", { children: parts.slice(1).join('{0}') })] }), _jsx("textarea", { value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: "Answer for you", rows: 3, className: "w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none focus:border-slate-400 disabled:bg-slate-50" })] })] }, `${item.questionNumber}-${itemIndex}`));
33
- }), 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] }))] }));
64
+ return (_jsx("div", { className: "rounded-xl border border-slate-200 bg-white p-3", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: cn('mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white', badgeClass({
65
+ isExample: item.isExample,
66
+ isReviewMode,
67
+ hasAttempt,
68
+ points,
69
+ partialScore: result?.partialScore,
70
+ mappingCorrect,
71
+ hasText: hasStudentText,
72
+ })), children: item.questionNumber || itemIndex + 1 }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold uppercase text-amber-800", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] })), _jsxs("p", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-1.5 text-[15px] leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "inline-flex items-center gap-1", children: item.options.map((option, optionIndex) => {
73
+ const selected = current.optionId === option.id;
74
+ const isKey = option.id === expectedId;
75
+ const pickedCorrect = showGradedReview && selected && isKey;
76
+ const pickedWrong = showGradedReview && selected && !isKey;
77
+ const keyOnly = showGradedReview && !selected && isKey;
78
+ const officialCorrect = showAnswerKey && isKey;
79
+ return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [optionIndex > 0 && (_jsx("span", { className: "text-sm font-medium text-slate-300", children: "|" })), _jsxs("button", { type: "button", disabled: !canEdit || item.isExample, onClick: () => setAnswer({ optionId: option.id }), className: cn('inline-flex min-w-[3.5rem] items-center justify-center gap-1 rounded-full border px-2.5 py-1 text-sm font-semibold transition-colors', !showReview &&
80
+ selected &&
81
+ 'border-indigo-600 bg-indigo-600 text-white shadow-sm', !showReview &&
82
+ !selected &&
83
+ 'border-slate-200 bg-white text-slate-800 hover:border-indigo-300 hover:bg-indigo-50', showAnswerKey &&
84
+ !isKey &&
85
+ 'border-slate-200 bg-white text-slate-800', officialCorrect &&
86
+ 'border-2 border-emerald-500 bg-white text-slate-800', pickedCorrect && 'border-emerald-600 bg-emerald-600 text-white', pickedWrong && 'border-red-500 bg-red-500 text-white', keyOnly && 'border-emerald-500 bg-white text-emerald-800', showGradedReview &&
87
+ !selected &&
88
+ !isKey &&
89
+ 'border-slate-200 bg-slate-50 text-slate-400', canEdit &&
90
+ !item.isExample &&
91
+ 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-200', (!canEdit || item.isExample) && 'cursor-default'), children: [option.text || option.id, pickedCorrect ? _jsx(Check, { className: "h-3 w-3" }) : null, pickedWrong ? _jsx(X, { className: "h-3 w-3" }) : null] })] }, option.id));
92
+ }) }), _jsx("span", { children: parts.slice(1).join('{0}') })] }), showReview && hasAttempt ? (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsxs("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', mappingCorrect
93
+ ? 'bg-emerald-50 text-emerald-700'
94
+ : 'bg-red-50 text-red-700'), children: ["Ch\u1ECDn ", mappingCorrect ? halfLabel : '0'] }), result?.aiCorrect !== undefined ? (_jsxs("span", { className: cn('rounded-full px-2 py-0.5 text-[11px] font-semibold', result.aiCorrect
95
+ ? 'bg-emerald-50 text-emerald-700'
96
+ : 'bg-red-50 text-red-700'), children: ["Vi\u1EBFt ", result.aiCorrect ? halfLabel : '0'] })) : null] })) : null, canEdit || hasStudentText || (isReviewMode && hasAttempt) ? (_jsxs("label", { className: "block space-y-1", children: [_jsx("span", { className: "text-xs font-medium text-slate-500", children: "Your answer" }), isReviewMode && !hasStudentText ? (_jsx("p", { className: "rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-2 text-sm italic text-slate-400", children: "Ch\u01B0a vi\u1EBFt" })) : (_jsx("textarea", { value: current.text, disabled: !canEdit || item.isExample, onChange: (event) => setAnswer({ text: event.target.value }), placeholder: "Write your answer here...", rows: 2, className: "min-h-0 w-full resize-none rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed text-slate-800 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 disabled:bg-slate-50" }))] })) : null, isReviewMode && sampleAnswers.length > 0 ? (_jsxs("p", { className: "text-xs text-slate-500", children: [_jsx("span", { className: "font-semibold", children: "Sample:" }), " ", sampleAnswers.join(' · ')] })) : null] })] }) }, `${item.questionNumber}-${itemIndex}`));
97
+ }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
34
98
  }
@@ -8,10 +8,10 @@ import { PointsInput } from '../../../../components/ui/points-input';
8
8
  import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
9
9
  import { Switch } from '../../../../components/ui/switch';
10
10
  import { useDebouncedCallback } from '../../../../shared/lib/hooks';
11
- import { ListChecks, Plus, Star, Trash2 } from 'lucide-react';
11
+ import { ListChecks, PenLine, Plus, Sparkles, Star, Trash2 } from 'lucide-react';
12
12
  import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
13
13
  import { ChooseThenAnswerGroupClient } from './ChooseThenAnswerGroupClient';
14
- import { createEmptyItem, createEmptyOption, sumGradablePoints, } from './map-choose-then-answer-group-data';
14
+ import { createEmptyItem, createEmptyOption, normalizeExpectedAnswers, sumGradablePoints, } from './map-choose-then-answer-group-data';
15
15
  import { CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/choose-then-answer-group.type';
16
16
  function normalizeItems(raw) {
17
17
  if (!Array.isArray(raw) || raw.length === 0) {
@@ -29,6 +29,7 @@ function normalizeItems(raw) {
29
29
  question: item.question || '',
30
30
  options,
31
31
  optionId: item.optionId || options[0]?.id || '',
32
+ expectedAnswers: normalizeExpectedAnswers(item.expectedAnswers),
32
33
  isExample: item.isExample === true,
33
34
  points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
34
35
  questionNumber: item.questionNumber || index + 1,
@@ -41,6 +42,7 @@ export function ChooseThenAnswerGroupCreator({ initialData, onChange, externalEr
41
42
  const [items, setItems] = useState(() => normalizeItems(initialData?.items));
42
43
  const [explanation, setExplanation] = useState(initialData?.explanation || '');
43
44
  const [isClientMode, setIsClientMode] = useState(false);
45
+ const [previewAnswers, setPreviewAnswers] = useState([]);
44
46
  const [errors, setErrors] = useState([]);
45
47
  const onChangeRef = useRef(onChange);
46
48
  const previousPayloadRef = useRef('');
@@ -156,6 +158,33 @@ export function ChooseThenAnswerGroupCreator({ initialData, onChange, externalEr
156
158
  };
157
159
  }));
158
160
  };
161
+ const updateExpectedAnswer = (itemIndex, answerIndex, value) => {
162
+ setItems((prev) => prev.map((item, i) => {
163
+ if (i !== itemIndex)
164
+ return item;
165
+ const expectedAnswers = [...(item.expectedAnswers || [''])];
166
+ expectedAnswers[answerIndex] = value;
167
+ return { ...item, expectedAnswers };
168
+ }));
169
+ };
170
+ const addExpectedAnswer = (itemIndex) => {
171
+ setItems((prev) => prev.map((item, i) => {
172
+ if (i !== itemIndex)
173
+ return item;
174
+ return { ...item, expectedAnswers: [...(item.expectedAnswers || ['']), ''] };
175
+ }));
176
+ };
177
+ const removeExpectedAnswer = (itemIndex, answerIndex) => {
178
+ setItems((prev) => prev.map((item, i) => {
179
+ const current = item.expectedAnswers || [''];
180
+ if (i !== itemIndex || current.length <= 1)
181
+ return item;
182
+ return {
183
+ ...item,
184
+ expectedAnswers: current.filter((_, j) => j !== answerIndex),
185
+ };
186
+ }));
187
+ };
159
188
  const addItem = () => {
160
189
  setItems((prev) => [...prev, createEmptyItem(prev.length + 1)]);
161
190
  };
@@ -168,10 +197,29 @@ export function ChooseThenAnswerGroupCreator({ initialData, onChange, externalEr
168
197
  })));
169
198
  };
170
199
  const displayErrors = [...errors, ...(externalErrors || [])];
200
+ const openPreview = () => {
201
+ setPreviewAnswers(items.map((item) => ({
202
+ optionId: item.isExample ? item.optionId : '',
203
+ text: '',
204
+ })));
205
+ setIsClientMode(true);
206
+ };
171
207
  if (isClientMode) {
172
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(ChooseThenAnswerGroupClient, { questionData: buildPayload(), isReviewMode: true })] }));
208
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(ChooseThenAnswerGroupClient, { questionData: buildPayload(), userAnswers: previewAnswers, onAnswerChange: (itemIndex, next) => {
209
+ setPreviewAnswers((prev) => {
210
+ const answers = items.map((item, index) => prev[index] || {
211
+ optionId: item.isExample ? item.optionId : '',
212
+ text: '',
213
+ });
214
+ answers[itemIndex] = next;
215
+ return answers;
216
+ });
217
+ } })] }));
173
218
  }
174
- return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("ul", { className: "list-disc space-y-1 rounded-md border border-red-200 bg-red-50 p-2 pl-6 text-xs text-red-700", children: displayErrors.map((error) => (_jsx("li", { children: error }, error))) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(ListChecks, { className: "h-3.5 w-3.5" }), title: "Ch\u1ECDn r\u1ED3i tr\u1EA3 l\u1EDDi (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "cta-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "cta-title", value: title, onChange: (event) => setTitle(event.target.value), placeholder: "GRAMMAR", className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "cta-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "cta-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] })] }) }), _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: ["Danh s\u00E1ch c\u00E2u (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Th\u00EAm c\u00E2u"] })] }) }), _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\u00E2u ", index + 1] }), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeItem(index), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsx(Input, { value: item.question, onChange: (event) => updateItem(index, { question: event.target.value }), placeholder: "Did you eat {0} for lunch today?", className: "h-10 border-gray-200 bg-white" }), _jsxs("p", { className: "text-xs text-slate-500", children: ["D\u00F9ng ", '{0}', " cho ch\u1ED7 hi\u1EC7n chip ch\u1ECDn."] }), item.options.map((option, optionIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: option.text, onChange: (event) => updateOption(index, optionIndex, event.target.value), placeholder: `Lựa chọn ${optionIndex + 1}`, className: "h-10 border-gray-200 bg-white" }), item.options.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeOption(index, optionIndex), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, option.id))), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => addOption(index), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Th\u00EAm l\u1EF1a ch\u1ECDn"] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("select", { className: "h-10 rounded-md border border-input bg-background px-3 text-sm", value: item.optionId, onChange: (event) => updateItem(index, { optionId: event.target.value }), children: item.options.map((option) => (_jsx("option", { value: option.id, children: option.text || option.id }, option.id))) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Switch, { id: `cta-example-${index}`, checked: item.isExample === true, onCheckedChange: (checked) => updateItem(index, { isExample: checked }) }), _jsxs(Label, { htmlFor: `cta-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(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
175
- points: Number.parseInt(event.target.value, 10) || 1,
176
- }) }))] })] }, `${item.questionNumber}-${index}`))), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) })] })] })] }));
219
+ return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("ul", { className: "list-disc space-y-1 rounded-md border border-red-200 bg-red-50 p-2 pl-6 text-xs text-red-700", children: displayErrors.map((error) => (_jsx("li", { children: error }, error))) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(ListChecks, { className: "h-3.5 w-3.5" }), title: "Ch\u1ECDn r\u1ED3i tr\u1EA3 l\u1EDDi (Nh\u00F3m)", preview: { onClick: openPreview } }), children: [_jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "cta-title", className: "text-sm font-semibold text-gray-800", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "cta-title", value: title, onChange: (event) => setTitle(event.target.value), placeholder: "GRAMMAR", className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "cta-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "cta-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] })] }), _jsxs("p", { className: "mt-2.5 rounded-lg border border-indigo-100 bg-indigo-50/70 px-3 py-2 text-xs leading-relaxed text-indigo-800", children: ["M\u1ED7i c\u00E2u c\u00F3 2 ph\u1EA7n: ", _jsx("strong", { children: "ch\u1ECDn t\u1EEB \u0111\u00FAng" }), " trong c\u00E2u (mapping), r\u1ED3i", ' ', _jsx("strong", { children: "vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi" }), " (AI). V\u00ED d\u1EE5: Did you eat many | much for lunch today? \u2192 No, I didn\u2019t eat much for lunch today."] })] }), _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: ["Danh s\u00E1ch c\u00E2u (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Th\u00EAm c\u00E2u"] })] }) }), _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 flex-wrap items-center justify-between gap-2", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", index + 1] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Switch, { id: `cta-example-${index}`, checked: item.isExample === true, onCheckedChange: (checked) => updateItem(index, { isExample: checked }) }), _jsxs(Label, { htmlFor: `cta-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 && (_jsxs("div", { className: "space-y-0.5", children: [_jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
220
+ points: Number.parseInt(event.target.value, 10) || 1,
221
+ }) }), _jsx("p", { className: "text-[10px] leading-tight text-slate-500", children: "C\u1EA3 c\u00E2u: 50% ch\u1ECDn + 50% vi\u1EBFt" })] })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeItem(index), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { className: "space-y-2 rounded-lg border border-slate-200 bg-slate-50/60 px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-slate-600", children: [_jsx(ListChecks, { className: "h-3.5 w-3.5" }), "1. Ch\u1ECDn t\u1EEB \u0111\u00FAng"] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi" }), _jsx(Input, { value: item.question, onChange: (event) => updateItem(index, { question: event.target.value }), placeholder: "Did you eat {0} for lunch today?", className: "h-10 border-gray-200 bg-white" }), _jsxs("p", { className: "text-xs text-slate-500", children: ["D\u00F9ng ", '{0}', " cho ch\u1ED7 hi\u1EC7n chip ch\u1ECDn."] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "L\u1EF1a ch\u1ECDn" }), _jsx("p", { className: "text-xs text-slate-500", children: "Ch\u1ECDn radio \u0111\u1EC3 \u0111\u00E1nh d\u1EA5u \u0111\u00E1p \u00E1n \u0111\u00FAng (chip)." }), item.options.map((option, optionIndex) => {
222
+ const isCorrect = item.optionId === option.id;
223
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: `cta-correct-${index}`, checked: isCorrect, onChange: () => updateItem(index, { optionId: option.id }), "aria-label": `Đáp án đúng: ${option.text || option.id}`, className: "h-4 w-4 shrink-0 accent-indigo-600" }), _jsx(Input, { value: option.text, onChange: (event) => updateOption(index, optionIndex, event.target.value), placeholder: `Lựa chọn ${optionIndex + 1}`, className: "h-10 border-gray-200 bg-white" }), isCorrect && (_jsx("span", { className: "shrink-0 text-[10px] font-semibold uppercase text-indigo-600", children: "\u0110\u00FAng" })), item.options.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeOption(index, optionIndex), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, option.id));
224
+ })] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => addOption(index), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Th\u00EAm l\u1EF1a ch\u1ECDn"] })] }), _jsxs("div", { className: "space-y-2 rounded-lg border border-violet-100 bg-violet-50/50 px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-violet-700", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), "2. Tr\u1EA3 l\u1EDDi c\u00E2u h\u1ECFi (AI)"] }), _jsxs("p", { className: "flex items-start gap-1.5 text-xs text-violet-800/80", children: [_jsx(PenLine, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), "H\u1ECDc sinh vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi c\u1EE7a m\u00ECnh. \u0110\u00E1p \u00E1n m\u1EABu gi\u00FAp AI ch\u1EA5m theo ngh\u0129a (\u0111\u1EC3 tr\u1ED1ng = ch\u1EA5m \u0111\u00FAng ch\u1EE7 \u0111\u1EC1)."] }), (item.expectedAnswers || ['']).map((answer, answerIndex) => (_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("textarea", { value: answer, onChange: (event) => updateExpectedAnswer(index, answerIndex, event.target.value), placeholder: "No, I didn\u2019t eat much for lunch today.", rows: 2, className: "min-h-[3.25rem] w-full resize-none rounded-md border border-gray-200 bg-white px-3 py-2 text-sm outline-none focus:border-violet-400 focus:ring-2 focus:ring-violet-100" }), (item.expectedAnswers || ['']).length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeExpectedAnswer(index, answerIndex), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `cta-expected-${index}-${answerIndex}`))), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => addExpectedAnswer(index), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Th\u00EAm \u0111\u00E1p \u00E1n m\u1EABu"] })] })] }, `${item.questionNumber}-${index}`))), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) })] })] })] }));
177
225
  }
@@ -1,11 +1,14 @@
1
- import type { ChooseThenAnswerGroupData, ChooseThenAnswerGroupItemData, ChooseThenAnswerOption, ChooseThenAnswerStudentItemAnswer } from '../../_shared/types/choose-then-answer-group.type';
1
+ import type { ChooseThenAnswerGradeDetail, ChooseThenAnswerGroupData, ChooseThenAnswerGroupItemData, ChooseThenAnswerItemResult, ChooseThenAnswerOption, ChooseThenAnswerStudentItemAnswer } from '../../_shared/types/choose-then-answer-group.type';
2
2
  export declare function nextPrefixedId(prefix: string, existing: string[]): string;
3
3
  export declare function sumGradablePoints(items: ChooseThenAnswerGroupItemData[]): number;
4
4
  export declare function createEmptyOption(existingIds: string[]): ChooseThenAnswerOption;
5
+ export declare function normalizeExpectedAnswers(value: unknown): string[];
5
6
  export declare function createEmptyItem(questionNumber: number): ChooseThenAnswerGroupItemData;
6
7
  export declare function parseItemAnswer(raw: unknown): {
7
8
  optionId: string;
9
+ expectedAnswers: string[];
8
10
  };
9
11
  export declare function parseStudentAnswer(raw: unknown): ChooseThenAnswerStudentItemAnswer;
10
12
  export declare function mapApiItem(item: unknown, index: number, fallbackAnswer?: unknown): ChooseThenAnswerGroupItemData;
11
13
  export declare function mapQuestionToChooseThenAnswerGroupData(question: Record<string, unknown> | null | undefined): ChooseThenAnswerGroupData;
14
+ export declare function toCtaItemResults(details: ChooseThenAnswerGradeDetail[] | undefined, items: ChooseThenAnswerGroupItemData[], userAnswers: ChooseThenAnswerStudentItemAnswer[]): ChooseThenAnswerItemResult[] | undefined;