@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,67 +1,96 @@
1
+ function asSubtype(value) {
2
+ return value === 'HIDDEN_WORD' ? 'HIDDEN_WORD' : 'STANDARD';
3
+ }
4
+ function mapClue(raw) {
5
+ return {
6
+ number: Number(raw.number),
7
+ clue: String(raw.clue || '').trim(),
8
+ startRow: Number(raw.startRow) || 1,
9
+ startCol: Number(raw.startCol) || 1,
10
+ length: Number(raw.length) || (raw.answer ? String(raw.answer).trim().length : 1),
11
+ ...(raw.isExample === true ? { isExample: true } : {}),
12
+ };
13
+ }
14
+ function collectAnswers(formClues, hiddenWord, existing) {
15
+ const answers = { ...(existing || {}) };
16
+ for (const c of formClues) {
17
+ const key = `${c.direction}-${c.number}`;
18
+ if (c.answer) {
19
+ answers[key] = String(c.answer).trim().toUpperCase();
20
+ }
21
+ }
22
+ const hidden = (hiddenWord || answers['hidden-word'] || '').trim();
23
+ if (hidden) {
24
+ answers['hidden-word'] = hidden.toUpperCase();
25
+ }
26
+ return answers;
27
+ }
1
28
  export const transformCrosswordPuzzle = (question) => {
2
29
  const contentData = question.content;
3
30
  const answerData = question.answer;
4
31
  let content;
5
32
  let correctAnswer;
6
33
  if (contentData?.gridSize && contentData?.clues) {
34
+ const type = asSubtype(contentData.type);
35
+ const markedColumn = Number(contentData.markedColumn);
7
36
  content = {
37
+ type,
8
38
  title: contentData.title || '',
39
+ ...(contentData.instruction ? { instruction: String(contentData.instruction) } : {}),
9
40
  gridSize: {
10
41
  rows: Number(contentData.gridSize.rows) || 12,
11
42
  cols: Number(contentData.gridSize.cols) || 12,
12
43
  },
44
+ ...(type === 'HIDDEN_WORD' && Number.isFinite(markedColumn) && markedColumn > 0
45
+ ? { markedColumn }
46
+ : {}),
13
47
  clues: {
14
- across: Array.isArray(contentData.clues.across) ? contentData.clues.across : [],
15
- down: Array.isArray(contentData.clues.down) ? contentData.clues.down : [],
48
+ across: Array.isArray(contentData.clues.across)
49
+ ? contentData.clues.across.map(mapClue)
50
+ : [],
51
+ down: Array.isArray(contentData.clues.down)
52
+ ? contentData.clues.down.map(mapClue)
53
+ : [],
16
54
  },
17
55
  };
56
+ const answers = { ...(answerData?.answers || {}) };
57
+ const hidden = String(answerData?.hiddenWord || answers['hidden-word'] || '').trim();
58
+ if (type === 'HIDDEN_WORD' && hidden) {
59
+ answers['hidden-word'] = hidden.toUpperCase();
60
+ }
18
61
  correctAnswer = {
19
- answers: answerData?.answers || {},
62
+ answers,
20
63
  caseSensitive: answerData?.caseSensitive || false,
21
64
  };
22
65
  }
23
66
  else if (Array.isArray(contentData?.clues)) {
24
67
  const formClues = contentData.clues;
25
- const across = formClues
26
- .filter((c) => c.direction === 'across')
27
- .map((c) => ({
28
- number: Number(c.number),
29
- clue: String(c.clue || '').trim(),
30
- startRow: Number(c.startRow) || 1,
31
- startCol: Number(c.startCol) || 1,
32
- length: Number(c.length) || (c.answer ? String(c.answer).trim().length : 1),
33
- }));
34
- const down = formClues
35
- .filter((c) => c.direction === 'down')
36
- .map((c) => ({
37
- number: Number(c.number),
38
- clue: String(c.clue || '').trim(),
39
- startRow: Number(c.startRow) || 1,
40
- startCol: Number(c.startCol) || 1,
41
- length: Number(c.length) || (c.answer ? String(c.answer).trim().length : 1),
42
- }));
68
+ const type = asSubtype(contentData.type);
69
+ const markedColumn = Number(contentData.markedColumn);
43
70
  content = {
71
+ type,
44
72
  title: contentData.title || '',
73
+ ...(contentData.instruction ? { instruction: String(contentData.instruction) } : {}),
45
74
  gridSize: {
46
75
  rows: Number(contentData.rows) || 12,
47
76
  cols: Number(contentData.cols) || 12,
48
77
  },
49
- clues: { across, down },
78
+ ...(type === 'HIDDEN_WORD' && Number.isFinite(markedColumn) && markedColumn > 0
79
+ ? { markedColumn }
80
+ : {}),
81
+ clues: {
82
+ across: formClues.filter((c) => c.direction === 'across').map(mapClue),
83
+ down: formClues.filter((c) => c.direction === 'down').map(mapClue),
84
+ },
50
85
  };
51
- const answers = {};
52
- for (const c of formClues) {
53
- const key = `${c.direction}-${c.number}`;
54
- if (c.answer) {
55
- answers[key] = String(c.answer).trim().toUpperCase();
56
- }
57
- }
58
86
  correctAnswer = {
59
- answers,
87
+ answers: collectAnswers(formClues, contentData.hiddenWord, answerData?.answers),
60
88
  caseSensitive: contentData.caseSensitive || false,
61
89
  };
62
90
  }
63
91
  else {
64
92
  content = {
93
+ type: 'STANDARD',
65
94
  gridSize: { rows: 12, cols: 12 },
66
95
  clues: { across: [], down: [] },
67
96
  };
@@ -2,13 +2,14 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useMemo } from 'react';
4
4
  import { Input } from '../../../../components/ui/input';
5
- import { Check, X, PenLine, AlertTriangle, ClipboardList, Lightbulb, BookOpen, Hash, Volume2, } from 'lucide-react';
5
+ import { Check, X, PenLine, ClipboardList, Lightbulb, BookOpen, Hash, Volume2, } from 'lucide-react';
6
6
  import { ImageLabelWordBank } from '../../groups/ImageLabelWordBank';
7
7
  import { hasImageLabels } from '../../_shared/types/question-group.utils';
8
8
  import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAnswer, isBlankInputCorrect, normalizeBlankAnswers, normalizeFillInBlankAnswers, } from './answer-utils';
9
9
  import { parseFillBlankSegments } from '../../../../shared/lib/utils/fill-in-blank';
10
10
  import { cn } from '../../../../shared/lib/utils';
11
11
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
12
+ import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
12
13
  // Sub-component: Resolve presigned URL for group image
13
14
  function GroupImage({ src, alt, className }) {
14
15
  const { previewUrl, isLoading } = usePresignedFileUrl(src);
@@ -143,11 +144,11 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
143
144
  const userInput = answers[placeholderIndex] || '';
144
145
  const isCorrect = isReviewMode && isAnswerCorrect(placeholderIndex, userInput);
145
146
  const isWrong = isReviewMode && userInput && !isCorrect;
146
- return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx("span", { className: "flex h-5 w-5 items-center justify-center rounded-md bg-orange-100 text-[10px] font-bold text-orange-600 mr-1 ring-1 ring-orange-200/60", children: placeholderIndex + 1 }), _jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(placeholderIndex, e.target.value), disabled: isReviewMode, className: cn('h-9 w-28 text-center text-sm font-medium rounded-lg border-2 transition-all duration-200', 'focus:ring-2 focus:ring-offset-1 focus:outline-none', isReviewMode
147
+ return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx("span", { className: "flex h-5 w-5 items-center justify-center rounded-md bg-orange-100 text-[10px] font-bold text-orange-600 mr-1 ring-1 ring-orange-200/60", children: placeholderIndex + 1 }), _jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(placeholderIndex, e.target.value), disabled: isReviewMode, className: cn('h-8 w-28 text-center text-sm font-medium rounded-md border px-3 py-1.5 transition-all duration-200', 'focus:ring-2 focus:ring-offset-1 focus:outline-none', isReviewMode
147
148
  ? isCorrect
148
- ? 'border-emerald-400 bg-emerald-50 text-emerald-700'
149
+ ? 'border-green-400 bg-green-50 text-green-800'
149
150
  : isWrong
150
- ? 'border-red-400 bg-red-50 text-red-700'
151
+ ? 'border-red-400 bg-red-50 text-red-800'
151
152
  : 'border-gray-200 bg-gray-50 text-gray-500'
152
153
  : 'border-orange-200 bg-white hover:border-orange-300 focus:border-orange-400 focus:ring-orange-200/50'), placeholder: "..." }), isReviewMode && (isCorrect || isWrong) && (_jsx("span", { className: cn('absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full shadow-sm ring-2 ring-white', isCorrect ? 'bg-emerald-500' : 'bg-red-500'), children: isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5 text-white" })) : (_jsx(X, { className: "h-2.5 w-2.5 text-white" })) }))] }, partIndex));
153
154
  }
@@ -162,11 +163,11 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
162
163
  const userInput = answers[blankIndex] || '';
163
164
  const isCorrect = isReviewMode && isAnswerCorrect(blankIndex, userInput);
164
165
  const isWrong = isReviewMode && userInput && !isCorrect;
165
- return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(blankIndex, e.target.value), disabled: isReviewMode, className: cn('h-9 w-32 text-center text-sm font-medium rounded-lg border-2 transition-all duration-200', 'focus:ring-2 focus:ring-offset-1 focus:outline-none', isReviewMode
166
+ return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(blankIndex, e.target.value), disabled: isReviewMode, className: cn('h-8 w-32 text-center text-sm font-medium rounded-md border px-3 py-1.5 transition-all duration-200', 'focus:ring-2 focus:ring-offset-1 focus:outline-none', isReviewMode
166
167
  ? isCorrect
167
- ? 'border-emerald-400 bg-emerald-50 text-emerald-700'
168
+ ? 'border-green-400 bg-green-50 text-green-800'
168
169
  : isWrong
169
- ? 'border-red-400 bg-red-50 text-red-700'
170
+ ? 'border-red-400 bg-red-50 text-red-800'
170
171
  : 'border-gray-200 bg-gray-50 text-gray-500'
171
172
  : 'border-orange-200 bg-white hover:border-orange-300 focus:border-orange-400 focus:ring-orange-200/50'), placeholder: "..." }), isReviewMode && (isCorrect || isWrong) && (_jsx("span", { className: cn('absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full shadow-sm ring-2 ring-white', isCorrect ? 'bg-emerald-500' : 'bg-red-500'), children: isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5 text-white" })) : (_jsx(X, { className: "h-2.5 w-2.5 text-white" })) }))] }, partIdx));
172
173
  }
@@ -208,21 +209,17 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
208
209
  // =====================================================
209
210
  if (!isLegacyFormat) {
210
211
  const hasAnyAnswer = placeholderIndices.some(idx => (answers[idx] || '').trim());
211
- return (_jsxs("div", { className: "space-y-4", children: [renderGroupBlock(), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:shadow-md", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-orange-500 to-amber-600 text-white", children: _jsx(PenLine, { className: "h-3.5 w-3.5" }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-semibold text-gray-700", children: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng" }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 ring-1 ring-inset ring-orange-200/60", children: [_jsx(Hash, { className: "h-3 w-3" }), placeholderIndices.length] })] })] }), isReviewMode && score && (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-bold', score.correct === score.total
212
- ? 'bg-emerald-100 text-emerald-700 ring-1 ring-emerald-200/60'
213
- : score.correct >= score.total / 2
214
- ? 'bg-yellow-100 text-yellow-700 ring-1 ring-yellow-200/60'
215
- : 'bg-red-100 text-red-700 ring-1 ring-red-200/60'), children: [score.correct === score.total && _jsx(Check, { className: "h-3 w-3" }), score.correct, "/", score.total, " \u0111\u00FAng"] }))] }), _jsxs("div", { className: "space-y-3 p-3", children: [showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx("div", { className: "rounded-xl bg-gradient-to-r from-orange-50/80 to-amber-50/50 p-5 border border-orange-100/80", children: renderQuestionWithBlanks() }), renderQuestionImage(), renderHints(), isReviewMode && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-slate-50 to-gray-50 px-4 py-2.5", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-slate-500 text-white", children: _jsx(ClipboardList, { className: "h-3 w-3" }) }), _jsx("h4", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wider", children: "Chi ti\u1EBFt \u0111\u00E1p \u00E1n" })] }), _jsx("div", { className: "p-4 space-y-2.5", children: placeholderIndices.map((idx) => {
216
- const userInput = answers[idx] || '';
217
- const correctAnswer = formatBlankAnswersDisplay(correctAnswersMatrix[idx]) || '—';
218
- const isCorrect = isAnswerCorrect(idx, userInput);
219
- const hasInput = userInput.trim().length > 0;
220
- return (_jsxs("div", { className: cn('rounded-lg border p-3 transition-colors', isCorrect
221
- ? 'border-emerald-200 bg-emerald-50/50'
222
- : hasInput
223
- ? 'border-red-200 bg-red-50/50'
224
- : 'border-gray-100 bg-gray-50/50'), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-orange-100 text-xs font-bold text-orange-600 ring-1 ring-orange-200/60", children: idx + 1 }), _jsxs("span", { className: "text-sm text-gray-500", children: ["Ch\u1ED7 tr\u1ED1ng ", idx + 1] })] }), _jsx("div", { className: cn('flex items-center gap-1 text-xs font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3.5 w-3.5" }), " \u0110\u00FAng"] })) : hasInput ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-3.5 w-3.5" }), " Sai"] })) : ('Bỏ trống') })] }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white px-3 py-2 border border-gray-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E3 nh\u1EADp" }), _jsx("span", { className: cn('font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 px-3 py-2 border border-emerald-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E1p \u00E1n" }), _jsx("span", { className: "font-semibold text-emerald-600", children: correctAnswer })] })] })] }, idx));
225
- }) })] })), !isReviewMode && !hasAnyAnswer && (_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.5", 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(AlertTriangle, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi v\u00E0o c\u00E1c ch\u1ED7 tr\u1ED1ng" })] }))] })] }), renderExplanation()] }));
212
+ return (_jsxs("div", { className: "space-y-4", children: [renderGroupBlock(), _jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng", points: questionData.points, isExample: questionData.isExample, review: isReviewMode && hasAnyAnswer ? (score && score.correct === score.total ? 'correct' : 'wrong') : null, headerExtra: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700", children: [_jsx(Hash, { className: "h-3 w-3" }), placeholderIndices.length] }), children: [showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx(KidPrompt, { children: renderQuestionWithBlanks() }), renderQuestionImage(), renderHints(), isReviewMode && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-slate-50 to-gray-50 px-4 py-2.5", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-slate-500 text-white", children: _jsx(ClipboardList, { className: "h-3 w-3" }) }), _jsx("h4", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wider", children: "Chi ti\u1EBFt \u0111\u00E1p \u00E1n" })] }), _jsx("div", { className: "p-4 space-y-2.5", children: placeholderIndices.map((idx) => {
213
+ const userInput = answers[idx] || '';
214
+ const correctAnswer = formatBlankAnswersDisplay(correctAnswersMatrix[idx]) || '—';
215
+ const isCorrect = isAnswerCorrect(idx, userInput);
216
+ const hasInput = userInput.trim().length > 0;
217
+ return (_jsxs("div", { className: cn('rounded-lg border p-3 transition-colors', isCorrect
218
+ ? 'border-emerald-200 bg-emerald-50/50'
219
+ : hasInput
220
+ ? 'border-red-200 bg-red-50/50'
221
+ : 'border-gray-100 bg-gray-50/50'), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-orange-100 text-xs font-bold text-orange-600 ring-1 ring-orange-200/60", children: idx + 1 }), _jsxs("span", { className: "text-sm text-gray-500", children: ["Ch\u1ED7 tr\u1ED1ng ", idx + 1] })] }), _jsx("div", { className: cn('flex items-center gap-1 text-xs font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3.5 w-3.5" }), " \u0110\u00FAng"] })) : hasInput ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-3.5 w-3.5" }), " Sai"] })) : ('Bỏ trống') })] }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white px-3 py-2 border border-gray-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E3 nh\u1EADp" }), _jsx("span", { className: cn('font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 px-3 py-2 border border-emerald-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E1p \u00E1n" }), _jsx("span", { className: "font-semibold text-emerald-600", children: correctAnswer })] })] })] }, idx));
222
+ }) })] })), !isReviewMode && !hasAnyAnswer && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi v\u00E0o c\u00E1c ch\u1ED7 tr\u1ED1ng" })] }))] }), renderExplanation()] }));
226
223
  }
227
224
  // =====================================================
228
225
  // LEGACY FORMAT RENDER
@@ -231,15 +228,13 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
231
228
  const userInput = answers[index] || '';
232
229
  const isCorrect = isReviewMode && isAnswerCorrect(index, userInput);
233
230
  const hasAnswered = userInput.trim() !== '';
234
- return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:shadow-md", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-orange-500 to-amber-600 text-white", children: _jsx(PenLine, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-semibold text-gray-700", children: ["C\u00E2u ", index + 1, ": \u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng"] })] }), isReviewMode && hasAnswered && (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-bold', isCorrect
235
- ? 'bg-emerald-100 text-emerald-700 ring-1 ring-emerald-200/60'
236
- : 'bg-red-100 text-red-700 ring-1 ring-red-200/60'), 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: "space-y-3 p-3", children: [index === 0 && showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx("div", { className: "rounded-xl bg-gradient-to-r from-orange-50/80 to-amber-50/50 p-5 border border-orange-100/80", children: renderLegacyBlank(blank.text, blank.id, index) }), index === 0 && renderQuestionImage(), index === 0 && renderHints(), isReviewMode && (_jsx("div", { className: cn('rounded-xl border p-3 transition-colors', isCorrect
237
- ? 'border-emerald-200 bg-emerald-50/50'
238
- : hasAnswered
239
- ? 'border-red-200 bg-red-50/50'
240
- : 'border-gray-100 bg-gray-50/50'), children: _jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white p-3 border border-gray-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E3 nh\u1EADp:" }), _jsx("span", { className: cn('text-sm font-semibold', isCorrect ? 'text-emerald-600' : hasAnswered ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 p-3 border border-emerald-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-emerald-600", children: formatBlankAnswersDisplay(blank.correctAnswers ?? normalizeBlankAnswers(blank.correctAnswer)) || '—' })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-md p-3 border', isCorrect
241
- ? 'bg-emerald-50 border-emerald-200'
242
- : 'bg-red-50 border-red-200'), children: [_jsx("span", { className: "text-sm text-gray-500", children: "K\u1EBFt qu\u1EA3:" }), _jsx("div", { className: cn('flex items-center gap-1.5 text-sm font-bold', isCorrect ? 'text-emerald-600' : 'text-red-600'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4" }), " Ch\u00EDnh x\u00E1c"] })) : (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-4 w-4" }), " Sai"] })) })] })] }) })), !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.5", 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(AlertTriangle, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi" })] }))] })] }, blank.id));
231
+ return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${index + 1}: Điền vào chỗ trống`, review: isReviewMode && hasAnswered ? (isCorrect ? 'correct' : 'wrong') : null, children: [index === 0 && showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx(KidPrompt, { children: renderLegacyBlank(blank.text, blank.id, index) }), index === 0 && renderQuestionImage(), index === 0 && renderHints(), isReviewMode && (_jsx("div", { className: cn('rounded-xl border p-3 transition-colors', isCorrect
232
+ ? 'border-emerald-200 bg-emerald-50/50'
233
+ : hasAnswered
234
+ ? 'border-red-200 bg-red-50/50'
235
+ : 'border-gray-100 bg-gray-50/50'), children: _jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white p-3 border border-gray-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E3 nh\u1EADp:" }), _jsx("span", { className: cn('text-sm font-semibold', isCorrect ? 'text-emerald-600' : hasAnswered ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 p-3 border border-emerald-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-emerald-600", children: formatBlankAnswersDisplay(blank.correctAnswers ?? normalizeBlankAnswers(blank.correctAnswer)) || '—' })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-md p-3 border', isCorrect
236
+ ? 'bg-emerald-50 border-emerald-200'
237
+ : 'bg-red-50 border-red-200'), children: [_jsx("span", { className: "text-sm text-gray-500", children: "K\u1EBFt qu\u1EA3:" }), _jsx("div", { className: cn('flex items-center gap-1.5 text-sm font-bold', isCorrect ? 'text-emerald-600' : 'text-red-600'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4" }), " Ch\u00EDnh x\u00E1c"] })) : (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-4 w-4" }), " Sai"] })) })] })] }) })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi" })] }))] }, blank.id));
243
238
  }), isReviewMode && score && (questionData.blanks?.length || 0) > 1 && (_jsx("div", { className: cn('overflow-hidden rounded-xl border shadow-sm', score.correct === score.total
244
239
  ? 'border-emerald-200 bg-gradient-to-r from-emerald-50 to-green-50'
245
240
  : score.correct >= score.total / 2
@@ -0,0 +1,6 @@
1
+ type FillInBlankAnswerKeyProps = {
2
+ answersByBlank: string[][];
3
+ blankIndices: number[];
4
+ };
5
+ export declare function FillInBlankAnswerKey({ answersByBlank, blankIndices, }: FillInBlankAnswerKeyProps): import("react").JSX.Element;
6
+ export {};
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ import { acceptedAnswerList } from './blank-utils';
4
+ function AnswerOrList({ answers }) {
5
+ if (answers.length === 0) {
6
+ return _jsx("span", { className: "font-semibold text-green-900", children: "(tr\u1ED1ng)" });
7
+ }
8
+ return (_jsx(_Fragment, { children: answers.map((answer, index) => (_jsxs(Fragment, { children: [index > 0 && (_jsx("span", { className: "mx-1 text-xs font-normal italic text-green-600", children: "ho\u1EB7c" })), _jsx("span", { className: "font-semibold text-green-900", children: answer })] }, `${answer}-${index}`))) }));
9
+ }
10
+ export function FillInBlankAnswerKey({ answersByBlank, blankIndices, }) {
11
+ const rows = blankIndices.map((blankIndex, order) => ({
12
+ order: order + 1,
13
+ answers: acceptedAnswerList(answersByBlank[blankIndex]),
14
+ }));
15
+ const manyBlanks = rows.length > 1;
16
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-xs font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "grid grid-cols-2 gap-1.5", children: rows.map((row) => (_jsxs("div", { className: "min-w-0 rounded-md border border-green-200 bg-white px-2 py-1.5 text-sm", children: [manyBlanks && (_jsxs("div", { className: "text-[10px] font-semibold uppercase tracking-wide text-green-600", children: ["Ch\u1ED7 tr\u1ED1ng ", row.order] })), _jsx("div", { className: "flex flex-wrap items-baseline", children: _jsx(AnswerOrList, { answers: row.answers }) })] }, row.order))) })] }));
17
+ }
@@ -1,9 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { BookOpen, Lightbulb, Star, Volume2 } from 'lucide-react';
3
+ import { BookOpen, Lightbulb, PenLine, Volume2 } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
5
  import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
- import { blankSlotCount, extractPlaceholderIndices, isBlankMatch, joinSlashAnswers, padStudentAnswers, resolveBlankOptions, splitStem, splitStemTextDecorations, toFillInBlankImageUrls, } from './blank-utils';
6
+ import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
7
+ import { FillInBlankAnswerKey } from './FillInBlankAnswerKey';
8
+ import { blankSlotCount, extractPlaceholderIndices, isBlankMatch, padStudentAnswers, resolveBlankOptions, splitStem, splitStemTextDecorations, toFillInBlankImageUrls, } from './blank-utils';
7
9
  const STEM_MARK_STYLE = {
8
10
  fontWeight: 700,
9
11
  fontSize: 20,
@@ -64,36 +66,38 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
64
66
  next[itemIndex] = { answers: current };
65
67
  onAnswerChange(next);
66
68
  };
67
- return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), questionData.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 } })] })), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toFillInBlankImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Group image ${imageIndex + 1}` }, `group-image-${imageIndex}`))) })), groupHints.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: groupHints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: items.map((item, itemIndex) => {
69
+ return (_jsxs("div", { className: "space-y-4", children: [_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" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] })), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toFillInBlankImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Group image ${imageIndex + 1}` }, `group-image-${imageIndex}`))) })), groupHints.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: groupHints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3", children: items.map((item, itemIndex) => {
68
70
  const blankCount = blankSlotCount(item.question || '{0}');
69
71
  const placeholderIndices = extractPlaceholderIndices(item.question || '{0}');
70
72
  const userValue = padStudentAnswers(userAnswers[itemIndex], blankCount);
71
73
  const stem = splitStem(item.question || '{0}');
72
74
  const showItemAudio = Boolean(item.audioUrl) && !questionData.audioUrl;
73
- 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-gray-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white", children: item.questionNumber || itemIndex + 1 }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [toFillInBlankImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-2", children: toFillInBlankImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Item ${item.questionNumber} image ${imageIndex + 1}` }, `${item.questionNumber}-${imageIndex}`))) })), _jsxs("div", { className: "min-w-0 space-y-2 text-sm leading-relaxed text-slate-800", children: [showItemAudio && item.audioUrl && (_jsx(MediaPreview, { src: item.audioUrl, kind: "audio", alt: `Item ${item.questionNumber} audio` })), stem.map((part, partIndex) => {
74
- if (part.type === 'text') {
75
- return (_jsx("span", { children: _jsx(StemText, { value: part.value }) }, partIndex));
76
- }
77
- const alts = item.answers[part.index] || [];
78
- const student = userValue[part.index] || '';
79
- const canEdit = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
80
- const correct = isReviewMode && isBlankMatch(student, alts);
81
- const wrong = isReviewMode && student.trim().length > 0 && !correct;
82
- const blankOptions = resolveBlankOptions(part.index, item.wordBank, questionData.wordBank);
83
- const useOptions = Boolean(blankOptions);
84
- const fieldClass = cn('mx-1 inline-block rounded-md border-2 px-1 py-0.5 text-center text-sm font-semibold outline-none', useOptions ? 'min-w-[5.5rem]' : 'w-16', item.isExample
85
- ? 'border-amber-300 bg-amber-50 text-amber-800'
86
- : correct
87
- ? 'border-emerald-400 bg-emerald-50 text-emerald-800'
88
- : wrong
89
- ? 'border-rose-400 bg-rose-50 text-rose-800'
90
- : canEdit
91
- ? 'border-blue-300 bg-white text-slate-800 focus:border-blue-500'
92
- : 'border-blue-200 bg-blue-50 text-blue-900');
93
- if (useOptions && blankOptions) {
94
- return (_jsxs("select", { disabled: !canEdit, value: item.isExample ? (alts[0] || '') : student, onChange: (event) => handleBlankChange(itemIndex, part.index, event.target.value), className: fieldClass, children: [!item.isExample && _jsx("option", { value: "", children: "..." }), blankOptions.map((option) => (_jsx("option", { value: option, children: option }, option)))] }, partIndex));
95
- }
96
- return (_jsx("input", { type: "text", disabled: !canEdit, value: item.isExample ? (alts[0] || '') : student, onChange: (event) => handleBlankChange(itemIndex, part.index, event.target.value), placeholder: item.isExample ? '' : '...', className: fieldClass }, partIndex));
97
- })] })] }), isReviewMode && !item.isExample && (_jsxs("div", { className: "border-t border-emerald-100 bg-emerald-50 px-3 py-2 text-xs text-emerald-800", children: ["\u0110\u00E1p \u00E1n: ", placeholderIndices.map((index) => joinSlashAnswers(item.answers[index]) || '(trống)').join(' | ')] }))] }, `${item.questionNumber}-${itemIndex}`));
75
+ const hasAnyAnswer = userValue.some((value) => value.trim().length > 0);
76
+ const allBlanksCorrect = placeholderIndices.every((index) => isBlankMatch(userValue[index] || '', item.answers[index] || []));
77
+ return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${item.questionNumber || itemIndex + 1}`, points: item.points || 0, isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnyAnswer ? (allBlanksCorrect ? 'correct' : 'wrong') : null, children: [toFillInBlankImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-2", children: toFillInBlankImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Item ${item.questionNumber} image ${imageIndex + 1}` }, `${item.questionNumber}-${imageIndex}`))) })), showItemAudio && item.audioUrl && (_jsx(MediaPreview, { src: item.audioUrl, kind: "audio", alt: `Item ${item.questionNumber} audio` })), _jsx(KidPrompt, { children: _jsx("div", { className: "min-w-0 text-sm leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
78
+ if (part.type === 'text') {
79
+ return (_jsx("span", { children: _jsx(StemText, { value: part.value }) }, partIndex));
80
+ }
81
+ const alts = item.answers[part.index] || [];
82
+ const student = userValue[part.index] || '';
83
+ const canEdit = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
84
+ const correct = isReviewMode && isBlankMatch(student, alts);
85
+ const wrong = isReviewMode && student.trim().length > 0 && !correct;
86
+ const blankOptions = resolveBlankOptions(part.index, item.wordBank, questionData.wordBank);
87
+ const useOptions = Boolean(blankOptions);
88
+ const fieldClass = cn('mx-1 inline-block rounded-md border px-3 py-1.5 text-center text-sm font-semibold outline-none', useOptions ? 'min-w-[5.5rem]' : 'w-16', item.isExample
89
+ ? 'border-amber-300 bg-amber-50 text-amber-800'
90
+ : correct
91
+ ? 'border-green-400 bg-green-50 text-green-800'
92
+ : wrong
93
+ ? 'border-red-400 bg-red-50 text-red-800'
94
+ : canEdit
95
+ ? 'border-blue-300 bg-white text-slate-800 focus:border-blue-500'
96
+ : 'border-blue-200 bg-blue-50 text-blue-900');
97
+ if (useOptions && blankOptions) {
98
+ return (_jsxs("select", { disabled: !canEdit, value: item.isExample ? (alts[0] || '') : student, onChange: (event) => handleBlankChange(itemIndex, part.index, event.target.value), className: fieldClass, children: [!item.isExample && _jsx("option", { value: "", children: "..." }), blankOptions.map((option) => (_jsx("option", { value: option, children: option }, option)))] }, partIndex));
99
+ }
100
+ return (_jsx("input", { type: "text", disabled: !canEdit, value: item.isExample ? (alts[0] || '') : student, onChange: (event) => handleBlankChange(itemIndex, part.index, event.target.value), placeholder: item.isExample ? '' : '...', className: fieldClass }, partIndex));
101
+ }) }) }), isReviewMode && !item.isExample && (_jsx("div", { className: "rounded-md border border-green-100 bg-green-50 px-3 py-2", children: _jsx(FillInBlankAnswerKey, { answersByBlank: item.answers, blankIndices: placeholderIndices }) }))] }, `${item.questionNumber}-${itemIndex}`));
98
102
  }) }), 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] }))] }));
99
103
  }