@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.10

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 (104) hide show
  1. package/dist/components/exams/take/components/QuestionRenderer.js +1 -5
  2. package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
  3. package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
  4. package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
  5. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
  6. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
  7. package/dist/components/exams/take/utils/answer-transformers.js +16 -1
  8. package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
  9. package/dist/components/exams/take/utils/question-transformers.d.ts +5 -0
  10. package/dist/components/exams/take/utils/question-transformers.js +6 -0
  11. package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
  12. package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
  13. package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
  14. package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
  15. package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
  16. package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
  17. package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
  18. package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
  19. package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
  20. package/dist/components/questions/_shared/components/compact/index.js +4 -0
  21. package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
  22. package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
  23. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
  24. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
  25. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
  26. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
  27. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
  28. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
  29. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
  30. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
  31. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
  32. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
  33. package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
  34. package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
  35. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
  36. package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
  37. package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
  38. package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
  39. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
  40. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
  41. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
  42. package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
  43. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
  44. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
  45. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
  46. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
  47. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
  48. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
  49. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
  50. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
  51. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
  52. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
  53. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
  54. package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
  55. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
  56. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
  57. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
  58. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
  59. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
  60. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
  61. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
  62. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
  63. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
  64. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
  65. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
  66. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
  67. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
  68. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +13 -13
  69. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +7 -6
  70. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
  71. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
  72. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
  73. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
  74. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
  75. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  76. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
  77. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
  78. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
  79. package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
  80. package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
  81. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
  82. package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
  83. package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
  84. package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
  85. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +21 -23
  86. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
  87. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
  88. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
  89. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
  90. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
  91. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
  92. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
  93. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
  94. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
  95. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
  96. package/dist/components/questions/viewer/QuestionViewer.js +2 -2
  97. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
  98. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
  99. package/dist/components/results/renderers/review-question-body-movers.js +1 -1
  100. package/dist/components/results/review-data.js +8 -1
  101. package/dist/shared/lib/utils/question-reverse-transform.js +31 -5
  102. package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
  103. package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
  104. package/package.json +1 -1
@@ -102,7 +102,7 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
102
102
  }
103
103
  case 'CROSSWORD_PUZZLE': {
104
104
  const data = transformCrosswordPuzzle(questions);
105
- return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
105
+ return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, markedColumn: data.markedColumn, hiddenWord: data.hiddenWord, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, caseSensitive: data.caseSensitive }));
106
106
  }
107
107
  case 'IMAGE_OBJECT_MATCHING': {
108
108
  // Reuse coloring renderer — same content structure as READ_AND_COLOR_OBJECTS
@@ -125,10 +125,6 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
125
125
  onAnswerChange(regionId, hintItemId);
126
126
  }, isReviewMode: isReviewMode }));
127
127
  }
128
- case 'CROSSWORD_PUZZLE': {
129
- const data = transformCrosswordPuzzle(questions);
130
- return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
131
- }
132
128
  case 'SORT_WORDS_INTO_CATEGORIES': {
133
129
  const data = transformSortWordsIntoCategories(questions);
134
130
  const gradableCount = data.words.filter((word) => !word.isExample).length;
@@ -8,9 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  import { useMemo } from 'react';
9
9
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
10
10
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
11
- function tokenize(sentence) {
12
- return sentence.trim().split(/\s+/).filter(Boolean);
13
- }
11
+ import { tokenizeCrossOutQuestion } from '../../../../questions/types/cross-out-word-group/tokenize';
14
12
  function normalizeWord(word) {
15
13
  return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
16
14
  }
@@ -27,12 +25,20 @@ function formatAnswerLabel(answer) {
27
25
  return 'câu đúng (không gạch)';
28
26
  return `gạch "${answer.crossOut}"${posInfo}`;
29
27
  }
28
+ function isCorrectToken(correct, tokenIndex, plain) {
29
+ if (!correct)
30
+ return false;
31
+ if (typeof correct.wordIndex === 'number' && correct.wordIndex >= 0) {
32
+ return correct.wordIndex === tokenIndex;
33
+ }
34
+ return Boolean(correct.crossOut) && normalizeWord(correct.crossOut) === plain && plain !== '';
35
+ }
30
36
  function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswer, id, }) {
31
37
  const isDisabled = isReviewMode || question.isExample;
32
38
  const currentAnswer = question.isExample
33
39
  ? question.exampleCorrectAnswer
34
40
  : answer;
35
- const tokens = useMemo(() => tokenize(question.question), [question.question]);
41
+ const tokens = useMemo(() => tokenizeCrossOutQuestion(question.question), [question.question]);
36
42
  const selectedWord = currentAnswer?.crossOut || '';
37
43
  const selectedIndex = currentAnswer?.wordIndex;
38
44
  const replaceWith = currentAnswer?.replaceWith || '';
@@ -76,11 +82,10 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
76
82
  });
77
83
  }
78
84
  else {
79
- const cleanWord = rawToken.replace(/[^\p{L}\p{N}'-]/gu, '') || rawToken;
80
85
  onAnswerChange(question.id, {
81
86
  type: mode,
82
87
  wordIndex: tokenIndex,
83
- crossOut: cleanWord,
88
+ crossOut: rawToken,
84
89
  ...(mode === 'CROSS_OUT_AND_REPLACE' ? { replaceWith } : {}),
85
90
  });
86
91
  }
@@ -109,9 +114,9 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
109
114
  ? 'bg-emerald-500'
110
115
  : isWrong
111
116
  ? 'bg-rose-500'
112
- : 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full border border-amber-300 bg-amber-100/90 px-2 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800", children: "EXAMPLE" })), _jsx("span", { className: `rounded-full px-2.5 py-0.5 text-[11px] font-bold ${mode === 'CROSS_OUT_AND_REPLACE'
117
+ : 'bg-blue-600'}`, children: question.questionNumber }), question.isExample && (_jsx("span", { className: "rounded-full border border-amber-300 bg-amber-100/90 px-2 py-0.5 text-[10.5px] font-extrabold uppercase tracking-wider text-amber-800", children: "EXAMPLE" })), isReviewMode ? (_jsx("span", { className: `rounded-full px-2.5 py-0.5 text-[11px] font-bold ${mode === 'CROSS_OUT_AND_REPLACE'
113
118
  ? 'bg-violet-100/80 text-violet-700 border border-violet-200/60'
114
- : 'bg-sky-100/80 text-sky-700 border border-sky-200/60'}`, children: mode === 'CROSS_OUT_AND_REPLACE' ? 'Gạch + Thay thế' : 'Chỉ gạch' }), !question.isExample && selectedWord && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10.5px] font-bold text-emerald-700", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }), "\u0110\u00E3 g\u1EA1ch: \"", selectedWord, "\"", typeof selectedIndex === 'number' && (_jsxs("span", { className: "text-[10px] text-emerald-600 font-normal", children: ["(t\u1EEB #", selectedIndex + 1, ")"] }))] }))] }), mode === 'CROSS_OUT_AND_REPLACE' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-bold text-slate-600 whitespace-nowrap", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: isDisabled, onChange: (e) => setReplaceWith(e.target.value), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 w-40 sm:w-52 rounded-lg border border-slate-200 bg-white px-2.5 text-sm font-semibold text-slate-800 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-slate-50" })] }))] }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2 text-[15.5px] sm:text-[16.5px] font-medium leading-loose text-slate-800 px-1", children: tokens.map((token, tokenIndex) => {
119
+ : 'bg-sky-100/80 text-sky-700 border border-sky-200/60'}`, children: mode === 'CROSS_OUT_AND_REPLACE' ? 'Gạch + Thay thế' : 'Chỉ gạch' })) : null, !question.isExample && selectedWord && (_jsxs("span", { className: "flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10.5px] font-bold text-emerald-700", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }), "\u0110\u00E3 g\u1EA1ch: \"", selectedWord, "\"", typeof selectedIndex === 'number' && (_jsxs("span", { className: "text-[10px] text-emerald-600 font-normal", children: ["(t\u1EEB #", selectedIndex + 1, ")"] }))] }))] }), mode === 'CROSS_OUT_AND_REPLACE' && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-bold text-slate-600 whitespace-nowrap", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: isDisabled, onChange: (e) => setReplaceWith(e.target.value), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 w-40 sm:w-52 rounded-lg border border-slate-200 bg-white px-2.5 text-sm font-semibold text-slate-800 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-slate-50" })] }))] }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2 text-[15.5px] sm:text-[16.5px] font-medium leading-loose text-slate-800 px-1", children: tokens.map((token, tokenIndex) => {
115
120
  const plain = normalizeWord(token);
116
121
  const isSelected = selectedIndex !== undefined && selectedIndex !== null
117
122
  ? selectedIndex === tokenIndex
@@ -119,12 +124,21 @@ function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctA
119
124
  const isCorrectTargetInReview = isReviewMode &&
120
125
  !question.isExample &&
121
126
  correctAnswer?.wordIndex === tokenIndex;
127
+ const isSelectedCorrect = isReviewMode &&
128
+ !question.isExample &&
129
+ isSelected &&
130
+ isCorrectToken(correctAnswer, tokenIndex, plain);
131
+ const isSelectedWrong = isReviewMode &&
132
+ !question.isExample &&
133
+ isSelected &&
134
+ !!correctAnswer &&
135
+ !isSelectedCorrect;
122
136
  return (_jsx("button", { type: "button", disabled: isDisabled || plain === '', onClick: () => setCrossOut(tokenIndex, token), className: `rounded-lg px-2 py-1 transition-all duration-150 select-none relative ${isSelected
123
137
  ? question.isExample
124
138
  ? 'bg-amber-200/90 text-amber-900 font-bold line-through decoration-amber-700 decoration-2 shadow-sm'
125
- : isCorrect
139
+ : isSelectedCorrect
126
140
  ? 'bg-emerald-100 text-emerald-800 font-bold line-through decoration-emerald-600 decoration-2 shadow-sm border border-emerald-300'
127
- : isWrong
141
+ : isSelectedWrong
128
142
  ? 'bg-rose-100 text-rose-700 font-bold line-through decoration-rose-600 decoration-2 shadow-sm border border-rose-300 scale-[0.98]'
129
143
  : 'bg-blue-100 text-blue-700 font-bold line-through decoration-blue-600 decoration-2 shadow-sm border border-blue-200 scale-[0.98]'
130
144
  : isCorrectTargetInReview
@@ -307,5 +307,5 @@ export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCo
307
307
  inputRefs.current.delete(key);
308
308
  }, type: "text", maxLength: 1, disabled: Boolean(isExample), value: cellValue, onChange: (e) => handleInputChange(e.target.value, r, c), onKeyDown: (e) => handleKeyDown(e, r, c), onFocus: () => handleCellClick(r, c), className: `w-full h-full text-center bg-transparent uppercase font-bold outline-none ${isExample ? 'cursor-default' : 'cursor-pointer'}` })) : (_jsxs("div", { className: "flex flex-col items-center justify-center", children: [_jsx("span", { children: cellValue || '—' }), reviewStatus && !reviewStatus.isCorrect && (_jsx("span", { className: "absolute -bottom-1 -right-1 text-[9px] bg-green-600 text-white font-bold px-1 rounded-full shadow", children: reviewStatus.expected }))] })), isReviewMode && reviewStatus && (_jsx("span", { className: `absolute -top-1 -right-1 rounded-full p-0.5 text-white ${reviewStatus.isCorrect ? 'bg-green-500' : 'bg-red-500'}`, children: reviewStatus.isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5" })) : (_jsx(X, { className: "h-2.5 w-2.5" })) }))] }, key));
309
309
  });
310
- }) }) })] })] })] }));
310
+ }) }) }), (markedColumn || hiddenWord) && (_jsxs("div", { className: "w-full max-w-md rounded-xl border-2 border-amber-200 bg-amber-50 p-4", children: [_jsx("label", { className: "block text-sm font-bold text-amber-900 mb-2", children: "What's the hidden word?" }), !isReviewMode ? (_jsx("input", { type: "text", value: answers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded-lg border border-amber-300 bg-white px-3 py-2 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [answers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: answers['hidden-word'] })), hiddenWord && (_jsxs("div", { className: "mt-1 text-xs", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: hiddenWord })] }))] }))] }))] })] })] }));
311
311
  }
@@ -8,6 +8,7 @@ import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
8
8
  import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
9
9
  import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
10
10
  import CambridgeYleAudioPlayer from '../../../../../components/themes/cambridge-yle/CambridgeYleAudioPlayer';
11
+ import { FillInBlankAnswerKey } from '../../../../questions/types/fill-in-blank-group/FillInBlankAnswerKey';
11
12
  import { extractPlaceholderIndices, isBlankMatch, padStudentAnswers, splitStem, toFillInBlankImageUrls, } from '../../../../questions/types/fill-in-blank-group/blank-utils';
12
13
  function emptyAnswer(blankCount) {
13
14
  return { answers: Array.from({ length: blankCount }, () => '') };
@@ -53,7 +54,7 @@ function ItemCard({ question, answer, onAnswerChange, isReviewMode, correctAnswe
53
54
  : blankWrong
54
55
  ? 'border-rose-400 bg-rose-50 text-rose-800'
55
56
  : 'border-blue-200 bg-blue-50/40 text-blue-900 focus:border-blue-500 focus:bg-white'}` }, partIndex));
56
- }) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsxs("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-sm text-emerald-800", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", expected.map((alts) => alts.join(' / ') || '(trống)').join(' | ')] }))] }));
57
+ }) })] }), isReviewMode && !question.isExample && expected.length > 0 && (_jsx("div", { className: "mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2", children: _jsx(FillInBlankAnswerKey, { answersByBlank: expected, blankIndices: blankIndices }) }))] }));
57
58
  }
58
59
  export function MoversFillInBlankGroupRenderer({ partNumber, partName, questionCount, instruction, audioUrl, imageUrl, showHints = false, hints = [], questions, answers, onAnswerChange, isReviewMode = false, correctAnswerMap, }) {
59
60
  return (_jsxs("div", { className: "relative flex h-full flex-col gap-4 overflow-y-auto custom-scrollbar sm:gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 p-3 sm:gap-3 sm:px-6 sm:pt-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), (audioUrl || toFillInBlankImageUrls(imageUrl).length > 0) && (_jsxs("div", { className: "sticky top-0 z-20 flex flex-col gap-3 border-b border-gray-100 bg-white/95 p-3 pb-2 shadow-sm backdrop-blur sm:px-6", children: [audioUrl && _jsx(CambridgeYleAudioPlayer, { audioSrc: audioUrl, compact: true }), toFillInBlankImageUrls(imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(imageUrl).map((url, imageIndex) => (_jsx(ResolvedImage, { src: url, alt: `Shared illustration ${imageIndex + 1}`, className: "max-h-[180px] rounded-lg object-contain" }, `group-image-${imageIndex}`))) }))] })), showHints && hints.length > 0 && !isReviewMode && (_jsx("div", { className: "flex flex-wrap gap-2 px-3 sm:px-6", children: hints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 sm:px-6 lg:grid-cols-3", children: questions.map((question) => (_jsx(ItemCard, { question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswer: correctAnswerMap?.[question.id] }, question.id))) })] }));
@@ -37,14 +37,24 @@ export function SpeakingSpontaneousQaGroupRenderer(props) {
37
37
  useEffect(() => {
38
38
  setSequenceStep(getInitialSequenceStep());
39
39
  }, [getInitialSequenceStep, setup.nav.currentIndex]);
40
+ const hasQuestionAudio = Boolean(content?.audioUrl?.trim());
40
41
  useEffect(() => {
41
42
  if (sequenceStep !== SPONTANEOUS_QA_SEQUENCE_STEP.DELAYING || shouldResumeCompletedState)
42
43
  return;
43
44
  const timer = setTimeout(() => {
44
- setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION);
45
+ setSequenceStep(hasQuestionAudio
46
+ ? SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION
47
+ : SPONTANEOUS_QA_SEQUENCE_STEP.ALLOW_RECORDING);
45
48
  }, 3000);
46
49
  return () => clearTimeout(timer);
47
- }, [sequenceStep, shouldResumeCompletedState]);
50
+ }, [hasQuestionAudio, sequenceStep, shouldResumeCompletedState]);
51
+ useEffect(() => {
52
+ if (sequenceStep === SPONTANEOUS_QA_SEQUENCE_STEP.PLAYING_QUESTION &&
53
+ !hasQuestionAudio &&
54
+ !shouldResumeCompletedState) {
55
+ setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.ALLOW_RECORDING);
56
+ }
57
+ }, [hasQuestionAudio, sequenceStep, shouldResumeCompletedState]);
48
58
  const handleTeacherVideoEnded = useCallback(() => {
49
59
  setSequenceStep(SPONTANEOUS_QA_SEQUENCE_STEP.DELAYING);
50
60
  props.onTeacherIntroPlayed?.();
@@ -27,8 +27,9 @@ export function useSpontaneousQaGroupRendererSetup({ part, answers, onAnswerChan
27
27
  flattenIndex: child.flattenIndex,
28
28
  expectedAnswers: child.expectedAnswers,
29
29
  points: child.points,
30
+ isExample: child.isExample,
30
31
  },
31
- is_example: false,
32
+ is_example: child.isExample,
32
33
  correct_answer: { expectedAnswers: child.expectedAnswers },
33
34
  total_points: String(child.points),
34
35
  question_type: 'SPONTANEOUS_QA_GROUP',
@@ -263,6 +263,16 @@ export function transformAnswersForSubmission(answers, parts) {
263
263
  if (speakingQuestionIds.has(questionId) && typeof answer === 'string') {
264
264
  continue;
265
265
  }
266
+ if (questionId === 'hidden-word') {
267
+ const parent = findGridParentQuestion(parts);
268
+ if (parent?.questionType === 'CROSSWORD_PUZZLE') {
269
+ if (!gridCellAnswers.has(parent.parentId)) {
270
+ gridCellAnswers.set(parent.parentId, {});
271
+ }
272
+ gridCellAnswers.get(parent.parentId)['hidden-word'] = String(answer);
273
+ }
274
+ continue;
275
+ }
266
276
  // Handle grid cell keys (from FILL_MISSING_WORDS_IN_GRID)
267
277
  if (isGridCellKey(questionId)) {
268
278
  const parent = findGridParentQuestion(parts);
@@ -454,9 +464,14 @@ export function transformAnswersForSubmission(answers, parts) {
454
464
  words[`down-${c.number}`] = word;
455
465
  }
456
466
  }
467
+ const hiddenWord = cellAnswers['hidden-word'];
468
+ const cells = Object.fromEntries(Object.entries(cellAnswers).filter(([key]) => key !== 'hidden-word'));
469
+ if (hiddenWord) {
470
+ words['hidden-word'] = hiddenWord;
471
+ }
457
472
  result.push({
458
473
  questionId: parentId,
459
- answer: { answers: words, cells: cellAnswers },
474
+ answer: { answers: words, cells },
460
475
  });
461
476
  }
462
477
  else {
@@ -63,21 +63,19 @@ export function normalizeCrossOutWordGroupAnswer(raw) {
63
63
  const rawIndex = obj.wordIndex;
64
64
  const wordIndex = typeof rawIndex === 'number' && Number.isInteger(rawIndex) && rawIndex >= 0
65
65
  ? rawIndex
66
- : rawIndex === null
67
- ? null
68
- : undefined;
66
+ : null;
69
67
  return type === 'CROSS_OUT_AND_REPLACE'
70
68
  ? {
71
69
  type,
72
- ...(wordIndex !== undefined ? { wordIndex } : {}),
70
+ wordIndex,
73
71
  crossOut: typeof obj.crossOut === 'string' ? obj.crossOut : '',
74
72
  replaceWith: typeof obj.replaceWith === 'string' ? obj.replaceWith : '',
75
73
  }
76
74
  : {
77
75
  type: 'CROSS_OUT',
78
- ...(wordIndex !== undefined ? { wordIndex } : {}),
76
+ wordIndex,
79
77
  crossOut: typeof obj.crossOut === 'string' ? obj.crossOut : '',
80
78
  };
81
79
  }
82
- return { type: 'CROSS_OUT', crossOut: '' };
80
+ return { type: 'CROSS_OUT', wordIndex: null, crossOut: '' };
83
81
  }
@@ -472,14 +472,19 @@ export interface CrosswordClueItem {
472
472
  startRow: number;
473
473
  startCol: number;
474
474
  length: number;
475
+ isExample?: boolean;
475
476
  }
476
477
  export interface CrosswordPuzzlePartData {
478
+ type?: 'STANDARD' | 'HIDDEN_WORD';
477
479
  title?: string;
478
480
  instruction: string;
479
481
  gridSize: {
480
482
  rows: number;
481
483
  cols: number;
482
484
  };
485
+ markedColumn?: number;
486
+ hiddenWord?: string;
487
+ caseSensitive?: boolean;
483
488
  clues: {
484
489
  across: CrosswordClueItem[];
485
490
  down: CrosswordClueItem[];
@@ -1258,6 +1258,7 @@ export function transformCrosswordPuzzle(questions) {
1258
1258
  startRow: Number(c.startRow) || 1,
1259
1259
  startCol: Number(c.startCol) || 1,
1260
1260
  length: Number(c.length) || 1,
1261
+ ...(c.isExample === true ? { isExample: true } : {}),
1261
1262
  }))
1262
1263
  : [];
1263
1264
  const down = Array.isArray(content?.clues?.down)
@@ -1267,15 +1268,20 @@ export function transformCrosswordPuzzle(questions) {
1267
1268
  startRow: Number(c.startRow) || 1,
1268
1269
  startCol: Number(c.startCol) || 1,
1269
1270
  length: Number(c.length) || 1,
1271
+ ...(c.isExample === true ? { isExample: true } : {}),
1270
1272
  }))
1271
1273
  : [];
1272
1274
  const correctAnswers = correctAnswer?.answers && typeof correctAnswer.answers === 'object'
1273
1275
  ? correctAnswer.answers
1274
1276
  : {};
1275
1277
  return {
1278
+ type: content?.type === 'HIDDEN_WORD' ? 'HIDDEN_WORD' : 'STANDARD',
1276
1279
  title: content?.title || '',
1277
1280
  instruction: question?.instruction || content?.instruction || 'Complete the crossword.',
1278
1281
  gridSize: { rows, cols },
1282
+ ...(typeof content?.markedColumn === 'number' ? { markedColumn: content.markedColumn } : {}),
1283
+ hiddenWord: correctAnswers['hidden-word'] || '',
1284
+ caseSensitive: correctAnswer?.caseSensitive === true,
1279
1285
  clues: { across, down },
1280
1286
  correctAnswers,
1281
1287
  explanation: question?.explanation,
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function optionLetter(index: number): string;
3
+ export declare function KidChoiceRow({ letter, selected, isReviewMode, isCorrect, disabled, onClick, children, className, }: {
4
+ letter: string;
5
+ selected?: boolean;
6
+ isReviewMode?: boolean;
7
+ isCorrect?: boolean;
8
+ disabled?: boolean;
9
+ onClick?: () => void;
10
+ children: ReactNode;
11
+ className?: string;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, X } from 'lucide-react';
4
+ import { cn } from '../../../../../shared/lib/utils';
5
+ export function optionLetter(index) {
6
+ return String.fromCharCode(65 + index);
7
+ }
8
+ export function KidChoiceRow({ letter, selected = false, isReviewMode = false, isCorrect = false, disabled = false, onClick, children, className, }) {
9
+ const showStatus = isReviewMode && (isCorrect || selected);
10
+ const rowTone = isReviewMode
11
+ ? isCorrect
12
+ ? 'border-green-400 bg-green-50'
13
+ : selected
14
+ ? 'border-red-400 bg-red-50'
15
+ : 'border-gray-200 bg-gray-50'
16
+ : selected
17
+ ? 'border-blue-500 bg-blue-50 shadow-sm ring-2 ring-blue-100'
18
+ : 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm';
19
+ const badgeTone = isReviewMode
20
+ ? isCorrect
21
+ ? 'bg-green-500 text-white'
22
+ : selected
23
+ ? 'bg-red-500 text-white'
24
+ : 'bg-gray-200 text-gray-600'
25
+ : selected
26
+ ? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm'
27
+ : 'bg-gray-100 text-gray-600 group-hover:bg-gray-200';
28
+ return (_jsxs("button", { type: "button", onClick: onClick, disabled: disabled || isReviewMode || !onClick, className: cn('group flex w-full items-center gap-2 rounded-md border px-3 py-1.5 text-left transition-all', rowTone, (disabled || isReviewMode || !onClick) && 'cursor-default', className), 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', badgeTone), children: letter }), _jsx("div", { className: cn('min-w-0 flex-1 text-sm font-medium', isReviewMode && isCorrect
29
+ ? 'text-green-800'
30
+ : isReviewMode && selected && !isCorrect
31
+ ? 'text-red-800'
32
+ : 'text-gray-700'), children: children }), showStatus && (_jsx("div", { className: cn('flex h-5 w-5 shrink-0 items-center justify-center rounded-full', isCorrect ? 'bg-green-500' : 'bg-red-500'), children: isCorrect ? _jsx(Check, { className: "h-3 w-3 text-white" }) : _jsx(X, { className: "h-3 w-3 text-white" }) }))] }));
33
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function KidInstruction({ children, className, }: {
3
+ children: ReactNode;
4
+ className?: string;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { cn } from '../../../../../shared/lib/utils';
4
+ export function KidInstruction({ children, className, }) {
5
+ if (!children) {
6
+ return null;
7
+ }
8
+ return (_jsx("div", { className: cn('rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800', className), children: children }));
9
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function KidPrompt({ children, className, }: {
3
+ children: ReactNode;
4
+ className?: string;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { cn } from '../../../../../shared/lib/utils';
4
+ export function KidPrompt({ children, className, }) {
5
+ if (!children) {
6
+ return null;
7
+ }
8
+ return (_jsx("div", { className: cn('rounded-md border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 px-3 py-2 text-sm font-semibold text-gray-800', className), children: children }));
9
+ }
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function KidQuestionCard({ icon, title, points, isExample, review, headerExtra, children, className, contentClassName, }: {
3
+ icon?: ReactNode;
4
+ title: string;
5
+ points?: number;
6
+ isExample?: boolean;
7
+ review?: 'correct' | 'wrong' | null;
8
+ headerExtra?: ReactNode;
9
+ children: ReactNode;
10
+ className?: string;
11
+ contentClassName?: string;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, CircleHelp, Star, X } from 'lucide-react';
4
+ import { cn } from '../../../../../shared/lib/utils';
5
+ export function KidQuestionCard({ icon, title, points, isExample = false, review, headerExtra, children, className, contentClassName, }) {
6
+ return (_jsxs("div", { className: cn('overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "truncate text-sm font-medium text-gray-700", children: title }), isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "C\u00E2u v\u00ED d\u1EE5"] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsxs("span", { className: "text-xs text-gray-500", children: [points, " \u0111i\u1EC3m"] })), review === 'correct' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700", children: [_jsx(Check, { className: "h-3 w-3" }), "\u0110\u00FAng"] })), review === 'wrong' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: [_jsx(X, { className: "h-3 w-3" }), "Sai"] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
7
+ }
@@ -4,3 +4,7 @@ export { CompactPreviewButton } from './CompactPreviewButton';
4
4
  export { CompactCreatorHeader } from './CompactCreatorHeader';
5
5
  export { CompactFieldCard } from './CompactFieldCard';
6
6
  export { CompactExplanationToggle } from './CompactExplanationToggle';
7
+ export { KidInstruction } from './KidInstruction';
8
+ export { KidPrompt } from './KidPrompt';
9
+ export { KidQuestionCard } from './KidQuestionCard';
10
+ export { KidChoiceRow, optionLetter } from './KidChoiceRow';
@@ -4,3 +4,7 @@ export { CompactPreviewButton } from './CompactPreviewButton';
4
4
  export { CompactCreatorHeader } from './CompactCreatorHeader';
5
5
  export { CompactFieldCard } from './CompactFieldCard';
6
6
  export { CompactExplanationToggle } from './CompactExplanationToggle';
7
+ export { KidInstruction } from './KidInstruction';
8
+ export { KidPrompt } from './KidPrompt';
9
+ export { KidQuestionCard } from './KidQuestionCard';
10
+ export { KidChoiceRow, optionLetter } from './KidChoiceRow';
@@ -1,13 +1,17 @@
1
- import type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer } from '../../../../shared/types/questions/crossword-puzzle';
1
+ import type { CrosswordClue, CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer, CrosswordSubtype } from '../../../../shared/types/questions/crossword-puzzle';
2
2
  export interface CrosswordClueFormItem extends CrosswordClue {
3
3
  id: string;
4
4
  answer: string;
5
5
  direction: 'across' | 'down';
6
6
  }
7
7
  export interface CrosswordPuzzleFormState {
8
+ type?: CrosswordSubtype;
8
9
  title?: string;
10
+ instruction?: string;
9
11
  rows: number;
10
12
  cols: number;
13
+ markedColumn?: number;
14
+ hiddenWord?: string;
11
15
  clues: CrosswordClueFormItem[];
12
16
  caseSensitive: boolean;
13
17
  points: number;
@@ -6,6 +6,7 @@ export interface SpontaneousQaGroupQuestionData {
6
6
  expectedAnswers: string[];
7
7
  points: number;
8
8
  questionNumber: number;
9
+ isExample?: boolean;
9
10
  }
10
11
  export interface SpontaneousQaGroupPartData {
11
12
  partNumber: number;
@@ -31,7 +32,23 @@ export interface SpontaneousQaGroupCreatorProps {
31
32
  getFormData?: () => SpontaneousQaGroupData;
32
33
  } | null>;
33
34
  }
35
+ export interface SpontaneousQaGroupStudentAnswer {
36
+ audioKeys: Array<string | null>;
37
+ }
38
+ export interface SpontaneousQaGroupGradeDetail {
39
+ itemId: string;
40
+ isCorrect?: boolean;
41
+ userAnswer?: unknown;
42
+ correctAnswer?: unknown;
43
+ partialScore?: number;
44
+ feedback?: string;
45
+ }
34
46
  export interface SpontaneousQaGroupClientProps {
35
47
  questionData: SpontaneousQaGroupData;
36
48
  isReviewMode?: boolean;
49
+ userAnswer?: SpontaneousQaGroupStudentAnswer;
50
+ onItemRecorded?: (flattenIndex: number, blob: Blob) => void;
51
+ uploadingIndex?: number | null;
52
+ gradeDetails?: SpontaneousQaGroupGradeDetail[];
53
+ disabled?: boolean;
37
54
  }
@@ -1,11 +1,10 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Card, CardContent } from '../../../../components/ui/card';
4
3
  import { Input } from '../../../../components/ui/input';
5
- import { Label } from '../../../../components/ui/label';
6
- import { CheckCircle2, XCircle, BookOpen } from 'lucide-react';
4
+ import { CheckCircle2, XCircle, PenLine } from 'lucide-react';
7
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
8
6
  import { toAnswerTheQuestionImageUrls, } from '../../_shared/types/answer-the-question.type';
7
+ import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
9
8
  import { useState } from 'react';
10
9
  function isDisplayableMediaUrl(value) {
11
10
  return (value.startsWith('http://')
@@ -33,7 +32,7 @@ export function AnswerTheQuestionClient({ questionData, onSubmit, isReviewMode =
33
32
  const [answer, setAnswer] = useState(userAnswer || '');
34
33
  const imageUrls = toAnswerTheQuestionImageUrls(questionData.imageUrl);
35
34
  if (!questionData.question && imageUrls.length === 0) {
36
- return (_jsx(Card, { children: _jsx(CardContent, { className: "p-3", children: _jsx("p", { className: "text-gray-500", children: "Kh\u00F4ng c\u00F3 d\u1EEF li\u1EC7u c\u00E2u h\u1ECFi" }) }) }));
35
+ return (_jsx(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: "Tr\u1EA3 l\u1EDDi c\u00E2u h\u1ECFi", children: _jsx("p", { className: "text-sm text-gray-500", children: "Kh\u00F4ng c\u00F3 d\u1EEF li\u1EC7u c\u00E2u h\u1ECFi" }) }));
37
36
  }
38
37
  const isExample = questionData.isExample || false;
39
38
  const correctAnswer = questionData.correctAnswer || '';
@@ -43,19 +42,19 @@ export function AnswerTheQuestionClient({ questionData, onSubmit, isReviewMode =
43
42
  const isCorrect = isReviewMode && userAnswer !== undefined
44
43
  ? userAnswer.trim().toLowerCase() === correctAnswer.trim().toLowerCase()
45
44
  : undefined;
46
- return (_jsx(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: _jsxs(CardContent, { className: "p-3 space-y-3", children: [(questionData.title || questionData.groupContent) && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-gradient-to-r from-indigo-50 to-blue-50 p-4 space-y-2", children: [questionData.title && (_jsx("h3", { className: "text-base font-bold text-indigo-900", children: questionData.title })), questionData.groupContent && (_jsx("p", { className: "text-sm text-indigo-700 leading-relaxed whitespace-pre-wrap", children: questionData.groupContent }))] })), imageUrls.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: imageUrls.map((url, imageIndex) => (_jsx("div", { className: "relative max-w-lg overflow-hidden rounded-xl border border-gray-200 bg-gray-50 shadow-sm", children: _jsx(MediaPreview, { src: url, alt: `Question image ${imageIndex + 1}` }) }, `atq-image-${imageIndex}`))) })), _jsxs("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 p-5", children: [isExample && (_jsxs("div", { className: "mb-2 inline-flex items-center gap-1.5 rounded-full bg-amber-100 border border-amber-300 px-3 py-1", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5 text-amber-600" }), _jsx("span", { className: "text-xs font-semibold text-amber-700 uppercase tracking-wide", children: "Example" })] })), questionData.question ? (_jsx(Label, { className: "text-base font-semibold text-gray-800 mb-3 block", children: questionData.question })) : null, questionData.suggestedAnswers && questionData.suggestedAnswers.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 mt-2 mb-1", children: questionData.suggestedAnswers.map((suggestion, index) => (_jsx("button", { type: "button", disabled: isReviewMode || isExample, onClick: () => {
47
- if (!isReviewMode && !isExample) {
48
- setAnswer(suggestion);
49
- onSubmit?.(suggestion);
50
- }
51
- }, className: `inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium transition-colors ${answer === suggestion
52
- ? 'border-teal-400 bg-teal-50 text-teal-700 ring-1 ring-teal-200'
53
- : 'border-gray-200 bg-white text-gray-600 hover:border-teal-300 hover:bg-teal-50/50 hover:text-teal-600'} ${isReviewMode || isExample ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`, children: suggestion }, index))) })), _jsxs("div", { className: "flex items-center gap-3 mt-3", children: [questionData.prefixText && (_jsx("span", { className: "text-base font-medium text-gray-700", children: questionData.prefixText })), _jsx(Input, { value: displayedAnswer, onChange: (e) => {
54
- setAnswer(e.target.value);
55
- onSubmit?.(e.target.value);
56
- }, placeholder: "Vi\u1EBFt \u0111\u00E1p \u00E1n...", disabled: isReviewMode || isExample, className: `max-w-xs border-2 text-lg font-medium ${isReviewMode
57
- ? isCorrect
58
- ? 'border-green-400 bg-green-50 text-green-700'
59
- : 'border-red-400 bg-red-50 text-red-700'
60
- : 'border-teal-300 focus:border-teal-500 focus:ring-teal-200'}` }), questionData.suffixText && (_jsx("span", { className: "text-base font-medium text-gray-700", children: questionData.suffixText })), isReviewMode && isCorrect !== undefined && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(CheckCircle2, { className: "h-5 w-5 text-green-500" })) : (_jsx(XCircle, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && isCorrect === false && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-green-50 border border-green-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: correctAnswer })] })] })), autoFillCorrectAnswers && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-teal-50 border border-teal-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-teal-600" }), _jsxs("span", { className: "text-sm text-teal-700", children: ["\u0110\u00E1p \u00E1n: ", _jsx("strong", { children: correctAnswer })] })] }))] }), explanation && isReviewMode && (_jsxs("div", { className: "rounded-xl border border-blue-200 bg-blue-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-blue-800 mb-1", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "text-sm text-blue-700", children: explanation })] }))] }) }));
45
+ return (_jsxs("div", { className: "space-y-3", children: [(questionData.title || questionData.groupContent) && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-gradient-to-r from-indigo-50 to-blue-50 p-4 space-y-2", children: [questionData.title && (_jsx("h3", { className: "text-base font-bold text-indigo-900", children: questionData.title })), questionData.groupContent && (_jsx("p", { className: "text-sm text-indigo-700 leading-relaxed whitespace-pre-wrap", children: questionData.groupContent }))] })), _jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: "Tr\u1EA3 l\u1EDDi c\u00E2u h\u1ECFi", points: questionData.points, isExample: isExample, review: isReviewMode && isCorrect !== undefined ? (isCorrect ? 'correct' : 'wrong') : null, children: [imageUrls.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: imageUrls.map((url, imageIndex) => (_jsx("div", { className: "relative max-w-lg overflow-hidden rounded-md border border-gray-200 bg-gray-50", children: _jsx(MediaPreview, { src: url, alt: `Question image ${imageIndex + 1}` }) }, `atq-image-${imageIndex}`))) })), questionData.question ? (_jsx(KidPrompt, { children: questionData.question })) : null, questionData.suggestedAnswers && questionData.suggestedAnswers.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 mt-2 mb-1", children: questionData.suggestedAnswers.map((suggestion, index) => (_jsx("button", { type: "button", disabled: isReviewMode || isExample, onClick: () => {
46
+ if (!isReviewMode && !isExample) {
47
+ setAnswer(suggestion);
48
+ onSubmit?.(suggestion);
49
+ }
50
+ }, className: `inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium transition-colors ${answer === suggestion
51
+ ? 'border-teal-400 bg-teal-50 text-teal-700 ring-1 ring-teal-200'
52
+ : 'border-gray-200 bg-white text-gray-600 hover:border-teal-300 hover:bg-teal-50/50 hover:text-teal-600'} ${isReviewMode || isExample ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`, children: suggestion }, index))) })), _jsxs("div", { className: "flex items-center gap-3 mt-1", children: [questionData.prefixText && (_jsx("span", { className: "text-sm font-medium text-gray-700", children: questionData.prefixText })), _jsx(Input, { value: displayedAnswer, onChange: (e) => {
53
+ setAnswer(e.target.value);
54
+ onSubmit?.(e.target.value);
55
+ }, placeholder: "Vi\u1EBFt \u0111\u00E1p \u00E1n...", disabled: isReviewMode || isExample, className: `max-w-xs border text-sm px-3 py-1.5 rounded-md font-medium ${isReviewMode
56
+ ? isCorrect
57
+ ? 'border-green-400 bg-green-50 text-green-800'
58
+ : 'border-red-400 bg-red-50 text-red-800'
59
+ : 'border-teal-300 focus:border-teal-500 focus:ring-teal-200'}` }), questionData.suffixText && (_jsx("span", { className: "text-sm font-medium text-gray-700", children: questionData.suffixText })), isReviewMode && isCorrect !== undefined && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(CheckCircle2, { className: "h-5 w-5 text-green-500" })) : (_jsx(XCircle, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && isCorrect === false && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-green-50 border border-green-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: correctAnswer })] })] })), autoFillCorrectAnswers && (_jsxs("div", { className: "mt-3 flex items-center gap-2 rounded-lg bg-teal-50 border border-teal-200 px-4 py-2", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-teal-600" }), _jsxs("span", { className: "text-sm text-teal-700", children: ["\u0110\u00E1p \u00E1n: ", _jsx("strong", { children: correctAnswer })] })] })), explanation && isReviewMode && (_jsxs("div", { className: "rounded-md border border-blue-200 bg-blue-50 px-3 py-2", children: [_jsx("p", { className: "text-sm font-semibold text-blue-800 mb-1", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "text-sm text-blue-700", children: explanation })] }))] })] }));
61
60
  }
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { BookOpen, CircleHelp, Lightbulb, Sparkles, Star, Volume2 } from 'lucide-react';
3
+ import { BookOpen, CircleHelp, Lightbulb, Sparkles, 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
+ import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
6
7
  function isAbsoluteMediaUrl(value) {
7
8
  return (value.startsWith('http://')
8
9
  || value.startsWith('https://')
@@ -41,15 +42,15 @@ function MediaPreview({ src, kind = 'image', alt }) {
41
42
  }
42
43
  export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
43
44
  const items = questionData.items || [];
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) => {
45
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { 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) => {
45
46
  const userValue = userAnswers[itemIndex] || '';
46
47
  const expectedAnswers = Array.isArray(item.expectedAnswers)
47
48
  ? item.expectedAnswers.filter((answer) => answer.trim() !== '')
48
49
  : [];
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}`));
50
+ return (_jsxs(KidQuestionCard, { icon: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }), title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, headerExtra: _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"] }), 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(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), isReviewMode || item.isExample || !onAnswerChange ? (userValue ? (_jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-700", children: userValue })) : null) : (_jsx("textarea", { value: userValue, onChange: (event) => {
51
+ const next = items.map((_, index) => userAnswers[index] || '');
52
+ next[itemIndex] = event.target.value;
53
+ onAnswerChange(next);
54
+ }, placeholder: "Nh\u1EADp c\u00E2u tr\u1EA3 l\u1EDDi...", rows: 2, className: "w-full rounded-md border border-slate-200 px-3 py-1.5 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-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-500", children: "Ch\u01B0a c\u00F3 \u0111\u00E1p \u00E1n" })) : null] }, `${item.questionNumber}-${itemIndex}`));
54
55
  }), 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] }))] }));
55
56
  }