@tinyweb_dev/oe-exam-sdk 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
  2. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
  3. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -2
  4. package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +5 -1
  5. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
  6. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
  7. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
  8. package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
  9. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
  10. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
  11. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
  12. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
  13. package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
  14. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +7 -0
  15. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +4 -1
  16. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +24 -22
  17. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +45 -58
  18. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.d.ts +6 -0
  19. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.js +145 -0
  20. package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +6 -15
  21. package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -4
  22. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
  23. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
  24. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
  25. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
  26. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
  27. package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
  28. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
  29. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
  30. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
  31. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
  32. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
  33. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
  34. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
  35. package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +79 -3
  36. package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +1 -0
  37. package/dist/components/questions/types/spontaneous-qa-group/index.js +1 -0
  38. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.d.ts +2 -0
  39. package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +74 -0
  40. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
  41. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
  42. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
  43. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
  44. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
  45. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
  46. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
  47. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
  48. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
  49. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
  50. package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
  51. package/dist/components/questions/viewer/QuestionViewer.js +22 -9
  52. package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -79
  53. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
  54. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
  55. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
  56. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
  57. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
  58. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
  59. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
  60. package/dist/shared/lib/utils/question-reverse-transform.js +12 -4
  61. package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
  62. package/dist/shared/types/questions/answer-the-question-group.js +5 -0
  63. package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -1
  64. package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
  65. package/package.json +1 -1
@@ -1,69 +1,168 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from 'react';
3
4
  import { Card } from '../../../../components/ui/card';
4
5
  import { Badge } from '../../../../components/ui/badge';
5
- // 10 Distinct pastel highlighter colors for word selections
6
6
  const WORD_COLORS = [
7
- { bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white', stroke: '#f59e0b' },
8
- { bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white', stroke: '#10b981' },
9
- { bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white', stroke: '#0ea5e9' },
10
- { bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white', stroke: '#a855f7' },
11
- { bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white', stroke: '#f43f5e' },
12
- { bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white', stroke: '#6366f1' },
13
- { bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white', stroke: '#14b8a6' },
14
- { bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white', stroke: '#eab308' },
15
- { bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white', stroke: '#ec4899' },
16
- { bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white', stroke: '#06b6d4' },
7
+ { bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white' },
8
+ { bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white' },
9
+ { bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white' },
10
+ { bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white' },
11
+ { bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white' },
12
+ { bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white' },
13
+ { bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white' },
14
+ { bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white' },
15
+ { bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white' },
16
+ { bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white' },
17
17
  ];
18
- export function FindWordsInMatrixClient({ content, correctAnswer, showSolution = false, }) {
18
+ function cleanWord(value) {
19
+ return (value || '').replace(/[\s\-_]/g, '').toUpperCase();
20
+ }
21
+ function getStraightLinePath(start, end) {
22
+ const dr = end.row - start.row;
23
+ const dc = end.col - start.col;
24
+ const stepR = dr === 0 ? 0 : dr > 0 ? 1 : -1;
25
+ const stepC = dc === 0 ? 0 : dc > 0 ? 1 : -1;
26
+ if (dr === 0 && dc !== 0) {
27
+ return Array.from({ length: Math.abs(dc) + 1 }, (_, i) => ({
28
+ row: start.row,
29
+ col: start.col + i * stepC,
30
+ }));
31
+ }
32
+ if (dc === 0 && dr !== 0) {
33
+ return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
34
+ row: start.row + i * stepR,
35
+ col: start.col,
36
+ }));
37
+ }
38
+ if (Math.abs(dr) === Math.abs(dc) && dr !== 0) {
39
+ return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
40
+ row: start.row + i * stepR,
41
+ col: start.col + i * stepC,
42
+ }));
43
+ }
44
+ return dr === 0 && dc === 0 ? [start] : [];
45
+ }
46
+ function readSelections(value) {
47
+ if (Array.isArray(value)) {
48
+ return value;
49
+ }
50
+ if (value && typeof value === 'object' && Array.isArray(value.words)) {
51
+ return value.words;
52
+ }
53
+ return [];
54
+ }
55
+ function pathLetters(grid, path) {
56
+ return path
57
+ .map((cell) => (grid[cell.row - 1]?.[cell.col - 1] || '').toUpperCase())
58
+ .join('');
59
+ }
60
+ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution = false, userAnswer, onAnswerChange, }) {
19
61
  const rows = content.gridSize?.rows || content.grid?.length || 14;
20
62
  const cols = content.gridSize?.cols || content.grid?.[0]?.length || 14;
21
63
  const grid = content.grid || [];
22
64
  const wordBank = content.wordBank || [];
23
65
  const isCategorize = content.type === 'CATEGORIZE' && Array.isArray(content.categories) && content.categories.length > 0;
24
66
  const categories = content.categories || [];
25
- // Build cell highlight map
26
- const cellColorMap = {};
27
- // Example selection
28
- if (content.exampleSelection?.path) {
29
- for (const c of content.exampleSelection.path) {
30
- cellColorMap[`${c.row}-${c.col}`] = { colorIdx: 0, word: 'Example' };
67
+ const canSelect = !showSolution && Boolean(onAnswerChange);
68
+ const selections = readSelections(userAnswer);
69
+ const [activeWordId, setActiveWordId] = useState(null);
70
+ const [anchor, setAnchor] = useState(null);
71
+ const playableWords = wordBank.filter((word) => !word.isExample);
72
+ const activeWord = playableWords.find((word) => word.id === activeWordId) ||
73
+ playableWords.find((word) => !selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word))) ||
74
+ null;
75
+ const cellColorMap = useMemo(() => {
76
+ const map = {};
77
+ if (content.exampleSelection?.path) {
78
+ for (const cell of content.exampleSelection.path) {
79
+ map[`${cell.row}-${cell.col}`] = { colorIdx: 0, word: 'Example' };
80
+ }
31
81
  }
32
- }
33
- // Solution paths if visible
34
- if (showSolution && correctAnswer?.words) {
35
- correctAnswer.words.forEach((item, idx) => {
36
- const colorIdx = (idx + 1) % WORD_COLORS.length;
37
- item.path?.forEach((c) => {
38
- cellColorMap[`${c.row}-${c.col}`] = { colorIdx, word: item.word || item.wordId };
82
+ const source = showSolution && correctAnswer?.words ? correctAnswer.words : selections;
83
+ source.forEach((item, index) => {
84
+ const colorIdx = (index + 1) % WORD_COLORS.length;
85
+ item.path?.forEach((cell) => {
86
+ map[`${cell.row}-${cell.col}`] = {
87
+ colorIdx,
88
+ word: item.word || item.wordId || '',
89
+ };
39
90
  });
40
91
  });
41
- }
42
- return (_jsxs("div", { className: "space-y-3", children: [content.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 dark:text-gray-100", children: content.title })), isCategorize ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4", children: categories.map((cat) => {
43
- const wordsInCat = wordBank.filter((w) => w.categoryId === cat.id);
44
- return (_jsxs(Card, { className: "p-3.5 bg-gray-50/70 dark:bg-gray-800/40 border", children: [_jsxs("div", { className: "font-bold text-sm text-gray-800 dark:text-gray-200 border-b pb-1.5 mb-2.5 flex items-center justify-between", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((w, idx) => {
45
- const isExample = Boolean(w.isExample);
46
- return (_jsxs(Badge, { variant: "outline", className: `px-2.5 py-1 text-xs font-medium ${isExample
47
- ? 'line-through bg-amber-50 text-amber-800 border-amber-300'
48
- : 'bg-white dark:bg-gray-800'}`, children: [w.word, isExample && _jsx("span", { className: "ml-1 text-amber-600 font-bold", children: "(Ex)" })] }, w.id || idx));
49
- }) })] }, cat.id));
50
- }) })) : (_jsxs(Card, { className: "p-4 bg-gray-50 dark:bg-gray-800/50 border-dashed border-2", children: [_jsx("div", { className: "text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3 flex items-center gap-2", children: _jsxs("span", { children: ["Word Bank (", wordBank.length, " words)"] }) }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((w, idx) => {
51
- const isExample = Boolean(w.isExample);
52
- return (_jsxs(Badge, { variant: "outline", className: `px-3 py-1.5 text-sm font-medium transition-all ${isExample
53
- ? 'line-through bg-amber-50 text-amber-800 border-amber-300'
54
- : 'bg-white dark:bg-gray-800 hover:shadow-sm'}`, children: [w.word, isExample && (_jsx("span", { className: "ml-1.5 text-xs text-amber-600 font-bold no-underline", children: "(Example)" }))] }, w.id || idx));
55
- }) })] })), _jsx("div", { className: "overflow-x-auto pb-2 flex justify-center", children: _jsx("div", { className: "inline-grid gap-1 p-3 bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800", style: {
56
- gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
57
- }, children: Array.from({ length: rows }).map((_, rIdx) => {
92
+ return map;
93
+ }, [content.exampleSelection, correctAnswer?.words, selections, showSolution]);
94
+ const emit = (next) => {
95
+ onAnswerChange?.({ words: next });
96
+ };
97
+ const handleWordClick = (wordId) => {
98
+ if (!canSelect)
99
+ return;
100
+ const already = selections.find((selection) => selection.wordId === wordId);
101
+ if (already) {
102
+ emit(selections.filter((selection) => selection.wordId !== wordId));
103
+ setActiveWordId(null);
104
+ setAnchor(null);
105
+ return;
106
+ }
107
+ setActiveWordId(wordId);
108
+ setAnchor(null);
109
+ };
110
+ const handleCellClick = (row, col) => {
111
+ if (!canSelect || !activeWord)
112
+ return;
113
+ const cell = { row, col };
114
+ if (!anchor) {
115
+ setAnchor(cell);
116
+ return;
117
+ }
118
+ const path = getStraightLinePath(anchor, cell);
119
+ const letters = pathLetters(grid, path);
120
+ const target = cleanWord(activeWord.word);
121
+ const matched = letters === target || [...letters].reverse().join('') === target;
122
+ setAnchor(null);
123
+ if (!matched || path.length === 0) {
124
+ setAnchor(cell);
125
+ return;
126
+ }
127
+ emit([
128
+ ...selections.filter((selection) => selection.wordId !== activeWord.id && cleanWord(selection.word) !== target),
129
+ {
130
+ wordId: activeWord.id,
131
+ word: activeWord.word,
132
+ categoryId: activeWord.categoryId,
133
+ path,
134
+ },
135
+ ]);
136
+ setActiveWordId(null);
137
+ };
138
+ const renderWordBadge = (word, index) => {
139
+ const isExample = Boolean(word.isExample);
140
+ const isFound = selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word));
141
+ const isActive = activeWord?.id === word.id;
142
+ return (_jsxs("button", { type: "button", disabled: !canSelect || isExample, onClick: () => handleWordClick(word.id), className: `rounded-full border px-3 py-1.5 text-sm font-medium ${isExample
143
+ ? 'border-amber-300 bg-amber-50 text-amber-800 line-through'
144
+ : isFound
145
+ ? 'border-emerald-400 bg-emerald-50 text-emerald-800'
146
+ : isActive
147
+ ? 'border-blue-500 bg-blue-50 text-blue-800'
148
+ : 'border-gray-200 bg-white'}`, children: [word.word, isExample ? _jsx("span", { className: "ml-1 font-bold", children: "(Ex)" }) : null] }, word.id || index));
149
+ };
150
+ return (_jsxs("div", { className: "space-y-3", children: [content.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 dark:text-gray-100", children: content.title })), canSelect ? (_jsx("p", { className: "text-xs text-slate-500", children: "Ch\u1ECDn t\u1EEB trong word bank, r\u1ED3i b\u1EA5m \u00F4 \u0111\u1EA7u v\u00E0 \u00F4 cu\u1ED1i tr\u00EAn l\u01B0\u1EDBi." })) : null, isCategorize ? (_jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3", children: categories.map((cat) => {
151
+ const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
152
+ return (_jsxs(Card, { className: "border bg-gray-50/70 p-3.5", children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between border-b pb-1.5 text-sm font-bold", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((word, index) => renderWordBadge(word, index)) })] }, cat.id));
153
+ }) })) : (_jsxs(Card, { className: "border-2 border-dashed bg-gray-50 p-4", children: [_jsxs("div", { className: "mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500", children: ["Word Bank (", wordBank.length, " words)"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => renderWordBadge(word, index)) })] })), _jsx("div", { className: "flex justify-center overflow-x-auto pb-2", children: _jsx("div", { className: "inline-grid gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm", style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }, children: Array.from({ length: rows }).map((_, rIdx) => {
58
154
  const r = rIdx + 1;
59
155
  return Array.from({ length: cols }).map((_, cIdx) => {
60
156
  const c = cIdx + 1;
61
157
  const char = grid[rIdx]?.[cIdx] || '';
62
158
  const highlight = cellColorMap[`${r}-${c}`];
63
159
  const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
64
- return (_jsx("div", { className: `h-9 w-9 sm:h-11 sm:w-11 flex items-center justify-center font-bold text-sm sm:text-base select-none rounded-md transition-colors border ${colorConfig
65
- ? `${colorConfig.bg} font-extrabold shadow-sm scale-95`
66
- : 'border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/30 text-gray-700 dark:text-gray-300'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
160
+ const isAnchor = anchor?.row === r && anchor?.col === c;
161
+ return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex h-9 w-9 items-center justify-center rounded-md border text-sm font-bold sm:h-11 sm:w-11 sm:text-base ${colorConfig
162
+ ? `${colorConfig.bg} shadow-sm`
163
+ : isAnchor
164
+ ? 'border-blue-500 bg-blue-50 text-blue-800'
165
+ : 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
67
166
  });
68
167
  }) }) })] }));
69
168
  }
@@ -1,2 +1,2 @@
1
1
  import type { SpontaneousQaGroupClientProps } from '../../_shared/types/spontaneous-qa-group.type';
2
- export declare function SpontaneousQaGroupClient({ questionData, }: SpontaneousQaGroupClientProps): import("react").JSX.Element;
2
+ export declare function SpontaneousQaGroupClient({ questionData, isReviewMode, }: SpontaneousQaGroupClientProps): import("react").JSX.Element;
@@ -1,9 +1,85 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { CircleHelp, Lightbulb } from 'lucide-react';
4
- export function SpontaneousQaGroupClient({ questionData, }) {
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { CircleHelp, Lightbulb, Mic, Square, Volume2 } from 'lucide-react';
5
+ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
6
+ function QuestionAudio({ url }) {
7
+ const { previewUrl, isLoading } = usePresignedFileUrl(url);
8
+ const src = previewUrl ||
9
+ (url.startsWith('http') || url.startsWith('blob:') ? url : '');
10
+ if (!src && !isLoading) {
11
+ return null;
12
+ }
13
+ 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
+ function SpeakingReplyMic() {
16
+ const [status, setStatus] = useState('idle');
17
+ const [error, setError] = useState('');
18
+ const [blobUrl, setBlobUrl] = useState('');
19
+ const recorderRef = useRef(null);
20
+ const chunksRef = useRef([]);
21
+ const streamRef = useRef(null);
22
+ useEffect(() => {
23
+ return () => {
24
+ recorderRef.current?.stop();
25
+ streamRef.current?.getTracks().forEach((track) => track.stop());
26
+ if (blobUrl) {
27
+ URL.revokeObjectURL(blobUrl);
28
+ }
29
+ };
30
+ }, [blobUrl]);
31
+ const stopStream = () => {
32
+ streamRef.current?.getTracks().forEach((track) => track.stop());
33
+ streamRef.current = null;
34
+ };
35
+ const startRecording = async () => {
36
+ setError('');
37
+ try {
38
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
39
+ streamRef.current = stream;
40
+ chunksRef.current = [];
41
+ const recorder = new MediaRecorder(stream);
42
+ recorderRef.current = recorder;
43
+ recorder.ondataavailable = (event) => {
44
+ if (event.data.size > 0) {
45
+ chunksRef.current.push(event.data);
46
+ }
47
+ };
48
+ recorder.onstop = () => {
49
+ const blob = new Blob(chunksRef.current, { type: recorder.mimeType || 'audio/webm' });
50
+ setBlobUrl((current) => {
51
+ if (current) {
52
+ URL.revokeObjectURL(current);
53
+ }
54
+ return URL.createObjectURL(blob);
55
+ });
56
+ setStatus('recorded');
57
+ stopStream();
58
+ };
59
+ recorder.start();
60
+ setStatus('recording');
61
+ }
62
+ catch {
63
+ setError('Không thể mở microphone. Hãy cho phép quyền ghi âm.');
64
+ stopStream();
65
+ }
66
+ };
67
+ const stopRecording = () => {
68
+ if (recorderRef.current && recorderRef.current.state !== 'inactive') {
69
+ recorderRef.current.stop();
70
+ }
71
+ };
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'
73
+ ? '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." }))] }));
79
+ }
80
+ export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, }) {
5
81
  const parts = questionData.parts || [];
6
- 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.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
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
7
83
  .filter((item) => item.trim() !== '')
8
84
  .map((item) => (_jsx("li", { children: item }, item))) })] }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`))) })] }, `part-${part.partNumber}-${partIndex}`)))] }));
9
85
  }
@@ -1,2 +1,3 @@
1
1
  export * from './SpontaneousQaGroupCreator';
2
2
  export * from './SpontaneousQaGroupClient';
3
+ export * from './map-spontaneous-qa-group-data';
@@ -1,2 +1,3 @@
1
1
  export * from './SpontaneousQaGroupCreator';
2
2
  export * from './SpontaneousQaGroupClient';
3
+ export * from './map-spontaneous-qa-group-data';
@@ -0,0 +1,2 @@
1
+ import type { SpontaneousQaGroupData } from '../../_shared/types/spontaneous-qa-group.type';
2
+ export declare function mapQuestionToSpontaneousQaGroupData(question: Record<string, unknown>): SpontaneousQaGroupData;
@@ -0,0 +1,74 @@
1
+ function isRecord(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ function asRecord(value) {
5
+ return isRecord(value) ? value : {};
6
+ }
7
+ function asString(value, fallback = '') {
8
+ return typeof value === 'string' ? value : fallback;
9
+ }
10
+ function asNumber(value, fallback) {
11
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
12
+ }
13
+ function normalizeExpectedAnswers(value) {
14
+ if (!Array.isArray(value)) {
15
+ return [];
16
+ }
17
+ return value
18
+ .filter((item) => typeof item === 'string')
19
+ .map((item) => item.trim())
20
+ .filter((item) => item !== '');
21
+ }
22
+ function mapQuestion(item, index) {
23
+ const record = asRecord(item);
24
+ const content = asRecord(record.content);
25
+ const correctAnswer = asRecord(record.correctAnswer);
26
+ return {
27
+ questionText: asString(content.questionText) ||
28
+ asString(content.question) ||
29
+ asString(record.questionText),
30
+ inputType: asString(content.inputType) === 'AUDIO' ? 'AUDIO' : 'TEXT',
31
+ audioUrl: asString(content.audioUrl),
32
+ expectedAnswers: normalizeExpectedAnswers(correctAnswer.expectedAnswers),
33
+ points: asNumber(record.points, 1),
34
+ questionNumber: asNumber(record.questionNumber, index + 1),
35
+ };
36
+ }
37
+ function mapPart(item, index) {
38
+ const record = asRecord(item);
39
+ const questions = Array.isArray(record.questions) ? record.questions : [];
40
+ return {
41
+ partNumber: asNumber(record.partNumber, index + 1),
42
+ partTitle: asString(record.partTitle, `PART ${index + 1}`),
43
+ topic: asString(record.topic),
44
+ questions: questions.length > 0
45
+ ? questions.map((question, questionIndex) => mapQuestion(question, questionIndex))
46
+ : [],
47
+ };
48
+ }
49
+ export function mapQuestionToSpontaneousQaGroupData(question) {
50
+ const content = asRecord(question.content);
51
+ const list = asRecord(question.spontaneousQAList ?? content.spontaneousQAList ?? content);
52
+ const itemsRaw = Array.isArray(list.items)
53
+ ? list.items
54
+ : Array.isArray(content.items)
55
+ ? content.items
56
+ : [];
57
+ const hasNestedParts = itemsRaw.some((item) => Array.isArray(asRecord(item).questions));
58
+ const parts = hasNestedParts
59
+ ? itemsRaw.map((item, index) => mapPart(item, index))
60
+ : [mapPart({
61
+ partNumber: 1,
62
+ partTitle: '',
63
+ topic: '',
64
+ questions: itemsRaw,
65
+ }, 0)];
66
+ return {
67
+ instruction: asString(asRecord(list.meta).instruction) ||
68
+ asString(asRecord(content.meta).instruction) ||
69
+ 'Talk about the topic.',
70
+ parts,
71
+ explanation: asString(question.explanation),
72
+ points: parts.reduce((total, part) => (total + part.questions.reduce((partTotal, item) => partTotal + (item.points || 0), 0)), 0),
73
+ };
74
+ }
@@ -1,2 +1,2 @@
1
1
  import type { TrueFalseCorrectGroupClientProps } from '../../_shared/types/true-false-correct-group.type';
2
- export declare function TrueFalseCorrectGroupClient({ questionData, isReviewMode, userAnswers, }: TrueFalseCorrectGroupClientProps): import("react").JSX.Element;
2
+ export declare function TrueFalseCorrectGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: TrueFalseCorrectGroupClientProps): import("react").JSX.Element;
@@ -2,24 +2,46 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
- export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
5
+ function emptyAnswer() {
6
+ return { isTrue: null, correction: '' };
7
+ }
8
+ export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
6
9
  const items = questionData.items || [];
10
+ const emit = (itemIndex, nextItem) => {
11
+ const item = items[itemIndex];
12
+ if (isReviewMode || item?.isExample || !onAnswerChange) {
13
+ return;
14
+ }
15
+ const next = items.map((_, index) => userAnswers[index] ?? emptyAnswer());
16
+ next[itemIndex] = nextItem;
17
+ onAnswerChange(next);
18
+ };
7
19
  return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => {
8
- const userValue = userAnswers[itemIndex];
9
20
  const correctIsTrue = item.isTrue === true;
21
+ const userValue = item.isExample
22
+ ? { isTrue: item.isTrue, correction: item.correction }
23
+ : userAnswers[itemIndex];
24
+ const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
25
+ const showStudentCorrection = userValue?.isTrue === false || (isReviewMode && !correctIsTrue);
10
26
  return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "flex items-center gap-3", children: [
11
27
  { value: true, label: 'True' },
12
28
  { value: false, label: 'False' },
13
29
  ].map((opt) => {
14
30
  const isCorrect = opt.value === correctIsTrue;
15
31
  const isSelected = userValue?.isTrue === opt.value;
16
- return (_jsxs("div", { className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', isReviewMode && isCorrect
32
+ return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => emit(itemIndex, {
33
+ isTrue: opt.value,
34
+ correction: opt.value ? '' : (userValue?.correction || ''),
35
+ }), className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', canSelect ? 'cursor-pointer hover:border-blue-300' : 'cursor-default', isReviewMode && isCorrect
17
36
  ? 'border-green-500 bg-green-50 text-green-700'
18
37
  : isReviewMode && isSelected && !isCorrect
19
38
  ? 'border-red-500 bg-red-50 text-red-700'
20
39
  : isSelected
21
40
  ? 'border-blue-500 bg-blue-50 text-blue-700'
22
41
  : 'border-gray-200 bg-white text-gray-700'), children: [opt.value ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(X, { className: "h-4 w-4" }), _jsx("span", { children: opt.label })] }, String(opt.value)));
23
- }) }), !correctIsTrue && (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50/70 px-3 py-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-amber-800", children: "C\u00E2u \u0111\u00FAng" }), _jsx("p", { className: "mt-1 text-sm text-amber-900", children: item.correction || 'Chưa nhập câu sửa' }), isReviewMode && userValue && userValue.isTrue === false && (_jsxs("p", { className: "mt-2 text-xs text-slate-600", children: ["H\u1ECDc sinh: ", userValue.correction || '(trống)'] }))] }))] })] }, `${item.questionNumber}-${itemIndex}`));
42
+ }) }), showStudentCorrection && canSelect ? (_jsxs("div", { className: "rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx("label", { className: "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500", children: "Correct the false sentence" }), _jsx("textarea", { value: userValue?.correction || '', onChange: (event) => emit(itemIndex, {
43
+ isTrue: false,
44
+ correction: event.target.value,
45
+ }), placeholder: "Write the correct sentence...", rows: 2, className: "w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })] })) : null, isReviewMode && !correctIsTrue ? (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50/70 px-3 py-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-amber-800", children: "C\u00E2u \u0111\u00FAng" }), _jsx("p", { className: "mt-1 text-sm text-amber-900", children: item.correction || 'Chưa nhập câu sửa' }), userValue && userValue.isTrue === false ? (_jsxs("p", { className: "mt-2 text-xs text-slate-600", children: ["H\u1ECDc sinh: ", userValue.correction || '(trống)'] })) : null] })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
24
46
  }), 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] }))] }));
25
47
  }
@@ -1,2 +1,2 @@
1
1
  import type { TrueFalseGroupClientProps } from '../../_shared/types/true-false-group.type';
2
- export declare function TrueFalseGroupClient({ questionData, isReviewMode, userAnswers, }: TrueFalseGroupClientProps): import("react").JSX.Element;
2
+ export declare function TrueFalseGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: TrueFalseGroupClientProps): import("react").JSX.Element;
@@ -2,18 +2,28 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
- export function TrueFalseGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
5
+ export function TrueFalseGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
6
6
  const items = questionData.items || [];
7
+ const handleSelect = (itemIndex, value) => {
8
+ const item = items[itemIndex];
9
+ if (isReviewMode || item?.isExample || !onAnswerChange) {
10
+ return;
11
+ }
12
+ const next = items.map((_, index) => userAnswers[index]);
13
+ next[itemIndex] = value;
14
+ onAnswerChange(next);
15
+ };
7
16
  return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => {
8
- const userValue = userAnswers[itemIndex];
17
+ const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
9
18
  const correctAnswer = item.correctAnswer;
19
+ const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
10
20
  return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "flex items-center gap-3", children: [
11
21
  { value: true, label: 'True' },
12
22
  { value: false, label: 'False' },
13
23
  ].map((opt) => {
14
24
  const isCorrect = opt.value === correctAnswer;
15
25
  const isSelected = opt.value === userValue;
16
- return (_jsxs("div", { className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', isReviewMode && isCorrect
26
+ return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => handleSelect(itemIndex, opt.value), className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', canSelect ? 'cursor-pointer hover:border-blue-300' : 'cursor-default', isReviewMode && isCorrect
17
27
  ? 'border-green-500 bg-green-50 text-green-700'
18
28
  : isReviewMode && isSelected && !isCorrect
19
29
  ? 'border-red-500 bg-red-50 text-red-700'
@@ -2,7 +2,8 @@ import type { Question } from '../../../shared/types/entities/question.types';
2
2
  type AnswerTheQuestionGroupViewerProps = {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
- userAnswer?: string[];
5
+ userAnswer?: unknown;
6
+ onSubmitAnswer?: (answer: string[]) => void;
6
7
  };
7
- export declare function AnswerTheQuestionGroupViewer({ question, isReviewMode, userAnswer, }: AnswerTheQuestionGroupViewerProps): import("react").JSX.Element;
8
+ export declare function AnswerTheQuestionGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: AnswerTheQuestionGroupViewerProps): import("react").JSX.Element;
8
9
  export {};
@@ -2,10 +2,17 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { AnswerTheQuestionGroupClient } from '../types/answer-the-question-group/AnswerTheQuestionGroupClient';
4
4
  import { mapQuestionToAnswerTheQuestionGroupData } from '../types/answer-the-question-group/map-answer-the-question-group-data';
5
- export function AnswerTheQuestionGroupViewer({ question, isReviewMode = false, userAnswer, }) {
5
+ function readStringAnswers(value) {
6
+ if (Array.isArray(value)) {
7
+ return value.map((item) => String(item ?? ''));
8
+ }
9
+ if (value && typeof value === 'object' && Array.isArray(value.answer)) {
10
+ return value.answer.map((item) => String(item ?? ''));
11
+ }
12
+ return undefined;
13
+ }
14
+ export function AnswerTheQuestionGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
6
15
  const questionData = mapQuestionToAnswerTheQuestionGroupData(question);
7
- const reviewAnswers = isReviewMode && !userAnswer
8
- ? questionData.items.map((item) => item.expectedAnswers[0] || '')
9
- : userAnswer;
10
- return (_jsx(AnswerTheQuestionGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: reviewAnswers }));
16
+ const storedAnswers = readStringAnswers(userAnswer);
17
+ return (_jsx(AnswerTheQuestionGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: storedAnswers, onAnswerChange: isReviewMode ? undefined : onSubmitAnswer }));
11
18
  }
@@ -1,9 +1,10 @@
1
1
  import type { Question } from '../../../shared/types/entities/question.types';
2
- import type { ChooseThenAnswerStudentItemAnswer } from '../_shared/types/choose-then-answer-group.type';
3
- export declare function ChooseThenAnswerGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: {
2
+ import type { ChooseThenAnswerGradeDetail, ChooseThenAnswerStudentItemAnswer } from '../_shared/types/choose-then-answer-group.type';
3
+ export declare function ChooseThenAnswerGroupViewer({ question, isReviewMode, userAnswer, gradeDetails, onSubmitAnswer, }: {
4
4
  question: Question;
5
5
  isReviewMode?: boolean;
6
6
  userAnswer?: unknown;
7
+ gradeDetails?: ChooseThenAnswerGradeDetail[];
7
8
  onSubmitAnswer?: (answer: {
8
9
  answer: ChooseThenAnswerStudentItemAnswer[];
9
10
  }) => void;
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { ChooseThenAnswerGroupClient } from '../types/choose-then-answer-group/ChooseThenAnswerGroupClient';
5
- import { mapQuestionToChooseThenAnswerGroupData, parseStudentAnswer, } from '../types/choose-then-answer-group/map-choose-then-answer-group-data';
5
+ import { mapQuestionToChooseThenAnswerGroupData, parseStudentAnswer, toCtaItemResults, } from '../types/choose-then-answer-group/map-choose-then-answer-group-data';
6
6
  function readStudentAnswers(value, itemCount) {
7
7
  const record = value && typeof value === 'object' && !Array.isArray(value)
8
8
  ? value
@@ -16,7 +16,7 @@ function readStudentAnswers(value, itemCount) {
16
16
  return undefined;
17
17
  return Array.from({ length: itemCount }, (_, index) => parseStudentAnswer(raw[index]));
18
18
  }
19
- export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, userAnswer, onSubmitAnswer, }) {
19
+ export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, userAnswer, gradeDetails, onSubmitAnswer, }) {
20
20
  const questionData = mapQuestionToChooseThenAnswerGroupData(question);
21
21
  const seed = questionData.items.map((item) => ({
22
22
  optionId: item.isExample ? item.optionId : '',
@@ -35,5 +35,6 @@ export function ChooseThenAnswerGroupViewer({ question, isReviewMode = false, us
35
35
  setLocalAnswers(updated);
36
36
  onSubmitAnswer?.({ answer: updated });
37
37
  };
38
- return (_jsx(ChooseThenAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode && !parsedUser ? undefined : (parsedUser ?? localAnswers), onAnswerChange: isReviewMode ? undefined : handleChange }));
38
+ const displayedAnswers = isReviewMode && !parsedUser ? [] : (parsedUser ?? localAnswers);
39
+ return (_jsx(ChooseThenAnswerGroupClient, { questionData: questionData, isReviewMode: isReviewMode, userAnswers: isReviewMode && !parsedUser ? undefined : displayedAnswers, itemResults: toCtaItemResults(gradeDetails, questionData.items, displayedAnswers), onAnswerChange: isReviewMode ? undefined : handleChange }));
39
40
  }
@@ -2,6 +2,8 @@ import type { Question } from '../../../shared/types/entities/question.types';
2
2
  type CrossOutWordGroupViewerProps = {
3
3
  question: Question;
4
4
  isReviewMode?: boolean;
5
+ userAnswer?: unknown;
6
+ onSubmitAnswer?: (answer: unknown[]) => void;
5
7
  };
6
- export declare function CrossOutWordGroupViewer({ question, isReviewMode, }: CrossOutWordGroupViewerProps): import("react").JSX.Element;
8
+ export declare function CrossOutWordGroupViewer({ question, isReviewMode, userAnswer, onSubmitAnswer, }: CrossOutWordGroupViewerProps): import("react").JSX.Element;
7
9
  export {};