@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.
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.d.ts +2 -2
- package/dist/components/questions/_shared/types/choose-the-correct-answer-group.type.js +5 -1
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.d.ts +7 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseAnswerGroupImageUpload.js +4 -1
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +24 -22
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupCreator.js +45 -58
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.d.ts +6 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupItemEditor.js +145 -0
- package/dist/components/questions/types/choose-the-correct-answer-group/map-choose-the-correct-answer-group-data.js +6 -15
- package/dist/components/questions/types/choose-the-correct-answer-group/transform.js +5 -4
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
- package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +79 -3
- package/dist/components/questions/types/spontaneous-qa-group/index.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/index.js +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.d.ts +2 -0
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +74 -0
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
- package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
- package/dist/components/questions/viewer/QuestionViewer.js +22 -9
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -79
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
- package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +12 -4
- package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
- package/dist/shared/types/questions/answer-the-question-group.js +5 -0
- package/dist/shared/types/questions/choose-the-correct-answer-group.d.ts +1 -1
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
- 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'
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 {
|
|
4
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts
CHANGED
|
@@ -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;
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
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
|
-
}) }),
|
|
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("
|
|
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?:
|
|
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
|
-
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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 {};
|