@tinyweb_dev/oe-exam-sdk 1.0.8 → 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 (138) hide show
  1. package/dist/components/exams/take/components/QuestionRenderer.js +1 -5
  2. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
  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/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
  7. package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
  8. package/dist/components/exams/take/utils/answer-transformers.js +16 -1
  9. package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
  10. package/dist/components/exams/take/utils/question-transformers.d.ts +5 -0
  11. package/dist/components/exams/take/utils/question-transformers.js +6 -0
  12. package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
  13. package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
  14. package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
  15. package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
  16. package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
  17. package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
  18. package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
  19. package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
  20. package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
  21. package/dist/components/questions/_shared/components/compact/index.js +4 -0
  22. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
  23. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
  24. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
  25. package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
  26. package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
  27. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
  28. package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
  29. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
  30. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
  31. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +16 -17
  32. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -4
  33. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
  34. package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
  35. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
  36. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
  37. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
  38. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -32
  39. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
  40. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +76 -11
  41. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
  42. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
  43. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
  44. package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
  45. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
  46. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +76 -7
  47. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
  48. package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
  49. package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
  50. package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
  51. package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
  52. package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
  53. package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
  54. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
  55. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +20 -8
  56. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
  57. package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
  58. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
  59. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
  60. package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
  61. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
  62. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
  63. package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
  64. package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
  65. package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
  66. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
  67. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
  68. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +188 -46
  69. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
  70. package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
  71. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
  72. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
  73. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
  74. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
  75. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
  76. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
  77. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
  78. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
  79. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
  80. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
  81. package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
  82. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
  83. package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
  84. package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +13 -13
  85. package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +7 -6
  86. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
  87. package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
  88. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
  89. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
  90. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
  91. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  92. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
  93. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
  94. package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
  95. package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
  96. package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
  97. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
  98. package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
  99. package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
  100. package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
  101. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
  102. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +39 -19
  103. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
  104. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +20 -19
  105. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
  106. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
  107. package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
  108. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
  109. package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
  110. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
  111. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
  112. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
  113. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
  114. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
  115. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
  116. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +12 -2
  117. package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
  118. package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
  119. package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
  120. package/dist/components/questions/viewer/QuestionViewer.js +23 -10
  121. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
  122. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
  123. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
  124. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
  125. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
  126. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
  127. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
  128. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
  129. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
  130. package/dist/components/results/renderers/review-question-body-movers.js +1 -1
  131. package/dist/components/results/review-data.js +8 -1
  132. package/dist/shared/lib/utils/question-reverse-transform.js +39 -5
  133. package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
  134. package/dist/shared/types/questions/answer-the-question-group.js +5 -0
  135. package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
  136. package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
  137. package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
  138. package/package.json +1 -1
@@ -1,9 +1,10 @@
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';
3
+ import { BookOpen, Lightbulb, Volume2 } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
6
  import { toMatchWordToPictureImageUrls } from '../../_shared/types/match-word-to-picture-group.type';
7
+ import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, } from '../../_shared/components/compact';
7
8
  function normalizeWord(value) {
8
9
  return value.trim().toLowerCase();
9
10
  }
@@ -69,11 +70,11 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
69
70
  usedWords.add(normalizeWord(value));
70
71
  }
71
72
  });
72
- return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-indigo-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-indigo-800", children: questionData.instruction }))] })), questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null, toMatchWordToPictureImageUrls(questionData.imageUrl).length > 0 ? (_jsx("div", { className: "flex flex-wrap justify-center gap-3 rounded-lg border border-slate-100 bg-slate-50 p-3", children: toMatchWordToPictureImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh bài (${imageIndex + 1})`, large: true }, `mwtp-cover-${imageIndex}`))) })) : null, questionData.passage ? (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })) : null, wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-violet-200 bg-gradient-to-r from-violet-50 to-indigo-50 p-4", children: [_jsx("p", { className: "mb-3 text-sm font-semibold text-violet-700", children: "Word bank:" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
73
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null, toMatchWordToPictureImageUrls(questionData.imageUrl).length > 0 ? (_jsx("div", { className: "flex flex-wrap justify-center gap-3 rounded-lg border border-slate-100 bg-slate-50 p-3", children: toMatchWordToPictureImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh bài (${imageIndex + 1})`, large: true }, `mwtp-cover-${imageIndex}`))) })) : null, questionData.passage ? (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })) : null, wordBank.length > 0 && (_jsxs("div", { className: "rounded-xl border border-blue-100 bg-blue-50/60 p-3", children: [_jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wider text-blue-700", children: "Word bank" }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => {
73
74
  const isUsed = usedWords.has(normalizeWord(word));
74
75
  return (_jsx("span", { className: cn('rounded-full border px-3 py-1 text-sm font-semibold', isUsed
75
- ? 'border-emerald-200 bg-emerald-50 text-emerald-700'
76
- : 'border-violet-200 bg-white text-slate-700'), children: word }, `${word}-${index}`));
76
+ ? 'border-blue-500 bg-blue-50 text-blue-800 ring-2 ring-blue-100'
77
+ : 'border-gray-200 bg-white text-slate-700'), children: word }, `${word}-${index}`));
77
78
  }) })] })), items.map((item, itemIndex) => {
78
79
  const selectedWord = item.isExample
79
80
  ? item.correctAnswer
@@ -81,14 +82,13 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
81
82
  const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
82
83
  const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
83
84
  Boolean(item.correctAnswer);
84
- 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-violet-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: "flex flex-col space-y-3 p-3 sm:flex-row sm:items-center sm:space-y-0 sm:gap-3", children: [toMatchWordToPictureImageUrls(item.imageUrl).length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: toMatchWordToPictureImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsxs("p", { className: "text-sm font-semibold text-gray-800", children: [item.questionNumber || itemIndex + 1, ". Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p"] }), _jsxs("div", { className: "flex items-center gap-3", children: [item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("select", { value: selectedWord, disabled: !canSelect, onChange: (event) => handleSelect(itemIndex, event.target.value), className: cn('h-10 max-w-xs rounded-md border-2 bg-white px-3 text-sm font-medium', isReviewMode && isCorrect
85
- ? 'border-green-400 bg-green-50 text-green-700'
86
- : isReviewMode && selectedWord && !isCorrect
87
- ? 'border-red-400 bg-red-50 text-red-700'
88
- : 'border-violet-300 focus:border-violet-500 focus:ring-violet-200', !canSelect && 'cursor-default opacity-80'), children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn t\u1EEB --" }), wordBank.map((word, wordIndex) => {
89
- const isUsedByOther = usedWords.has(normalizeWord(word)) &&
90
- normalizeWord(word) !== normalizeWord(selectedWord);
91
- return (_jsx("option", { value: word, disabled: isUsedByOther, children: word }, `${word}-${wordIndex}`));
92
- })] })), isReviewMode && !item.isExample && (_jsx("span", { className: "flex items-center gap-1", children: isCorrect ? (_jsx(Check, { className: "h-5 w-5 text-green-500" })) : (_jsx(X, { className: "h-5 w-5 text-red-500" })) }))] }), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] })] }, `${item.questionNumber}-${itemIndex}`));
85
+ return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [toMatchWordToPictureImageUrls(item.imageUrl).length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: toMatchWordToPictureImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (wordBank.map((word, wordIndex) => {
86
+ const isSelected = normalizeWord(word) === normalizeWord(selectedWord);
87
+ const isUsedByOther = usedWords.has(normalizeWord(word)) && !isSelected;
88
+ const isWordCorrect = normalizeWord(word) === normalizeWord(item.correctAnswer);
89
+ return (_jsx(KidChoiceRow, { letter: optionLetter(wordIndex), selected: isSelected, isReviewMode: isReviewMode, isCorrect: isReviewMode && isWordCorrect, disabled: !canSelect || isUsedByOther, onClick: canSelect && !isUsedByOther
90
+ ? () => handleSelect(itemIndex, isSelected ? '' : word)
91
+ : undefined, children: word }, `${word}-${wordIndex}`));
92
+ })), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] }) }, `${item.questionNumber}-${itemIndex}`));
93
93
  }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
94
94
  }
@@ -4,6 +4,7 @@ import { BookOpen, Lightbulb } from 'lucide-react';
4
4
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
5
  import { ResolvedImage } from '../../../../components/common/ResolvedImage';
6
6
  import { MatchingWithLinesBoard } from './MatchingWithLinesBoard';
7
+ import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
7
8
  function ClientAudio({ url }) {
8
9
  const { previewUrl, isLoading } = usePresignedFileUrl(url);
9
10
  const src = previewUrl ||
@@ -32,10 +33,10 @@ export function MatchingWithLinesGroupClient({ questionData, isReviewMode = fals
32
33
  }
33
34
  });
34
35
  };
35
- return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-indigo-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-indigo-800", children: questionData.instruction }))] })), questionData.passage ? (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })) : null, questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null, questionData.imageUrl ? (_jsx("div", { className: "flex justify-center rounded-lg border border-slate-100 bg-slate-50 p-3", children: _jsx(ResolvedImage, { src: questionData.imageUrl, alt: "Match passage image", className: "max-h-44 w-auto rounded-md object-contain" }) })) : null, _jsx(MatchingWithLinesBoard, { leftItems: items.map((item) => ({
36
- imageUrl: item.imageUrl,
37
- text: item.text,
38
- isExample: item.isExample,
39
- questionNumber: item.questionNumber,
40
- })), rightItems: rightItems, values: values, correctValues: isReviewMode ? correctValues : undefined, onChange: handleChange, isReviewMode: isReviewMode, disabled: isReviewMode || !canEdit }), 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] }))] }));
36
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), questionData.passage ? (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })) : null, questionData.audioUrl ? _jsx(ClientAudio, { url: questionData.audioUrl }) : null, questionData.imageUrl ? (_jsx("div", { className: "flex justify-center rounded-lg border border-slate-100 bg-slate-50 p-3", children: _jsx(ResolvedImage, { src: questionData.imageUrl, alt: "Match passage image", className: "max-h-44 w-auto rounded-md object-contain" }) })) : null, _jsx(KidQuestionCard, { title: "N\u1ED1i b\u1EB1ng \u0111\u01B0\u1EDDng k\u1EBB", children: _jsx(MatchingWithLinesBoard, { leftItems: items.map((item) => ({
37
+ imageUrl: item.imageUrl,
38
+ text: item.text,
39
+ isExample: item.isExample,
40
+ questionNumber: item.questionNumber,
41
+ })), rightItems: rightItems, values: values, correctValues: isReviewMode ? correctValues : undefined, onChange: handleChange, isReviewMode: isReviewMode, disabled: isReviewMode || !canEdit }) }), 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] }))] }));
41
42
  }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useRef } from 'react';
4
- import { cn } from '../../../../shared/lib/utils';
5
- import { Check, X, BookOpen, FileText, ClipboardList, Lightbulb } from 'lucide-react';
4
+ import { Check, BookOpen, Lightbulb } from 'lucide-react';
5
+ import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
6
6
  export function ReadPassageAndAnswerQuestionsClient({ questionData, userAnswer, isReviewMode = false, onSubmit, autoFillCorrectAnswers = false, explanation: explanationProp, }) {
7
7
  const { passage, passageTitle, instructions, singleQuestion, explanation: dataExplanation } = questionData;
8
8
  const explanation = explanationProp || dataExplanation;
@@ -38,28 +38,10 @@ export function ReadPassageAndAnswerQuestionsClient({ questionData, userAnswer,
38
38
  const isCorrect = isReviewMode && selectedAnswer === singleQuestion.correctAnswer;
39
39
  const isWrong = isReviewMode && selectedAnswer >= 0 && selectedAnswer !== singleQuestion.correctAnswer;
40
40
  const hasAnswered = selectedAnswer >= 0;
41
- return (_jsxs("div", { className: "space-y-3", children: [instructions && (_jsx("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 shadow-sm", children: _jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white shadow-sm", children: _jsx(FileText, { className: "h-4 w-4" }) }), _jsx("p", { className: "text-sm font-medium text-blue-900", children: instructions })] }) })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 text-white shadow-sm", children: _jsx(BookOpen, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "\u0110o\u1EA1n v\u0103n \u0111\u1ECDc hi\u1EC3u" })] }), _jsxs("div", { className: "p-3 space-y-3", children: [passageTitle && (_jsx("h3", { className: "text-center text-lg font-bold text-gray-900 border-b border-gray-100 pb-3", children: passageTitle })), _jsx("div", { className: "rounded-lg bg-gradient-to-br from-slate-50 to-gray-50 p-5 border border-gray-100", children: _jsx("p", { className: "whitespace-pre-wrap text-base leading-relaxed text-gray-700", children: passage }) })] })] }), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:shadow-md", children: [_jsxs("div", { className: cn('flex items-center justify-between border-b px-3 py-2', isReviewMode && isCorrect && 'border-green-200 bg-gradient-to-r from-green-50 to-emerald-50', isReviewMode && isWrong && 'border-red-200 bg-gradient-to-r from-red-50 to-rose-50', !isReviewMode && 'border-gray-100 bg-gradient-to-r from-gray-50 to-white'), children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('flex h-8 w-8 items-center justify-center rounded-lg text-sm font-bold text-white shadow-sm', isReviewMode && isCorrect && 'bg-gradient-to-br from-green-500 to-emerald-600', isReviewMode && isWrong && 'bg-gradient-to-br from-red-500 to-rose-600', !isReviewMode && 'bg-gradient-to-br from-indigo-500 to-purple-600'), children: _jsx(ClipboardList, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-800", children: singleQuestion.question })] }), isReviewMode && hasAnswered && (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold', isCorrect ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'), children: [isCorrect ? _jsx(Check, { className: "h-3.5 w-3.5" }) : _jsx(X, { className: "h-3.5 w-3.5" }), isCorrect ? 'Đúng' : 'Sai'] }))] }), _jsxs("div", { className: "p-3 space-y-3", children: [singleQuestion.options.map((option, optIndex) => {
42
- const isSelected = selectedAnswer === optIndex;
43
- const isCorrectOption = optIndex === singleQuestion.correctAnswer;
44
- const letter = String.fromCharCode(65 + optIndex);
45
- const optText = typeof option === 'string' ? option : option?.text ?? option?.imageUrl ?? option?.content ?? '';
46
- return (_jsxs("button", { type: "button", onClick: () => handleAnswerChange(optIndex), disabled: isReviewMode, className: cn('group flex w-full items-center gap-3 rounded-lg border-2 p-3.5 text-left transition-all', isReviewMode
47
- ? isCorrectOption
48
- ? 'border-green-400 bg-green-50'
49
- : isSelected
50
- ? 'border-red-400 bg-red-50'
51
- : 'border-gray-200 bg-gray-50'
52
- : isSelected
53
- ? 'border-indigo-500 bg-indigo-50 shadow-sm ring-2 ring-indigo-100'
54
- : 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm'), children: [_jsx("div", { className: cn('flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg text-sm font-bold transition-all', isReviewMode
55
- ? isCorrectOption
56
- ? 'bg-green-500 text-white'
57
- : isSelected
58
- ? 'bg-red-500 text-white'
59
- : 'bg-gray-200 text-gray-600'
60
- : isSelected
61
- ? 'bg-gradient-to-br from-indigo-500 to-purple-600 text-white shadow-sm'
62
- : 'bg-gray-100 text-gray-600 group-hover:bg-gray-200'), children: letter }), _jsx("span", { className: cn('flex-1 text-sm font-medium', isReviewMode && isCorrectOption ? 'text-green-800' :
63
- isReviewMode && isSelected && !isCorrectOption ? 'text-red-800' : 'text-gray-700'), children: optText }), isReviewMode && (isCorrectOption || isSelected) && (_jsx("div", { className: cn('flex h-6 w-6 items-center justify-center rounded-full', isCorrectOption ? 'bg-green-500' : 'bg-red-500'), children: isCorrectOption ? _jsx(Check, { className: "h-4 w-4 text-white" }) : _jsx(X, { className: "h-4 w-4 text-white" }) }))] }, optIndex));
64
- }), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2 mt-3", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng ch\u1ECDn m\u1ED9t \u0111\u00E1p \u00E1n" })] })), isReviewMode && isWrong && (_jsx("div", { className: "rounded-lg bg-gradient-to-r from-green-50 to-emerald-50 p-4 border border-green-200 mt-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Check, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsxs("strong", { children: [String.fromCharCode(65 + singleQuestion.correctAnswer), ". ", typeof singleQuestion.options[singleQuestion.correctAnswer] === 'string' ? singleQuestion.options[singleQuestion.correctAnswer] : singleQuestion.options[singleQuestion.correctAnswer]?.text ?? ''] })] })] }) }))] })] }), isReviewMode && explanation && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-blue-100 bg-white/50 px-4 py-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm", children: _jsx(Lightbulb, { className: "h-4 w-4" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("div", { className: "p-4", children: _jsx("p", { className: "text-sm text-gray-700 leading-relaxed", children: explanation }) })] }))] }));
41
+ return (_jsxs("div", { className: "space-y-3", children: [_jsx(KidInstruction, { children: instructions }), _jsxs(KidQuestionCard, { icon: _jsx(BookOpen, { className: "h-3.5 w-3.5" }), title: "\u0110o\u1EA1n v\u0103n \u0111\u1ECDc hi\u1EC3u", children: [passageTitle && (_jsx("h3", { className: "text-center text-sm font-bold text-gray-900", children: passageTitle })), _jsx("p", { className: "whitespace-pre-wrap text-sm leading-relaxed text-gray-700", children: passage })] }), _jsxs(KidQuestionCard, { title: "Ch\u1ECDn \u0111\u00E1p \u00E1n \u0111\u00FAng", review: isReviewMode && hasAnswered ? (isCorrect ? 'correct' : 'wrong') : null, children: [_jsx(KidPrompt, { children: singleQuestion.question }), _jsx("div", { className: "space-y-1.5", children: singleQuestion.options.map((option, optIndex) => {
42
+ const isSelected = selectedAnswer === optIndex;
43
+ const isCorrectOption = optIndex === singleQuestion.correctAnswer;
44
+ const optText = typeof option === 'string' ? option : option?.text ?? option?.imageUrl ?? option?.content ?? '';
45
+ return (_jsx(KidChoiceRow, { letter: optionLetter(optIndex), selected: isSelected, isReviewMode: isReviewMode, isCorrect: isReviewMode && isCorrectOption, onClick: () => handleAnswerChange(optIndex), children: optText }, optIndex));
46
+ }) }), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2 mt-3", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng ch\u1ECDn m\u1ED9t \u0111\u00E1p \u00E1n" })] })), isReviewMode && isWrong && (_jsx("div", { className: "rounded-lg bg-gradient-to-r from-green-50 to-emerald-50 p-4 border border-green-200 mt-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Check, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsxs("strong", { children: [String.fromCharCode(65 + singleQuestion.correctAnswer), ". ", typeof singleQuestion.options[singleQuestion.correctAnswer] === 'string' ? singleQuestion.options[singleQuestion.correctAnswer] : singleQuestion.options[singleQuestion.correctAnswer]?.text ?? ''] })] })] }) }))] }), isReviewMode && explanation && (_jsxs("div", { className: "overflow-hidden rounded-xl border border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-blue-100 bg-white/50 px-4 py-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm", children: _jsx(Lightbulb, { className: "h-4 w-4" }) }), _jsx("h4", { className: "text-sm font-semibold text-gray-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("div", { className: "p-4", children: _jsx("p", { className: "text-sm text-gray-700 leading-relaxed", children: explanation }) })] }))] }));
65
47
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Lightbulb } from 'lucide-react';
4
4
  import { SortWordsIntoCategoriesBoard } from './SortWordsIntoCategoriesBoard';
5
5
  import { buildPlacements, padPlacements } from './map-sort-words-into-categories-data';
6
+ import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
6
7
  export function SortWordsIntoCategoriesClient({ questionData, isReviewMode = false, userPlacements, onPlacementsChange, }) {
7
8
  const correctPlacements = buildPlacements(questionData.words);
8
9
  const exampleLocked = questionData.words.map((word) => ({
@@ -20,5 +21,5 @@ export function SortWordsIntoCategoriesClient({ questionData, isReviewMode = fal
20
21
  return item;
21
22
  });
22
23
  const canEdit = Boolean(onPlacementsChange) && !isReviewMode;
23
- return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50 px-3 py-2", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-slate-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-slate-800", children: questionData.instruction }))] })), _jsx(SortWordsIntoCategoriesBoard, { categories: questionData.categories, words: questionData.words, placements: placements, correctPlacements: isReviewMode ? correctPlacements : undefined, onChange: canEdit ? onPlacementsChange : undefined, disabled: !canEdit, isReviewMode: isReviewMode }), 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] }))] }));
24
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.title ? _jsx(KidPrompt, { children: questionData.title }) : null, _jsx(KidInstruction, { children: questionData.instruction }), _jsx(KidQuestionCard, { title: "S\u1EAFp x\u1EBFp t\u1EEB v\u00E0o nh\u00F3m", children: _jsx(SortWordsIntoCategoriesBoard, { categories: questionData.categories, words: questionData.words, placements: placements, correctPlacements: isReviewMode ? correctPlacements : undefined, onChange: canEdit ? onPlacementsChange : undefined, disabled: !canEdit, isReviewMode: isReviewMode }) }), 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] }))] }));
24
25
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Mic, Volume2, CheckCircle2, Play, Pause, MessageCircle, Users } from 'lucide-react';
4
4
  import { Button } from '../../../../components/ui/button';
5
5
  import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
6
+ import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
6
7
  // ─── Sub-component: Audio Player ─────────────────────────────────────────────
7
8
  function ConversationAudioPlayer({ audioUrl, label }) {
8
9
  const { previewUrl } = usePresignedFileUrl(audioUrl);
@@ -33,9 +34,9 @@ export function SpeakingConversationPreviewClient({ questionData, isReviewMode =
33
34
  : questionData.images.length === 4
34
35
  ? 'grid-cols-4'
35
36
  : 'grid-cols-3';
36
- return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _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-gray-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gray-100 text-gray-600", children: _jsx(MessageCircle, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "H\u1ED9i tho\u1EA1i Speaking (KFS)" })] }), _jsxs("div", { className: `inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ${isPartnerQuestion
37
- ? 'bg-orange-100 text-orange-700 border border-orange-200'
38
- : 'bg-green-100 text-green-700 border border-green-200'}`, children: [_jsx(Mic, { className: "h-3 w-3" }), isPartnerQuestion ? 'Bạn thi (ví dụ)' : 'Học sinh trả lời'] })] }), _jsxs("div", { className: "p-3 space-y-3", children: [(questionData.partnerName || questionData.partnerAvatarUrl) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-orange-50 border border-orange-100 px-4 py-2.5", children: [_jsx("span", { className: "text-xs font-medium text-orange-600", children: "Nh\u00E2n v\u1EADt b\u1EA1n thi:" }), _jsx(PartnerAvatar, { avatarUrl: questionData.partnerAvatarUrl, name: questionData.partnerName })] })), questionData.questionText && (_jsxs("div", { className: "rounded-lg bg-blue-50 border border-blue-200 px-4 py-3", children: [_jsx("p", { className: "text-xs font-medium text-blue-600 mb-1", children: "N\u1ED9i dung c\u00E2u h\u1ECFi:" }), _jsx("p", { className: "text-sm font-semibold text-gray-800", children: questionData.questionText })] })), _jsxs("div", { className: "space-y-3", children: [questionData.interlocutorAudioUrl && (_jsx(ConversationAudioPlayer, { audioUrl: questionData.interlocutorAudioUrl, label: "Audio ng\u01B0\u1EDDi h\u1ECFi (Interlocutor)" })), questionData.partnerAudioUrl && (_jsx(ConversationAudioPlayer, { audioUrl: questionData.partnerAudioUrl, label: "Audio b\u1EA1n thi (Partner)" }))] }), hasImages && (_jsxs("div", { children: [_jsx("p", { className: "text-xs font-medium text-gray-500 mb-2", children: "H\u00ECnh \u1EA3nh th\u1EA3o lu\u1EADn:" }), _jsx("div", { className: `grid ${gridCols} gap-3`, style: { maxWidth: '500px' }, children: questionData.images.map((img) => (_jsx(PreviewImageItem, { url: img.url, label: img.label }, img.id))) })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: isPartnerQuestion
39
- ? 'Câu hỏi ví dụ — Bạn thi sẽ trả lời, học sinh lắng nghe'
40
- : 'Học sinh sẽ nhấn nút ghi âm và nói câu trả lời tại đây' })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] }))] })] })] }));
37
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "H\u1ED9i tho\u1EA1i Speaking (KFS)", icon: _jsx(MessageCircle, { className: "h-3.5 w-3.5" }), headerExtra: _jsxs("div", { className: `inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ${isPartnerQuestion
38
+ ? 'bg-orange-100 text-orange-700 border border-orange-200'
39
+ : 'bg-green-100 text-green-700 border border-green-200'}`, children: [_jsx(Mic, { className: "h-3 w-3" }), isPartnerQuestion ? 'Bạn thi (ví dụ)' : 'Học sinh trả lời'] }), children: [(questionData.partnerName || questionData.partnerAvatarUrl) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-orange-50 border border-orange-100 px-4 py-2.5", children: [_jsx("span", { className: "text-xs font-medium text-orange-600", children: "Nh\u00E2n v\u1EADt b\u1EA1n thi:" }), _jsx(PartnerAvatar, { avatarUrl: questionData.partnerAvatarUrl, name: questionData.partnerName })] })), questionData.questionText && (_jsx(KidPrompt, { children: questionData.questionText })), _jsxs("div", { className: "space-y-3", children: [questionData.interlocutorAudioUrl && (_jsx(ConversationAudioPlayer, { audioUrl: questionData.interlocutorAudioUrl, label: "Audio ng\u01B0\u1EDDi h\u1ECFi (Interlocutor)" })), questionData.partnerAudioUrl && (_jsx(ConversationAudioPlayer, { audioUrl: questionData.partnerAudioUrl, label: "Audio b\u1EA1n thi (Partner)" }))] }), hasImages && (_jsxs("div", { children: [_jsx("p", { className: "text-xs font-medium text-gray-500 mb-2", children: "H\u00ECnh \u1EA3nh th\u1EA3o lu\u1EADn:" }), _jsx("div", { className: `grid ${gridCols} gap-3`, style: { maxWidth: '500px' }, children: questionData.images.map((img) => (_jsx(PreviewImageItem, { url: img.url, label: img.label }, img.id))) })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: isPartnerQuestion
40
+ ? 'Câu hỏi ví dụ — Bạn thi sẽ trả lời, học sinh lắng nghe'
41
+ : 'Học sinh sẽ nhấn nút ghi âm và nói câu trả lời tại đây' })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] }))] })] }));
41
42
  }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Mic, CheckCircle2, Info, CreditCard } from 'lucide-react';
4
4
  import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
5
+ import { KidQuestionCard } from '../../_shared/components/compact';
5
6
  function asRecord(value) {
6
7
  return value && typeof value === 'object' && !Array.isArray(value)
7
8
  ? value
@@ -23,5 +24,5 @@ export function mapQuestionToSpeakingCueCardData(question) {
23
24
  }
24
25
  export function SpeakingCueCardClient({ questionData, isReviewMode = false, }) {
25
26
  const answers = questionData.expectedAnswers || [];
26
- return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gray-50 px-3 py-2", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gray-100 text-gray-600", children: _jsx(CreditCard, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)" })] }) }), _jsxs("div", { className: "p-3 space-y-3", children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-purple-50 ring-2 ring-purple-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-purple-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh \u0111\u1ECDc th\u1EBB g\u1EE3i \u00FD, chu\u1EA9n b\u1ECB, r\u1ED3i tr\u1EA3 l\u1EDDi b\u1EB1ng gi\u1ECDng n\u00F3i" })] }), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] })] }));
27
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Th\u1EBB g\u1EE3i \u00FD (Cue Card)", icon: _jsx(CreditCard, { className: "h-3.5 w-3.5" }), children: [_jsx("div", { className: "rounded-xl border border-purple-100 bg-purple-50/40 px-5 py-4", children: _jsx(RichTextHtml, { html: questionData.cueCardHtml, className: "text-sm text-slate-800", fallback: _jsx("p", { className: "text-sm text-gray-400", children: "Ch\u01B0a c\u00F3 n\u1ED9i dung cue card" }) }) }), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-purple-50 ring-2 ring-purple-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-purple-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh \u0111\u1ECDc th\u1EBB g\u1EE3i \u00FD, chu\u1EA9n b\u1ECB, r\u1ED3i tr\u1EA3 l\u1EDDi b\u1EB1ng gi\u1ECDng n\u00F3i" })] }), isReviewMode && answers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n m\u1EABu" })] }), _jsx("div", { className: "space-y-2", children: answers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, `${answer}-${index}`))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] }));
27
28
  }
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Mic, CheckCircle2, Info, ImageIcon, FileQuestion } from 'lucide-react';
3
+ import { Mic, CheckCircle2, Info, ImageIcon } from 'lucide-react';
4
4
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
5
+ import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
5
6
  export function SpeakingDescribeImageClient({ questionData, isReviewMode = false, }) {
6
7
  const { previewUrl: imageUrl } = usePresignedFileUrl(questionData.imageUrl || '');
7
- return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gray-50 px-3 py-2", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gray-100 text-gray-600", children: _jsx(Mic, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm font-medium text-gray-700", children: "M\u00F4 t\u1EA3 h\u00ECnh \u1EA3nh (Speaking)" })] }) }), _jsxs("div", { className: "p-3 space-y-3", children: [questionData.question && (_jsxs("div", { className: "flex gap-3", children: [_jsx(FileQuestion, { className: "h-5 w-5 text-indigo-500 shrink-0 mt-0.5" }), _jsx("div", { className: "text-base font-semibold text-gray-800", children: questionData.question })] })), imageUrl ? (_jsx("div", { className: "w-full max-w-3xl mx-auto rounded-xl overflow-hidden shadow-sm border border-gray-200 bg-gray-50 flex justify-center", children: _jsx("img", { src: imageUrl, alt: "Scene", className: "w-[80%] max-h-[400px] object-contain flex-shrink-0" }) })) : (_jsxs("div", { className: "flex flex-col items-center justify-center py-10 rounded-xl border border-dashed border-gray-300 bg-gray-50", children: [_jsx(ImageIcon, { className: "h-8 w-8 text-gray-400 mb-2" }), _jsx("span", { className: "text-sm text-gray-500", children: "Ch\u01B0a t\u1EA3i h\u00ECnh \u1EA3nh m\u00F4 t\u1EA3" })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: isReviewMode && questionData.audioUrl
8
- ? "Audio ghi âm của học sinh (đã lưu)"
9
- : "Học sinh sẽ nhìn hình ảnh và nhấn nút micro để mô tả" })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4 mt-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n t\u1EF1 \u0111\u1ED9ng (Tham chi\u1EBFu AI)" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4 mt-2", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch gi\u00E1o vi\u00EAn" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] })] }));
8
+ return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "M\u00F4 t\u1EA3 h\u00ECnh \u1EA3nh (Speaking)", icon: _jsx(Mic, { className: "h-3.5 w-3.5" }), children: [questionData.question && (_jsx(KidPrompt, { children: questionData.question })), imageUrl ? (_jsx("div", { className: "w-full max-w-3xl mx-auto rounded-xl overflow-hidden shadow-sm border border-gray-200 bg-gray-50 flex justify-center", children: _jsx("img", { src: imageUrl, alt: "Scene", className: "w-[80%] max-h-[400px] object-contain flex-shrink-0" }) })) : (_jsxs("div", { className: "flex flex-col items-center justify-center py-10 rounded-xl border border-dashed border-gray-300 bg-gray-50", children: [_jsx(ImageIcon, { className: "h-8 w-8 text-gray-400 mb-2" }), _jsx("span", { className: "text-sm text-gray-500", children: "Ch\u01B0a t\u1EA3i h\u00ECnh \u1EA3nh m\u00F4 t\u1EA3" })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: isReviewMode && questionData.audioUrl
9
+ ? "Audio ghi âm của học sinh (đã lưu)"
10
+ : "Học sinh sẽ nhìn hình ảnh và nhấn nút micro để mô tả" })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4 mt-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n t\u1EF1 \u0111\u1ED9ng (Tham chi\u1EBFu AI)" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50 p-4 mt-2", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-amber-600" }), _jsx("p", { className: "text-sm font-semibold text-amber-800", children: "Gi\u1EA3i th\u00EDch gi\u00E1o vi\u00EAn" })] }), _jsx("p", { className: "text-sm text-amber-700 leading-relaxed whitespace-pre-wrap", children: questionData.explanation })] }))] })] }));
10
11
  }
@@ -1,2 +1,2 @@
1
1
  import type { SpontaneousQaGroupClientProps } from '../../_shared/types/spontaneous-qa-group.type';
2
- export declare function SpontaneousQaGroupClient({ questionData, isReviewMode, }: SpontaneousQaGroupClientProps): import("react").JSX.Element;
2
+ export declare function SpontaneousQaGroupClient({ questionData, isReviewMode, userAnswer, onItemRecorded, uploadingIndex, gradeDetails, disabled, }: SpontaneousQaGroupClientProps): import("react").JSX.Element;
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from 'react';
4
- import { CircleHelp, Lightbulb, Mic, Square, Volume2 } from 'lucide-react';
4
+ import { Lightbulb, Mic, Square, Volume2 } from 'lucide-react';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
+ import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
6
7
  function QuestionAudio({ url }) {
7
8
  const { previewUrl, isLoading } = usePresignedFileUrl(url);
8
9
  const src = previewUrl ||
@@ -12,7 +13,28 @@ function QuestionAudio({ url }) {
12
13
  }
13
14
  return (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-100 bg-blue-50/60 px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 shrink-0 text-blue-500" }), isLoading ? (_jsx("div", { className: "h-8 flex-1 animate-pulse rounded-md bg-blue-100" })) : (_jsx("audio", { controls: true, src: src, className: "h-8 w-full", preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
14
15
  }
15
- function SpeakingReplyMic() {
16
+ function readGradeUser(detail) {
17
+ if (!detail?.userAnswer || typeof detail.userAnswer !== 'object' || Array.isArray(detail.userAnswer)) {
18
+ return { feedback: detail?.feedback };
19
+ }
20
+ const raw = detail.userAnswer;
21
+ return {
22
+ transcript: typeof raw.transcript === 'string' ? raw.transcript : undefined,
23
+ contentScore: typeof raw.contentScore === 'number' ? raw.contentScore : undefined,
24
+ pronunciationScore: typeof raw.pronunciationScore === 'number' ? raw.pronunciationScore : undefined,
25
+ fluencyScore: typeof raw.fluencyScore === 'number' ? raw.fluencyScore : undefined,
26
+ feedback: typeof raw.feedback === 'string' ? raw.feedback : detail.feedback,
27
+ };
28
+ }
29
+ function findGradeDetail(details, flattenIndex) {
30
+ if (!details?.length) {
31
+ return undefined;
32
+ }
33
+ return details.find((item) => (item.itemId === String(flattenIndex) ||
34
+ item.itemId === `item-${flattenIndex}` ||
35
+ item.itemId === `item-${flattenIndex + 1}`));
36
+ }
37
+ function SpeakingReplyMic({ flattenIndex, audioKey, disabled, uploading, onRecorded, }) {
16
38
  const [status, setStatus] = useState('idle');
17
39
  const [error, setError] = useState('');
18
40
  const [blobUrl, setBlobUrl] = useState('');
@@ -33,6 +55,9 @@ function SpeakingReplyMic() {
33
55
  streamRef.current = null;
34
56
  };
35
57
  const startRecording = async () => {
58
+ if (disabled || uploading || !onRecorded) {
59
+ return;
60
+ }
36
61
  setError('');
37
62
  try {
38
63
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@@ -55,6 +80,7 @@ function SpeakingReplyMic() {
55
80
  });
56
81
  setStatus('recorded');
57
82
  stopStream();
83
+ onRecorded(flattenIndex, blob);
58
84
  };
59
85
  recorder.start();
60
86
  setStatus('recording');
@@ -69,17 +95,34 @@ function SpeakingReplyMic() {
69
95
  recorderRef.current.stop();
70
96
  }
71
97
  };
72
- return (_jsxs("div", { className: "flex flex-col items-center gap-2 rounded-lg border border-dashed border-indigo-200 bg-indigo-50/60 px-4 py-3", children: [_jsx("button", { type: "button", onClick: status === 'recording' ? stopRecording : startRecording, title: status === 'recording' ? 'Dừng ghi âm' : 'Nhấn micro để trả lời', className: `inline-flex h-12 w-12 items-center justify-center rounded-full text-white shadow-sm transition-colors ${status === 'recording'
98
+ const canRecord = Boolean(onRecorded) && !disabled && !uploading;
99
+ return (_jsxs("div", { className: "flex flex-col items-center gap-2 rounded-lg border border-dashed border-indigo-200 bg-indigo-50/60 px-4 py-3", children: [canRecord ? (_jsx("button", { type: "button", onClick: status === 'recording' ? stopRecording : startRecording, title: status === 'recording' ? 'Dừng ghi âm' : 'Nhấn micro để trả lời', className: `inline-flex h-12 w-12 items-center justify-center rounded-full text-white shadow-sm transition-colors ${status === 'recording'
73
100
  ? 'animate-pulse bg-red-500 hover:bg-red-600'
74
- : 'bg-indigo-600 hover:bg-indigo-700'}`, children: status === 'recording' ? _jsx(Square, { className: "h-4 w-4 fill-current" }) : _jsx(Mic, { className: "h-5 w-5" }) }), _jsx("p", { className: "text-center text-xs text-indigo-700", children: status === 'recording'
75
- ? 'Đang ghi âm... Nhấn để dừng'
76
- : status === 'recorded'
77
- ? 'Đã ghi. Nhấn micro để ghi lại'
78
- : 'Nhấn micro để trả lời bằng giọng nói' }), error && _jsx("p", { className: "text-center text-xs text-red-600", children: error }), status === 'recorded' && blobUrl && (_jsx("audio", { controls: true, src: blobUrl, className: "h-8 w-full max-w-sm", preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." }))] }));
101
+ : 'bg-indigo-600 hover:bg-indigo-700'}`, children: status === 'recording' ? _jsx(Square, { className: "h-4 w-4 fill-current" }) : _jsx(Mic, { className: "h-5 w-5" }) })) : (_jsx("div", { className: "inline-flex h-12 w-12 items-center justify-center rounded-full bg-slate-300 text-white", children: _jsx(Mic, { className: "h-5 w-5" }) })), _jsx("p", { className: "text-center text-xs text-indigo-700", children: uploading
102
+ ? 'Đang tải lên bản ghi...'
103
+ : status === 'recording'
104
+ ? 'Đang ghi âm... Nhấn để dừng'
105
+ : audioKey || status === 'recorded'
106
+ ? 'Đã ghi. Nhấn micro để ghi lại'
107
+ : 'Nhấn micro để trả lời bằng giọng nói' }), error && _jsx("p", { className: "text-center text-xs text-red-600", children: error }), audioKey ? (_jsx("div", { className: "w-full max-w-sm", children: _jsx(QuestionAudio, { url: audioKey }) })) : status === 'recorded' && blobUrl ? (_jsx("audio", { controls: true, src: blobUrl, className: "h-8 w-full max-w-sm", preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })) : null] }));
79
108
  }
80
- export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, }) {
109
+ export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, userAnswer, onItemRecorded, uploadingIndex = null, gradeDetails, disabled = false, }) {
81
110
  const parts = questionData.parts || [];
82
- 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 })), parts.map((part, partIndex) => (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "border-b border-gray-100 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [_jsx("p", { className: "text-sm font-semibold text-slate-800", children: part.partTitle || `PART ${part.partNumber || partIndex + 1}` }), part.topic && (_jsx("p", { className: "mt-1 text-sm text-slate-600", children: part.topic }))] }), _jsx("div", { className: "space-y-3 p-3", children: part.questions.map((question, questionIndex) => (_jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", 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-blue-100 text-blue-700", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", question.questionNumber || questionIndex + 1] })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [question.points || 0, " \u0111i\u1EC3m"] })] }), _jsx("p", { className: "text-sm font-semibold text-gray-800", children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx("div", { className: "mt-2", children: _jsx(QuestionAudio, { url: question.audioUrl }) }) : null, _jsx("div", { className: "mt-3", children: _jsx(SpeakingReplyMic, {}) }), isReviewMode && question.expectedAnswers.filter((item) => item.trim() !== '').length > 0 && (_jsxs("div", { className: "mt-2 rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "mb-1 flex items-center gap-1 text-xs font-semibold text-amber-700", children: [_jsx(Lightbulb, { className: "h-3 w-3" }), "\u0110\u00E1p \u00E1n m\u1EABu"] }), _jsx("ul", { className: "list-disc space-y-1 pl-4 text-xs text-amber-800", children: question.expectedAnswers
111
+ const audioKeys = Array.isArray(userAnswer?.audioKeys) ? userAnswer.audioKeys : [];
112
+ let flattenIndex = 0;
113
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), parts.map((part, partIndex) => (_jsxs(KidQuestionCard, { icon: _jsx(Mic, { className: "h-3.5 w-3.5" }), title: part.partTitle || `PART ${part.partNumber || partIndex + 1}`, children: [part.topic ? _jsx(KidPrompt, { children: part.topic }) : null, part.questions.map((question, questionIndex) => {
114
+ const currentIndex = flattenIndex;
115
+ flattenIndex += 1;
116
+ const audioKey = audioKeys[currentIndex];
117
+ const detail = findGradeDetail(gradeDetails, currentIndex);
118
+ const grade = readGradeUser(detail);
119
+ const showMic = !isReviewMode && !question.isExample;
120
+ return (_jsxs(KidQuestionCard, { title: `Câu ${question.questionNumber || questionIndex + 1}`, points: detail?.partialScore != null ? undefined : (question.points || 0), isExample: question.isExample, headerExtra: detail?.partialScore != null ? (_jsxs("span", { className: "text-xs text-gray-500", children: [detail.partialScore, " / ", question.points || 0, " \u0111i\u1EC3m"] })) : undefined, children: [_jsx(KidPrompt, { children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, showMic && (_jsx(SpeakingReplyMic, { flattenIndex: currentIndex, audioKey: typeof audioKey === 'string' ? audioKey : null, disabled: disabled, uploading: uploadingIndex === currentIndex, onRecorded: onItemRecorded })), detail && !question.isExample && (_jsxs("div", { className: "space-y-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-700", children: [grade.transcript && (_jsxs("p", { children: [_jsx("span", { className: "font-semibold", children: "Transcript:" }), " ", grade.transcript] })), (grade.contentScore != null || grade.pronunciationScore != null || grade.fluencyScore != null) && (_jsx("p", { children: [
121
+ grade.contentScore != null ? `Content ${Math.round(grade.contentScore)}` : null,
122
+ grade.pronunciationScore != null ? `Pronunciation ${Math.round(grade.pronunciationScore)}` : null,
123
+ grade.fluencyScore != null ? `Fluency ${Math.round(grade.fluencyScore)}` : null,
124
+ ].filter(Boolean).join(' · ') })), grade.feedback && _jsx("p", { children: grade.feedback })] })), isReviewMode && question.expectedAnswers.filter((item) => item.trim() !== '').length > 0 && (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50 px-3 py-2", children: [_jsxs("div", { className: "mb-1 flex items-center gap-1 text-xs font-semibold text-amber-700", children: [_jsx(Lightbulb, { className: "h-3 w-3" }), "\u0110\u00E1p \u00E1n m\u1EABu"] }), _jsx("ul", { className: "list-disc space-y-1 pl-4 text-xs text-amber-800", children: question.expectedAnswers
83
125
  .filter((item) => item.trim() !== '')
84
- .map((item) => (_jsx("li", { children: item }, item))) })] }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`))) })] }, `part-${part.partNumber}-${partIndex}`)))] }));
126
+ .map((item) => (_jsx("li", { children: item }, item))) })] }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
127
+ })] }, `part-${part.partNumber}-${partIndex}`)))] }));
85
128
  }
@@ -6,11 +6,12 @@ import { Label } from '../../../../components/ui/label';
6
6
  import { Input } from '../../../../components/ui/input';
7
7
  import { Textarea } from '../../../../components/ui/textarea';
8
8
  import { PointsInput } from '../../../../components/ui/points-input';
9
+ import { Switch } from '../../../../components/ui/switch';
9
10
  import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
10
11
  import { FileUpload } from '../../../../components/ui/file-upload';
11
12
  import { useDebouncedCallback } from '../../../../shared/lib/hooks';
12
13
  import { toast } from 'sonner';
13
- import { Layers, Mic, Plus, Trash2, Type, Volume2 } from 'lucide-react';
14
+ import { Layers, Mic, Plus, Star, Trash2, Type, Volume2 } from 'lucide-react';
14
15
  import { SpontaneousQaGroupClient } from './SpontaneousQaGroupClient';
15
16
  import { createEmptyPart, createEmptyQuestion, normalizeParts, sumPartPoints, } from './creator-helpers';
16
17
  export function SpontaneousQaGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
@@ -172,10 +173,13 @@ export function SpontaneousQaGroupCreator({ initialData, onChange, externalError
172
173
  onClick: () => setIsClientMode(true),
173
174
  } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "group-instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "group-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: "In pairs, ask and answer the questions.", className: "h-10 border-gray-200 bg-white" })] }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1ED5ng \u0111i\u1EC3m \u0111\u01B0\u1EE3c c\u1ED9ng t\u1EEB \u0111i\u1EC3m t\u1EEBng c\u00E2u trong t\u1EA5t c\u1EA3 part." })] }), parts.map((part, partIndex) => (_jsxs(CompactFieldCard, { header: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "text-sm font-semibold text-gray-800", children: ["Part ", partIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removePart(partIndex), disabled: parts.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), children: [_jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Ti\u00EAu \u0111\u1EC1 part" }), _jsx(Input, { value: part.partTitle, onChange: (event) => updatePart(partIndex, { partTitle: event.target.value }), placeholder: "PART 1" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Ch\u1EE7 \u0111\u1EC1" }), _jsx(Input, { value: part.topic, onChange: (event) => updatePart(partIndex, { topic: event.target.value }), placeholder: "Talk about school." })] })] }), part.questions.map((question, questionIndex) => (_jsxs("div", { className: "space-y-2.5 rounded-md border border-slate-200 px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "text-sm font-semibold text-slate-700", children: ["C\u00E2u ", questionIndex + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeQuestion(partIndex, questionIndex), disabled: part.questions.length <= 1, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex w-fit rounded-lg border border-slate-200 bg-slate-100 p-1", children: [_jsxs("button", { type: "button", onClick: () => updateQuestion(partIndex, questionIndex, { inputType: 'AUDIO' }), className: `flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium ${question.inputType === 'AUDIO' ? 'bg-white text-blue-700 shadow-sm' : 'text-slate-500'}`, children: [_jsx(Mic, { className: "h-3.5 w-3.5" }), " Audio"] }), _jsxs("button", { type: "button", onClick: () => updateQuestion(partIndex, questionIndex, { inputType: 'TEXT' }), className: `flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium ${question.inputType === 'TEXT' ? 'bg-white text-emerald-700 shadow-sm' : 'text-slate-500'}`, children: [_jsx(Type, { className: "h-3.5 w-3.5" }), " Text (TTS)"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "C\u00E2u h\u1ECFi" }), _jsx(Textarea, { value: question.questionText, onChange: (event) => updateQuestion(partIndex, questionIndex, {
174
175
  questionText: event.target.value,
175
- }), rows: 2, placeholder: "Do you like your timetable at school? Why?/Why not?" })] }), question.inputType === 'AUDIO' ? (_jsx(FileUpload, { id: `audio-${partIndex}-${questionIndex}`, label: "File audio c\u00E2u h\u1ECFi", accept: "audio/*", value: question.audioUrl, onChange: (url) => updateQuestion(partIndex, questionIndex, { audioUrl: url }), maxSize: 20, placeholder: "T\u1EA3i l\u00EAn file audio (MP3, WAV...)", autoUpload: true, prefix: "speaking/spontaneous-qa-group" })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: !question.questionText.trim(), onClick: handleGenerateAudio, children: [_jsx(Volume2, { className: "mr-2 h-3.5 w-3.5" }), "T\u1EA1o Audio"] })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "\u0110\u00E1p \u00E1n m\u1EABu" }), question.expectedAnswers.map((answer, answerIndex) => (_jsx(Input, { value: answer, onChange: (event) => updateExpectedAnswer(partIndex, questionIndex, answerIndex, event.target.value), placeholder: "\u0110\u00E1p \u00E1n m\u1EABu cho AI ch\u1EA5m" }, `ans-${partIndex}-${questionIndex}-${answerIndex}`))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => addExpectedAnswer(partIndex, questionIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm \u0111\u00E1p \u00E1n m\u1EABu"] })] }), _jsx(PointsInput, { value: question.points, allowZero: true, onChange: (event) => {
176
- const parsed = Number.parseInt(event.target.value.replace(/[^0-9]/g, ''), 10);
177
- updateQuestion(partIndex, questionIndex, {
178
- points: Number.isNaN(parsed) ? 0 : Math.max(0, parsed),
179
- });
180
- } })] }, `q-${partIndex}-${questionIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: () => addQuestion(partIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm c\u00E2u h\u1ECFi"] })] }, `part-${partIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: addPart, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm part"] }), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) }), allErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: allErrors.map((error) => (_jsx("p", { children: error }, error))) }))] }));
176
+ }), rows: 2, placeholder: "Do you like your timetable at school? Why?/Why not?" })] }), question.inputType === 'AUDIO' ? (_jsx(FileUpload, { id: `audio-${partIndex}-${questionIndex}`, label: "File audio c\u00E2u h\u1ECFi", accept: "audio/*", value: question.audioUrl, onChange: (url) => updateQuestion(partIndex, questionIndex, { audioUrl: url }), maxSize: 20, placeholder: "T\u1EA3i l\u00EAn file audio (MP3, WAV...)", autoUpload: true, prefix: "speaking/spontaneous-qa-group" })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: !question.questionText.trim(), onClick: handleGenerateAudio, children: [_jsx(Volume2, { className: "mr-2 h-3.5 w-3.5" }), "T\u1EA1o Audio"] })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "\u0110\u00E1p \u00E1n m\u1EABu" }), question.expectedAnswers.map((answer, answerIndex) => (_jsx(Input, { value: answer, onChange: (event) => updateExpectedAnswer(partIndex, questionIndex, answerIndex, event.target.value), placeholder: "\u0110\u00E1p \u00E1n m\u1EABu cho AI ch\u1EA5m" }, `ans-${partIndex}-${questionIndex}-${answerIndex}`))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => addExpectedAnswer(partIndex, questionIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm \u0111\u00E1p \u00E1n m\u1EABu"] })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: question.isExample === true, onCheckedChange: (checked) => updateQuestion(partIndex, questionIndex, {
177
+ isExample: checked,
178
+ points: checked ? 0 : (question.points || 1),
179
+ }) }), _jsxs(Label, { className: "flex cursor-pointer items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "C\u00E2u v\u00ED d\u1EE5"] })] }), !question.isExample && (_jsx(PointsInput, { value: question.points, allowZero: true, onChange: (event) => {
180
+ const parsed = Number.parseInt(event.target.value.replace(/[^0-9]/g, ''), 10);
181
+ updateQuestion(partIndex, questionIndex, {
182
+ points: Number.isNaN(parsed) ? 0 : Math.max(0, parsed),
183
+ });
184
+ } }))] })] }, `q-${partIndex}-${questionIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: () => addQuestion(partIndex), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm c\u00E2u h\u1ECFi"] })] }, `part-${partIndex}`))), _jsxs(Button, { type: "button", variant: "outline", onClick: addPart, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Th\u00EAm part"] }), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) }), allErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: allErrors.map((error) => (_jsx("p", { children: error }, error))) }))] }));
181
185
  }
@@ -6,6 +6,7 @@ export function createEmptyQuestion(questionNumber) {
6
6
  expectedAnswers: [''],
7
7
  points: 1,
8
8
  questionNumber,
9
+ isExample: false,
9
10
  };
10
11
  }
11
12
  export function createEmptyPart(partNumber) {
@@ -32,12 +33,17 @@ export function normalizeParts(rawParts) {
32
33
  expectedAnswers: Array.isArray(question.expectedAnswers) && question.expectedAnswers.length > 0
33
34
  ? question.expectedAnswers
34
35
  : [''],
35
- points: typeof question.points === 'number' ? question.points : 1,
36
+ isExample: question.isExample === true,
37
+ points: question.isExample === true
38
+ ? 0
39
+ : typeof question.points === 'number' ? question.points : 1,
36
40
  questionNumber: question.questionNumber || questionIndex + 1,
37
41
  }))
38
42
  : [createEmptyQuestion(1)],
39
43
  }));
40
44
  }
41
45
  export function sumPartPoints(parts) {
42
- return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => (partTotal + (typeof question.points === 'number' ? question.points : 1)), 0)), 0);
46
+ return parts.reduce((total, part) => (total + part.questions.reduce((partTotal, question) => (partTotal + (question.isExample
47
+ ? 0
48
+ : typeof question.points === 'number' ? question.points : 1)), 0)), 0);
43
49
  }
@@ -10,6 +10,7 @@ export interface SpontaneousQaGroupFlatChild {
10
10
  inputType: 'TEXT' | 'AUDIO';
11
11
  expectedAnswers: string[];
12
12
  points: number;
13
+ isExample: boolean;
13
14
  }
14
15
  export declare function flattenSpontaneousQaGroupContent(content: unknown): SpontaneousQaGroupFlatChild[];
15
16
  /** Speaking Bank / translate lists — no teacher video or one-at-a-time mic. */
@@ -42,7 +42,8 @@ export function flattenSpontaneousQaGroupContent(content) {
42
42
  audioUrl: asString(childContent.audioUrl),
43
43
  inputType: asString(childContent.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
44
44
  expectedAnswers: normalizeExpectedAnswers(asRecord(child.correctAnswer).expectedAnswers),
45
- points: asNumber(child.points, 1),
45
+ isExample: child.isExample === true,
46
+ points: child.isExample === true ? 0 : asNumber(child.points, 1),
46
47
  });
47
48
  });
48
49
  });
@@ -62,7 +63,8 @@ export function flattenSpontaneousQaGroupContent(content) {
62
63
  audioUrl: asString(childContent.audioUrl),
63
64
  inputType: asString(childContent.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
64
65
  expectedAnswers: normalizeExpectedAnswers(asRecord(child.correctAnswer).expectedAnswers),
65
- points: asNumber(child.points, 1),
66
+ isExample: child.isExample === true,
67
+ points: child.isExample === true ? 0 : asNumber(child.points, 1),
66
68
  });
67
69
  });
68
70
  return flattened;
@@ -23,6 +23,7 @@ function mapQuestion(item, index) {
23
23
  const record = asRecord(item);
24
24
  const content = asRecord(record.content);
25
25
  const correctAnswer = asRecord(record.correctAnswer);
26
+ const isExample = record.isExample === true;
26
27
  return {
27
28
  questionText: asString(content.questionText) ||
28
29
  asString(content.question) ||
@@ -30,7 +31,8 @@ function mapQuestion(item, index) {
30
31
  inputType: asString(content.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
31
32
  audioUrl: asString(content.audioUrl),
32
33
  expectedAnswers: normalizeExpectedAnswers(correctAnswer.expectedAnswers),
33
- points: asNumber(record.points, 1),
34
+ isExample,
35
+ points: isExample ? 0 : asNumber(record.points, 1),
34
36
  questionNumber: asNumber(record.questionNumber, index + 1),
35
37
  };
36
38
  }
@@ -66,9 +68,9 @@ export function mapQuestionToSpontaneousQaGroupData(question) {
66
68
  return {
67
69
  instruction: asString(asRecord(list.meta).instruction) ||
68
70
  asString(asRecord(content.meta).instruction) ||
69
- 'Talk about the topic.',
71
+ 'In pairs, ask and answer the questions.',
70
72
  parts,
71
73
  explanation: asString(question.explanation),
72
- points: parts.reduce((total, part) => (total + part.questions.reduce((partTotal, item) => partTotal + (item.points || 0), 0)), 0),
74
+ points: parts.reduce((total, part) => (total + part.questions.reduce((partTotal, item) => (partTotal + (item.isExample ? 0 : (item.points || 0))), 0)), 0),
73
75
  };
74
76
  }