@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.11

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 (124) hide show
  1. package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
  2. package/dist/components/exams/take/components/QuestionRenderer.js +7 -7
  3. package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
  4. package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
  5. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
  6. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.d.ts +14 -0
  7. package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.js +36 -0
  8. package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +4 -0
  9. package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
  10. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  11. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
  12. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
  13. package/dist/components/exams/take/utils/answer-transformers.js +17 -1
  14. package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
  15. package/dist/components/exams/take/utils/question-transformers.d.ts +32 -3
  16. package/dist/components/exams/take/utils/question-transformers.js +51 -2
  17. package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
  18. package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
  19. package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
  20. package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
  21. package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
  22. package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
  23. package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
  24. package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
  25. package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
  26. package/dist/components/questions/_shared/components/compact/index.js +4 -0
  27. package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
  28. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +1 -0
  29. package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +14 -0
  30. package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +2 -2
  31. package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
  32. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -0
  33. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
  34. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
  35. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
  36. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
  37. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
  38. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
  39. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
  40. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
  41. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
  42. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
  43. package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
  44. package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
  45. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
  46. package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
  47. package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
  48. package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
  49. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
  50. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
  51. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
  52. package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
  53. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
  54. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
  55. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
  56. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
  57. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
  58. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
  59. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
  60. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
  61. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
  62. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
  63. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
  64. package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
  65. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
  66. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
  67. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
  68. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
  69. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
  70. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
  71. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
  72. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
  73. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
  74. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
  75. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
  76. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
  77. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
  78. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +24 -35
  79. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +16 -3
  80. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +2 -1
  81. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +25 -6
  82. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +1 -1
  83. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +22 -10
  84. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +60 -18
  85. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +2 -1
  86. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +24 -5
  87. package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +8 -4
  88. package/dist/components/questions/types/matching-with-lines-group/transform.js +5 -4
  89. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
  90. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
  91. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
  92. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
  93. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
  94. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  95. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
  96. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
  97. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
  98. package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
  99. package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
  100. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
  101. package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
  102. package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
  103. package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
  104. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +21 -23
  105. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
  106. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
  107. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
  108. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
  109. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
  110. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
  111. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
  112. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
  113. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
  114. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
  115. package/dist/components/questions/viewer/QuestionViewer.js +2 -2
  116. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
  117. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
  118. package/dist/components/results/renderers/review-question-body-movers.js +7 -3
  119. package/dist/components/results/review-data.js +20 -1
  120. package/dist/shared/lib/utils/question-reverse-transform.js +48 -7
  121. package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
  122. package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
  123. package/dist/shared/types/questions/matching-with-lines-group.d.ts +2 -2
  124. package/package.json +1 -1
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useMemo } from 'react';
4
- import { Check, X, CircleHelp, Lightbulb, ClipboardList, Users, BookOpen } from 'lucide-react';
4
+ import { Check, X, Lightbulb, ClipboardList, Users, BookOpen } from 'lucide-react';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
6
  import { cn } from '../../../../shared/lib/utils';
7
7
  import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
8
8
  import { isRichTextEmpty, richTextToPlainText } from '../../../../shared/lib/rich-text';
9
9
  import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, optionToString } from './normalize';
10
+ import { KidChoiceRow, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
10
11
  // Helper component to resolve presigned URL for each image option
11
12
  function OptionImage({ src, alt, className }) {
12
13
  const { previewUrl, isLoading } = usePresignedFileUrl(src);
@@ -110,41 +111,19 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
110
111
  .split('\n')
111
112
  .map((line) => `<p class="last:mb-0">${line}</p>`)
112
113
  .join(''),
113
- } })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xs", 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-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: isMultipleAnswers ? 'Chọn các đáp án đúng' : 'Chọn đáp án đúng' })] }), questionData.points !== undefined && (_jsx("div", { className: "flex items-center gap-1.5 rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-semibold text-blue-700", children: _jsxs("span", { children: [questionData.points, " \u0111i\u1EC3m"] }) }))] }), _jsxs("div", { className: "space-y-3 p-3", children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx("div", { className: "rounded-md border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 px-3 py-2", children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-sm font-semibold text-gray-800" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
114
- /* eslint-disable-next-line @next/next/no-img-element */
115
- _jsx("img", { src: imagePreviewUrl || cleanImageUrl, alt: "Question illustration", className: "max-h-56 w-auto object-contain rounded-md", onError: (e) => {
116
- const target = e.target;
117
- target.onerror = null;
118
- target.src = '/images/placeholder-image.svg';
119
- } })) })] })), showOptions && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("span", { className: "text-xs font-medium text-gray-500 uppercase tracking-wider", children: isMultipleAnswers ? 'Chọn một hoặc nhiều đáp án' : 'Chọn đáp án' }), displayOptions.map((option, index) => {
120
- const isSelected = selectedAnswer.includes(index);
121
- const isCorrectAnswer = isCorrect(index);
122
- const letter = String.fromCharCode(65 + index);
123
- const optionText = optionToString(option);
124
- return (_jsxs("button", { type: "button", onClick: () => handleAnswerChange(index), disabled: isReviewMode, className: cn('group flex w-full items-center gap-2 rounded-md border px-3 py-1.5 text-left transition-all', isReviewMode
125
- ? isCorrectAnswer
126
- ? 'border-green-400 bg-green-50'
127
- : isSelected
128
- ? 'border-red-400 bg-red-50'
129
- : 'border-gray-200 bg-gray-50'
130
- : isSelected
131
- ? 'border-blue-500 bg-blue-50 shadow-sm ring-2 ring-blue-100'
132
- : 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm'), children: [_jsx("div", { className: cn('flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-bold transition-all', isReviewMode
133
- ? isCorrectAnswer
134
- ? 'bg-green-500 text-white'
135
- : isSelected
136
- ? 'bg-red-500 text-white'
137
- : 'bg-gray-200 text-gray-600'
138
- : isSelected
139
- ? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm'
140
- : 'bg-gray-100 text-gray-600 group-hover:bg-gray-200'), children: letter }), questionData.optionType === 'image' ? (_jsx("div", { className: "flex-1 flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx(RichTextHtml, { html: optionText, className: cn('flex-1 text-sm font-medium', isReviewMode && isCorrectAnswer
141
- ? 'text-green-800'
142
- : isReviewMode && isSelected && !isCorrectAnswer
143
- ? 'text-red-800'
144
- : 'text-gray-700') })), isReviewMode && (isCorrectAnswer || isSelected) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', isCorrectAnswer ? 'bg-green-500' : 'bg-red-500'), children: isCorrectAnswer
145
- ? _jsx(Check, { className: "h-4 w-4 text-white" })
146
- : _jsx(X, { className: "h-4 w-4 text-white" }) }))] }, index));
147
- })] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: isMultipleAnswers ? 'Vui lòng chọn ít nhất một đáp án' : 'Vui lòng chọn một đáp án' })] }))] })] }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2", children: [_jsx(Lightbulb, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600" }), _jsxs("p", { className: "text-xs text-gray-700", children: [_jsx("span", { className: "font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch: " }), questionData.explanation] })] })), isReviewMode && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-slate-500 to-gray-600 text-white", children: _jsx(ClipboardList, { className: "h-3.5 w-3.5" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "T\u1ED5ng k\u1EBFt" })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-gray-50 p-3", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n c\u1EE7a b\u1EA1n:" }), _jsx("span", { className: cn('text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : hasAnswered ? 'text-red-600' : 'text-gray-500'), children: formatAnswerLabel(selectedAnswer) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-lg bg-green-50 p-3 border border-green-100", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-green-600", children: formatAnswerLabel(normalizedCorrectAnswers) })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-lg p-3 border', isAnswerCorrect
114
+ } })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })), _jsxs(KidQuestionCard, { title: isMultipleAnswers ? 'Chọn các đáp án đúng' : 'Chọn đáp án đúng', points: questionData.points, review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx(KidPrompt, { children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-sm font-semibold text-gray-800" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })) : (
115
+ /* eslint-disable-next-line @next/next/no-img-element */
116
+ _jsx("img", { src: imagePreviewUrl || cleanImageUrl, alt: "Question illustration", className: "max-h-56 w-auto object-contain rounded-md", onError: (e) => {
117
+ const target = e.target;
118
+ target.onerror = null;
119
+ target.src = '/images/placeholder-image.svg';
120
+ } })) })] })), showOptions && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("span", { className: "text-xs font-medium text-gray-500 uppercase tracking-wider", children: isMultipleAnswers ? 'Chọn một hoặc nhiều đáp án' : 'Chọn đáp án' }), displayOptions.map((option, index) => {
121
+ const isSelected = selectedAnswer.includes(index);
122
+ const isCorrectAnswer = isCorrect(index);
123
+ const letter = optionLetter(index);
124
+ const optionText = optionToString(option);
125
+ return (_jsx(KidChoiceRow, { letter: letter, selected: isSelected, isReviewMode: isReviewMode, isCorrect: isCorrectAnswer, onClick: () => handleAnswerChange(index), children: questionData.optionType === 'image' ? (_jsx("div", { className: "flex items-center justify-center py-1", children: _jsx(OptionImage, { src: optionText, alt: `Đáp án ${letter}` }) })) : (_jsx(RichTextHtml, { html: optionText })) }, index));
126
+ })] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: isMultipleAnswers ? 'Vui lòng chọn ít nhất một đáp án' : 'Vui lòng chọn một đáp án' })] }))] }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2", children: [_jsx(Lightbulb, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600" }), _jsxs("p", { className: "text-xs text-gray-700", children: [_jsx("span", { className: "font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch: " }), questionData.explanation] })] })), isReviewMode && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-slate-500 to-gray-600 text-white", children: _jsx(ClipboardList, { className: "h-3.5 w-3.5" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "T\u1ED5ng k\u1EBFt" })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-gray-50 p-3", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n c\u1EE7a b\u1EA1n:" }), _jsx("span", { className: cn('text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : hasAnswered ? 'text-red-600' : 'text-gray-500'), children: formatAnswerLabel(selectedAnswer) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-lg bg-green-50 p-3 border border-green-100", children: [_jsx("span", { className: "text-sm text-gray-600", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-green-600", children: formatAnswerLabel(normalizedCorrectAnswers) })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-lg p-3 border', isAnswerCorrect
148
127
  ? 'bg-green-50 border-green-200'
149
128
  : 'bg-red-50 border-red-200'), children: [_jsx("span", { className: "text-sm text-gray-600", children: "K\u1EBFt qu\u1EA3:" }), _jsx("div", { className: cn('flex items-center gap-1.5 text-sm font-semibold', isAnswerCorrect ? 'text-green-600' : 'text-red-600'), children: isAnswerCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4" }), _jsx("span", { children: "Ch\u00EDnh x\u00E1c" })] })) : (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-4 w-4" }), _jsx("span", { children: "Sai" })] })) })] })] })] }))] }));
150
129
  }
@@ -1,11 +1,11 @@
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, X } from 'lucide-react';
4
- import { cn } from '../../../../shared/lib/utils';
3
+ import { BookOpen, Lightbulb, Volume2 } from 'lucide-react';
5
4
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
5
  import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
7
6
  import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
8
7
  import { getChooseAnswerGroupOptionImageSrc, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
8
+ import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
9
9
  function ClientImageItem({ url, alt }) {
10
10
  const { previewUrl, isLoading } = usePresignedFileUrl(url);
11
11
  const src = previewUrl ||
@@ -24,21 +24,6 @@ function ClientAudio({ url }) {
24
24
  }
25
25
  return (_jsxs("div", { className: "flex items-center gap-3 rounded-xl border border-indigo-100 bg-indigo-50/70 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-indigo-600" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-indigo-100" })) : (_jsx("audio", { controls: true, className: "h-9 w-full", src: src, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
26
26
  }
27
- function optionLetter(index) {
28
- return String.fromCharCode(65 + index);
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
- }
42
27
  export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
43
28
  const items = questionData.items || [];
44
29
  const handleSelect = (itemIndex, optionId) => {
@@ -50,20 +35,14 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
50
35
  next[itemIndex] = optionId;
51
36
  onAnswerChange(next);
52
37
  };
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) => {
38
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), toChooseAnswerGroupImageUrls(questionData.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `group-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "overflow-x-auto", children: _jsx(RichTextHtml, { html: questionData.passage, className: "text-gray-800 leading-relaxed" }) })] })), items.map((item, itemIndex) => {
54
39
  const selectedId = userAnswers[itemIndex];
55
40
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
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) => {
57
- const isCorrect = option.id === item.correctAnswer;
58
- const isSelected = option.id === selectedId;
59
- const tone = reviewOptionTone(isReviewMode, isCorrect, isSelected);
60
- const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
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));
62
- }) })) : (_jsx("div", { className: "space-y-2", children: item.options.map((option, optionIndex) => {
63
- const isCorrect = option.id === item.correctAnswer;
64
- const isSelected = option.id === selectedId;
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));
67
- }) }))] })] }, `${item.questionNumber}-${itemIndex}`));
41
+ return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, children: [!isRichTextEmpty(item.question) && (_jsx(KidPrompt, { children: _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}`))) })), _jsx("div", { className: "space-y-1.5", children: item.options.map((option, optionIndex) => {
42
+ const isCorrect = option.id === item.correctAnswer;
43
+ const isSelected = option.id === selectedId;
44
+ const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
45
+ return (_jsx(KidChoiceRow, { letter: optionLetter(optionIndex), selected: isSelected, isReviewMode: isReviewMode, isCorrect: isReviewMode && isCorrect, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, option.id) : undefined, children: item.optionType === 'image' ? (optionImageSrc ? (_jsx(ClientImageItem, { url: optionImageSrc, alt: `Đáp án ${optionLetter(optionIndex)}` })) : (`Đáp án ${optionLetter(optionIndex)}`)) : isRichTextEmpty(option.text) ? (`Đáp án ${optionLetter(optionIndex)}`) : (_jsx(RichTextHtml, { html: option.text })) }, option.id || optionIndex));
46
+ }) })] }, `${item.questionNumber}-${itemIndex}`));
68
47
  }), 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] }))] }));
69
48
  }
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Check, Lightbulb, Star, X } from 'lucide-react';
3
+ import { Check, Lightbulb, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
+ import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
5
6
  function emptyAnswer() {
6
7
  return { optionId: '', text: '' };
7
8
  }
@@ -35,7 +36,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
35
36
  const items = questionData.items || [];
36
37
  const hasAttempt = Boolean(itemResults?.length) ||
37
38
  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) => {
39
+ return (_jsxs("div", { className: "space-y-2", children: [(questionData.title || questionData.instruction) && (_jsxs(KidInstruction, { children: [questionData.title ? (_jsx("p", { className: "mb-1 text-[11px] font-semibold text-indigo-500", children: questionData.title })) : null, questionData.instruction] })), items.map((item, itemIndex) => {
39
40
  const stored = userAnswers[itemIndex] || emptyAnswer();
40
41
  const current = item.isExample
41
42
  ? { optionId: item.optionId, text: stored.text }
@@ -61,7 +62,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
61
62
  text: next.text ?? current.text,
62
63
  });
63
64
  };
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
+ return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: points, isExample: item.isExample, 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
66
  isExample: item.isExample,
66
67
  isReviewMode,
67
68
  hasAttempt,
@@ -69,7 +70,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
69
70
  partialScore: result?.partialScore,
70
71
  mappingCorrect,
71
72
  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
+ })), children: item.questionNumber || itemIndex + 1 }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsxs("p", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-1.5 text-sm font-semibold 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
74
  const selected = current.optionId === option.id;
74
75
  const isKey = option.id === expectedId;
75
76
  const pickedCorrect = showGradedReview && selected && isKey;
@@ -78,9 +79,9 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
78
79
  const officialCorrect = showAnswerKey && isKey;
79
80
  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
81
  selected &&
81
- 'border-indigo-600 bg-indigo-600 text-white shadow-sm', !showReview &&
82
+ 'border-blue-500 bg-blue-500 text-white shadow-sm ring-2 ring-blue-100', !showReview &&
82
83
  !selected &&
83
- 'border-slate-200 bg-white text-slate-800 hover:border-indigo-300 hover:bg-indigo-50', showAnswerKey &&
84
+ 'border-gray-200 bg-white text-slate-800 hover:border-blue-300 hover:bg-blue-50', showAnswerKey &&
84
85
  !isKey &&
85
86
  'border-slate-200 bg-white text-slate-800', officialCorrect &&
86
87
  '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 &&
@@ -93,6 +94,6 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
93
94
  ? 'bg-emerald-50 text-emerald-700'
94
95
  : '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
96
  ? '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
+ : '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: "C\u00E2u tr\u1EA3 l\u1EDDi c\u1EE7a b\u1EA1n" }), 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: "Vi\u1EBFt c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y...", 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: "M\u1EABu:" }), " ", sampleAnswers.join(' · ')] })) : null] })] }) }, `${item.questionNumber}-${itemIndex}`));
97
98
  }), 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] }))] }));
98
99
  }
@@ -1,10 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { CircleHelp, Lightbulb, Star } from 'lucide-react';
3
+ import { Lightbulb } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
- function tokenize(sentence) {
6
- return sentence.trim().split(/\s+/).filter(Boolean);
7
- }
5
+ import { tokenizeCrossOutQuestion } from './tokenize';
6
+ import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
8
7
  function normalizeWord(word) {
9
8
  return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
10
9
  }
@@ -21,6 +20,14 @@ function formatAnswer(answer) {
21
20
  return 'câu đúng (không gạch)';
22
21
  return `gạch "${answer.crossOut}"${posInfo}`;
23
22
  }
23
+ function isCorrectToken(correct, tokenIndex, plain) {
24
+ if (!correct)
25
+ return false;
26
+ if (typeof correct.wordIndex === 'number' && correct.wordIndex >= 0) {
27
+ return correct.wordIndex === tokenIndex;
28
+ }
29
+ return Boolean(correct.crossOut) && normalizeWord(correct.crossOut) === plain && plain !== '';
30
+ }
24
31
  export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
25
32
  const items = questionData.items || [];
26
33
  const emit = (itemIndex, nextItem) => {
@@ -32,47 +39,58 @@ export function CrossOutWordGroupClient({ questionData, isReviewMode = false, us
32
39
  next[itemIndex] = nextItem;
33
40
  onAnswerChange(next);
34
41
  };
35
- 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) => {
42
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => {
36
43
  const isReplace = item.type === 'CROSS_OUT_AND_REPLACE';
37
44
  const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
38
45
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
39
- const tokens = tokenize(item.question || '');
46
+ const tokens = tokenizeCrossOutQuestion(item.question || '');
40
47
  const selectedIndex = userValue?.wordIndex;
41
48
  const selectedWord = userValue?.crossOut || '';
42
49
  const replaceWith = userValue?.replaceWith || '';
43
50
  const correct = item.correctAnswer;
44
- 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"] })), _jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
45
- ? 'bg-violet-100 text-violet-700'
46
- : 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [isReplace ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-semibold text-slate-600", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: !canSelect, onChange: (event) => emit(itemIndex, {
47
- type: item.type,
48
- wordIndex: selectedIndex ?? null,
49
- crossOut: selectedWord,
50
- replaceWith: event.target.value,
51
- }), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 min-w-[10rem] rounded-lg border border-slate-200 px-2.5 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 disabled:bg-slate-50" })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-sm font-medium leading-relaxed text-slate-800", children: tokens.map((token, tokenIndex) => {
52
- const plain = normalizeWord(token);
53
- const isSelected = typeof selectedIndex === 'number'
51
+ return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, headerExtra: isReviewMode ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
52
+ ? 'bg-violet-100 text-violet-700'
53
+ : 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })) : null, children: [isReplace ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-semibold text-slate-600", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: !canSelect, onChange: (event) => emit(itemIndex, {
54
+ type: item.type,
55
+ wordIndex: selectedIndex ?? null,
56
+ crossOut: selectedWord,
57
+ replaceWith: event.target.value,
58
+ }), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 min-w-[10rem] rounded-lg border border-slate-200 px-2.5 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 disabled:bg-slate-50" })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-sm font-medium leading-relaxed text-slate-800", children: tokens.map((token, tokenIndex) => {
59
+ const plain = normalizeWord(token);
60
+ const isSelected = typeof selectedIndex === 'number'
61
+ ? selectedIndex === tokenIndex
62
+ : Boolean(selectedWord) && normalizeWord(selectedWord) === plain && plain !== '';
63
+ const isCorrectTarget = isReviewMode && !item.isExample && correct?.wordIndex === tokenIndex;
64
+ const isSelectedCorrect = isReviewMode &&
65
+ !item.isExample &&
66
+ isSelected &&
67
+ isCorrectToken(correct, tokenIndex, plain);
68
+ const isSelectedWrong = isReviewMode &&
69
+ !item.isExample &&
70
+ isSelected &&
71
+ Boolean(correct) &&
72
+ !isSelectedCorrect;
73
+ return (_jsx("button", { type: "button", disabled: !canSelect || plain === '', onClick: () => {
74
+ const already = typeof selectedIndex === 'number'
54
75
  ? selectedIndex === tokenIndex
55
- : Boolean(selectedWord) && normalizeWord(selectedWord) === plain && plain !== '';
56
- const isCorrectTarget = isReviewMode && !item.isExample && correct?.wordIndex === tokenIndex;
57
- return (_jsx("button", { type: "button", disabled: !canSelect || plain === '', onClick: () => {
58
- const already = typeof selectedIndex === 'number'
59
- ? selectedIndex === tokenIndex
60
- : selectedWord === token;
61
- emit(itemIndex, {
62
- type: item.type,
63
- wordIndex: already ? null : tokenIndex,
64
- crossOut: already
65
- ? ''
66
- : token.replace(/[^\p{L}\p{N}'-]/gu, '') || token,
67
- ...(isReplace ? { replaceWith } : {}),
68
- });
69
- }, className: cn('rounded-lg px-2 py-1 transition-colors', isSelected
76
+ : selectedWord === token;
77
+ emit(itemIndex, {
78
+ type: item.type,
79
+ wordIndex: already ? null : tokenIndex,
80
+ crossOut: already ? '' : token,
81
+ ...(isReplace ? { replaceWith } : {}),
82
+ });
83
+ }, className: cn('rounded-lg px-2 py-1 transition-colors', isSelectedWrong
84
+ ? 'border border-rose-200 bg-rose-100 font-bold text-rose-700 line-through decoration-rose-600'
85
+ : isSelectedCorrect
86
+ ? 'border border-emerald-300 bg-emerald-100 font-bold text-emerald-800 line-through decoration-emerald-600'
87
+ : isSelected
70
88
  ? 'border border-blue-200 bg-blue-100 font-bold text-blue-700 line-through decoration-blue-600'
71
89
  : isCorrectTarget
72
90
  ? 'border-2 border-dashed border-emerald-500 bg-emerald-50 font-bold text-emerald-800'
73
91
  : canSelect
74
92
  ? 'cursor-pointer hover:bg-blue-50 hover:text-blue-700'
75
93
  : 'cursor-default'), children: token }, `${item.questionNumber}-token-${tokenIndex}`));
76
- }) }), isReviewMode ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("span", { className: "font-medium", children: "\u0110\u00E1p \u00E1n: " }), formatAnswer(item.correctAnswer)] })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
94
+ }) }), isReviewMode ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("span", { className: "font-medium", children: "\u0110\u00E1p \u00E1n: " }), formatAnswer(item.correctAnswer)] })) : null] }, `${item.questionNumber}-${itemIndex}`));
77
95
  }), 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] }))] }));
78
96
  }
@@ -13,6 +13,8 @@ import { useDebouncedCallback } from '../../../../shared/lib/hooks';
13
13
  import { HelpCircle, Plus, Star, Trash2 } from 'lucide-react';
14
14
  import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
15
15
  import { CrossOutWordGroupClient } from './CrossOutWordGroupClient';
16
+ import { createCrossOutAnswer, createEmptyCrossOutItem, DEFAULT_CROSS_OUT_INSTRUCTION, } from './map-cross-out-word-group-data';
17
+ import { tokenizeCrossOutQuestion } from './tokenize';
16
18
  function parsePointsValue(raw, allowZero = false) {
17
19
  if (raw === '')
18
20
  return allowZero ? 0 : 1;
@@ -22,43 +24,9 @@ function parsePointsValue(raw, allowZero = false) {
22
24
  const min = allowZero ? 0 : 1;
23
25
  return Math.max(min, parsed);
24
26
  }
25
- const DEFAULT_INSTRUCTION = 'Cross out the extra or incorrect word. If the word is incorrect, write the correct word. If the line is correct, leave it blank.';
26
- function tokenize(sentence) {
27
- return sentence.trim().split(/\s+/).filter(Boolean);
28
- }
29
- function createAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
30
- const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
31
- ? wordIndex
32
- : wordIndex === null
33
- ? null
34
- : undefined;
35
- if (type === 'CROSS_OUT_AND_REPLACE') {
36
- return {
37
- type,
38
- ...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
39
- crossOut,
40
- replaceWith,
41
- };
42
- }
43
- return {
44
- type: 'CROSS_OUT',
45
- ...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
46
- crossOut,
47
- };
48
- }
49
- function createEmptyItem(questionNumber) {
50
- return {
51
- type: 'CROSS_OUT',
52
- question: '',
53
- correctAnswer: createAnswer('CROSS_OUT'),
54
- isExample: false,
55
- points: 1,
56
- questionNumber,
57
- };
58
- }
59
27
  function normalizeItems(rawItems) {
60
28
  if (!Array.isArray(rawItems) || rawItems.length === 0) {
61
- return [createEmptyItem(1)];
29
+ return [createEmptyCrossOutItem(1)];
62
30
  }
63
31
  return rawItems.map((item, index) => {
64
32
  const type = item.type === 'CROSS_OUT_AND_REPLACE' ||
@@ -66,7 +34,7 @@ function normalizeItems(rawItems) {
66
34
  ? 'CROSS_OUT_AND_REPLACE'
67
35
  : 'CROSS_OUT';
68
36
  const rawIndex = item.correctAnswer?.wordIndex;
69
- const correctAnswer = createAnswer(type, item.correctAnswer?.crossOut || '', item.correctAnswer?.replaceWith || '', rawIndex);
37
+ const correctAnswer = createCrossOutAnswer(type, item.correctAnswer?.crossOut || '', item.correctAnswer?.replaceWith || '', rawIndex);
70
38
  return {
71
39
  type,
72
40
  question: item.question || '',
@@ -81,7 +49,7 @@ function sumPoints(items) {
81
49
  return items.reduce((total, item) => item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1), 0);
82
50
  }
83
51
  export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
84
- const [instruction, setInstruction] = useState(initialData?.instruction || DEFAULT_INSTRUCTION);
52
+ const [instruction, setInstruction] = useState(initialData?.instruction || DEFAULT_CROSS_OUT_INSTRUCTION);
85
53
  const [items, setItems] = useState(() => normalizeItems(initialData?.items));
86
54
  const [explanation, setExplanation] = useState(initialData?.explanation || '');
87
55
  const [isClientMode, setIsClientMode] = useState(false);
@@ -121,7 +89,20 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
121
89
  if (!item.question.trim()) {
122
90
  nextErrors.push(`Câu ${index + 1}: Nội dung câu không được để trống.`);
123
91
  }
92
+ const wordIndex = item.correctAnswer.wordIndex;
93
+ if (wordIndex === undefined) {
94
+ nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch hoặc để trống nếu câu đúng (wordIndex).`);
95
+ }
96
+ if (typeof wordIndex === 'number') {
97
+ const tokens = tokenizeCrossOutQuestion(item.question);
98
+ if (tokens[wordIndex] !== item.correctAnswer.crossOut) {
99
+ nextErrors.push(`Câu ${index + 1}: Từ gạch phải khớp token tại wordIndex.`);
100
+ }
101
+ }
124
102
  if (item.type === 'CROSS_OUT_AND_REPLACE') {
103
+ if (typeof wordIndex !== 'number') {
104
+ nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch (wordIndex).`);
105
+ }
125
106
  if (!item.correctAnswer.crossOut.trim()) {
126
107
  nextErrors.push(`Câu ${index + 1}: Cần chọn hoặc nhập từ cần gạch (crossOut).`);
127
108
  }
@@ -160,7 +141,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
160
141
  next[index] = {
161
142
  ...current,
162
143
  type,
163
- correctAnswer: createAnswer(type, current.correctAnswer.crossOut, current.correctAnswer.replaceWith || '', current.correctAnswer.wordIndex),
144
+ correctAnswer: createCrossOutAnswer(type, current.correctAnswer.crossOut, current.correctAnswer.replaceWith || '', current.correctAnswer.wordIndex),
164
145
  };
165
146
  return next;
166
147
  });
@@ -174,7 +155,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
174
155
  const type = current.type;
175
156
  next[index] = {
176
157
  ...current,
177
- correctAnswer: createAnswer(type, patch.crossOut !== undefined ? patch.crossOut : current.correctAnswer.crossOut, patch.replaceWith !== undefined
158
+ correctAnswer: createCrossOutAnswer(type, patch.crossOut !== undefined ? patch.crossOut : current.correctAnswer.crossOut, patch.replaceWith !== undefined
178
159
  ? patch.replaceWith
179
160
  : current.correctAnswer.replaceWith || '', patch.wordIndex !== undefined ? patch.wordIndex : current.correctAnswer.wordIndex),
180
161
  };
@@ -194,15 +175,14 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
194
175
  });
195
176
  }
196
177
  else {
197
- const cleanWord = token.replace(/[^\p{L}\p{N}'-]/gu, '') || token;
198
178
  updateItemAnswer(itemIndex, {
199
179
  wordIndex: tokenIndex,
200
- crossOut: cleanWord,
180
+ crossOut: token,
201
181
  });
202
182
  }
203
183
  };
204
184
  const addItem = () => {
205
- setItems((prev) => [...prev, createEmptyItem(prev.length + 1)]);
185
+ setItems((prev) => [...prev, createEmptyCrossOutItem(prev.length + 1)]);
206
186
  };
207
187
  const removeItem = (index) => {
208
188
  setItems((prev) => {
@@ -217,8 +197,8 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
217
197
  if (isClientMode) {
218
198
  return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(CrossOutWordGroupClient, { questionData: currentPayload })] }));
219
199
  }
220
- return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: 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) => {
221
- const tokens = tokenize(item.question);
200
+ return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: DEFAULT_CROSS_OUT_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) => {
201
+ const tokens = tokenizeCrossOutQuestion(item.question);
222
202
  const selectedWordIndex = item.correctAnswer.wordIndex;
223
203
  return (_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("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700", children: item.questionNumber || index + 1 }), _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: [_jsx("div", { className: "w-52", children: _jsxs(Select, { value: item.type, onValueChange: (value) => updateItemType(index, value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Lo\u1EA1i c\u00E2u" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "CROSS_OUT", children: "Ch\u1EC9 g\u1EA1ch (CROSS_OUT)" }), _jsx(SelectItem, { value: "CROSS_OUT_AND_REPLACE", children: "G\u1EA1ch + thay (CROSS_OUT_AND_REPLACE)" })] })] }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, {
224
204
  isExample: checked,
@@ -3,3 +3,4 @@ export * from './CrossOutWordGroupClient';
3
3
  export * from './transform';
4
4
  export * from './register';
5
5
  export * from './map-cross-out-word-group-data';
6
+ export * from './tokenize';
@@ -3,3 +3,4 @@ export * from './CrossOutWordGroupClient';
3
3
  export * from './transform';
4
4
  export * from './register';
5
5
  export * from './map-cross-out-word-group-data';
6
+ export * from './tokenize';
@@ -14,20 +14,18 @@ function normalizeMode(type) {
14
14
  export function createCrossOutAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
15
15
  const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
16
16
  ? wordIndex
17
- : wordIndex === null
18
- ? null
19
- : undefined;
17
+ : null;
20
18
  if (type === 'CROSS_OUT_AND_REPLACE') {
21
19
  return {
22
20
  type,
23
- ...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
21
+ wordIndex: parsedIndex,
24
22
  crossOut,
25
23
  replaceWith,
26
24
  };
27
25
  }
28
26
  return {
29
27
  type: 'CROSS_OUT',
30
- ...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
28
+ wordIndex: parsedIndex,
31
29
  crossOut,
32
30
  };
33
31
  }
@@ -47,10 +45,12 @@ export function mapCrossOutWordGroupItem(item, index, fallbackAnswer) {
47
45
  const correctAnswerRaw = asRecord(record.correctAnswer);
48
46
  const answerObj = isRecord(correctAnswerRaw.answer)
49
47
  ? correctAnswerRaw.answer
50
- : isRecord(fallbackAnswer)
51
- ? fallbackAnswer
52
- : {};
53
- const type = normalizeMode(content.type || answerObj.type);
48
+ : 'crossOut' in correctAnswerRaw || 'wordIndex' in correctAnswerRaw
49
+ ? correctAnswerRaw
50
+ : isRecord(fallbackAnswer)
51
+ ? fallbackAnswer
52
+ : {};
53
+ const type = normalizeMode(content.type || record.type || answerObj.type);
54
54
  const isExample = record.isExample === true;
55
55
  const rawIndex = typeof answerObj.wordIndex === 'number'
56
56
  ? answerObj.wordIndex
@@ -0,0 +1,2 @@
1
+ export declare function normalizeCrossOutQuestionText(question: string): string;
2
+ export declare function tokenizeCrossOutQuestion(question: string): string[];
@@ -0,0 +1,11 @@
1
+ export function normalizeCrossOutQuestionText(question) {
2
+ return question
3
+ .replace(/\\n/g, ' ')
4
+ .replace(/\r\n|\r|\n/g, ' ')
5
+ .replace(/\s+/g, ' ')
6
+ .trim();
7
+ }
8
+ export function tokenizeCrossOutQuestion(question) {
9
+ const normalized = normalizeCrossOutQuestionText(question);
10
+ return normalized ? normalized.split(' ').filter(Boolean) : [];
11
+ }