@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/QuestionRenderer.js +1 -5
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
- package/dist/components/exams/take/utils/answer-transformers.js +16 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +5 -0
- package/dist/components/exams/take/utils/question-transformers.js +6 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
- package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
- package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
- package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
- package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
- package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
- package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
- package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +13 -13
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +7 -6
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
- package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
- package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +21 -23
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
- package/dist/components/questions/viewer/QuestionViewer.js +2 -2
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/results/renderers/review-question-body-movers.js +1 -1
- package/dist/components/results/review-data.js +8 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +31 -5
- package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo, useState } from 'react';
|
|
4
4
|
import { Card } from '../../../../components/ui/card';
|
|
5
5
|
import { Badge } from '../../../../components/ui/badge';
|
|
6
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
const WORD_COLORS = [
|
|
7
8
|
{ bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white' },
|
|
8
9
|
{ bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white' },
|
|
@@ -67,11 +68,21 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
|
|
|
67
68
|
const canSelect = !showSolution && Boolean(onAnswerChange);
|
|
68
69
|
const selections = readSelections(userAnswer);
|
|
69
70
|
const [activeWordId, setActiveWordId] = useState(null);
|
|
70
|
-
const [
|
|
71
|
+
const [tappedPath, setTappedPath] = useState([]);
|
|
71
72
|
const playableWords = wordBank.filter((word) => !word.isExample);
|
|
72
73
|
const activeWord = playableWords.find((word) => word.id === activeWordId) ||
|
|
73
74
|
playableWords.find((word) => !selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word))) ||
|
|
74
75
|
null;
|
|
76
|
+
const targetChars = cleanWord(activeWord?.word).split('').filter(Boolean);
|
|
77
|
+
const tappedChars = tappedPath.map((cell) => (grid[cell.row - 1]?.[cell.col - 1] || '').toUpperCase());
|
|
78
|
+
const isReverseMode = tappedChars.length > 1
|
|
79
|
+
&& targetChars.length > 1
|
|
80
|
+
&& tappedChars[0] === targetChars[targetChars.length - 1]
|
|
81
|
+
&& tappedChars[1] === targetChars[targetChars.length - 2];
|
|
82
|
+
const activeCharSequence = isReverseMode ? [...targetChars].reverse() : targetChars;
|
|
83
|
+
const nextExpectedChar = tappedPath.length < activeCharSequence.length
|
|
84
|
+
? activeCharSequence[tappedPath.length]
|
|
85
|
+
: null;
|
|
75
86
|
const cellColorMap = useMemo(() => {
|
|
76
87
|
const map = {};
|
|
77
88
|
if (content.exampleSelection?.path) {
|
|
@@ -94,6 +105,22 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
|
|
|
94
105
|
const emit = (next) => {
|
|
95
106
|
onAnswerChange?.({ words: next });
|
|
96
107
|
};
|
|
108
|
+
const completeWord = (path) => {
|
|
109
|
+
if (!activeWord)
|
|
110
|
+
return;
|
|
111
|
+
const target = cleanWord(activeWord.word);
|
|
112
|
+
emit([
|
|
113
|
+
...selections.filter((selection) => selection.wordId !== activeWord.id && cleanWord(selection.word) !== target),
|
|
114
|
+
{
|
|
115
|
+
wordId: activeWord.id,
|
|
116
|
+
word: activeWord.word,
|
|
117
|
+
categoryId: activeWord.categoryId,
|
|
118
|
+
path,
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
setTappedPath([]);
|
|
122
|
+
setActiveWordId(null);
|
|
123
|
+
};
|
|
97
124
|
const handleWordClick = (wordId) => {
|
|
98
125
|
if (!canSelect)
|
|
99
126
|
return;
|
|
@@ -101,39 +128,50 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
|
|
|
101
128
|
if (already) {
|
|
102
129
|
emit(selections.filter((selection) => selection.wordId !== wordId));
|
|
103
130
|
setActiveWordId(null);
|
|
104
|
-
|
|
131
|
+
setTappedPath([]);
|
|
105
132
|
return;
|
|
106
133
|
}
|
|
107
134
|
setActiveWordId(wordId);
|
|
108
|
-
|
|
135
|
+
setTappedPath([]);
|
|
109
136
|
};
|
|
110
137
|
const handleCellClick = (row, col) => {
|
|
111
|
-
if (!canSelect || !activeWord)
|
|
138
|
+
if (!canSelect || !activeWord || targetChars.length === 0)
|
|
112
139
|
return;
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
const char = (grid[row - 1]?.[col - 1] || '').toUpperCase();
|
|
141
|
+
const last = tappedPath[tappedPath.length - 1];
|
|
142
|
+
if (last && last.row === row && last.col === col) {
|
|
143
|
+
setTappedPath((prev) => prev.slice(0, -1));
|
|
116
144
|
return;
|
|
117
145
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
setAnchor(null);
|
|
123
|
-
if (!matched || path.length === 0) {
|
|
124
|
-
setAnchor(cell);
|
|
146
|
+
if (tappedPath.length === 0) {
|
|
147
|
+
if (char === targetChars[0] || char === targetChars[targetChars.length - 1]) {
|
|
148
|
+
setTappedPath([{ row, col }]);
|
|
149
|
+
}
|
|
125
150
|
return;
|
|
126
151
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
const existingIdx = tappedPath.findIndex((cell) => cell.row === row && cell.col === col);
|
|
153
|
+
if (existingIdx >= 0) {
|
|
154
|
+
setTappedPath((prev) => prev.slice(0, existingIdx + 1));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const straightPath = getStraightLinePath(tappedPath[0], { row, col });
|
|
158
|
+
const target = targetChars.join('');
|
|
159
|
+
const letters = pathLetters(grid, straightPath);
|
|
160
|
+
if (straightPath.length === targetChars.length
|
|
161
|
+
&& (letters === target || [...letters].reverse().join('') === target)) {
|
|
162
|
+
completeWord(straightPath);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const prev = tappedPath[tappedPath.length - 1];
|
|
166
|
+
const isAdjacent = Math.abs(row - prev.row) <= 1 && Math.abs(col - prev.col) <= 1;
|
|
167
|
+
if (isAdjacent && char === nextExpectedChar) {
|
|
168
|
+
const nextPath = [...tappedPath, { row, col }];
|
|
169
|
+
if (nextPath.length === activeCharSequence.length) {
|
|
170
|
+
completeWord(nextPath);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
setTappedPath(nextPath);
|
|
174
|
+
}
|
|
137
175
|
};
|
|
138
176
|
const renderWordBadge = (word, index) => {
|
|
139
177
|
const isExample = Boolean(word.isExample);
|
|
@@ -147,7 +185,12 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
|
|
|
147
185
|
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
148
186
|
: 'border-gray-200 bg-white'}`, children: [word.word, isExample ? _jsx("span", { className: "ml-1 font-bold", children: "(Ex)" }) : null] }, word.id || index));
|
|
149
187
|
};
|
|
150
|
-
|
|
188
|
+
const questionInstruction = content.instruction?.trim();
|
|
189
|
+
return (_jsxs(KidQuestionCard, { title: content.title || 'Tìm từ trong lưới', children: [questionInstruction ? (_jsx(KidInstruction, { children: questionInstruction })) : null, canSelect ? (_jsx("p", { className: "text-xs font-medium text-slate-600", children: activeWord ? (_jsxs(_Fragment, { children: ["\u0110ang t\u00ECm ", _jsx("strong", { children: activeWord.word }), ' — ', tappedPath.length === 0
|
|
190
|
+
? `bấm chữ cái đầu (${targetChars[0]}) hoặc cuối (${targetChars[targetChars.length - 1]}) trên lưới.`
|
|
191
|
+
: nextExpectedChar
|
|
192
|
+
? `tiếp theo bấm chữ ${nextExpectedChar}, hoặc bấm thẳng ô cuối của từ.`
|
|
193
|
+
: 'bấm ô cuối để hoàn thành từ.'] })) : (_jsx(_Fragment, { children: "Ch\u1ECDn m\u1ED9t t\u1EEB trong danh s\u00E1ch, r\u1ED3i b\u1EA5m t\u1EEBng ch\u1EEF tr\u00EAn l\u01B0\u1EDBi (ho\u1EB7c \u00F4 \u0111\u1EA7u r\u1ED3i \u00F4 cu\u1ED1i)." })) })) : null, isCategorize ? (_jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3", children: categories.map((cat) => {
|
|
151
194
|
const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
|
|
152
195
|
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
196
|
}) })) : (_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) => {
|
|
@@ -157,10 +200,10 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
|
|
|
157
200
|
const char = grid[rIdx]?.[cIdx] || '';
|
|
158
201
|
const highlight = cellColorMap[`${r}-${c}`];
|
|
159
202
|
const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
|
|
160
|
-
const
|
|
203
|
+
const isTapped = tappedPath.some((cell) => cell.row === r && cell.col === c);
|
|
161
204
|
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
205
|
? `${colorConfig.bg} shadow-sm`
|
|
163
|
-
:
|
|
206
|
+
: isTapped
|
|
164
207
|
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
165
208
|
: 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
|
|
166
209
|
});
|
|
@@ -70,7 +70,9 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
70
70
|
return {
|
|
71
71
|
type,
|
|
72
72
|
title: content.title || '',
|
|
73
|
-
instruction:
|
|
73
|
+
instruction: content.instruction
|
|
74
|
+
|| initialData?.instruction
|
|
75
|
+
|| 'Find these words in the wordsearch box.',
|
|
74
76
|
rows,
|
|
75
77
|
cols,
|
|
76
78
|
grid,
|
|
@@ -94,6 +96,7 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
94
96
|
content: {
|
|
95
97
|
type: next.type,
|
|
96
98
|
title: next.title,
|
|
99
|
+
instruction: next.instruction,
|
|
97
100
|
gridSize: { rows: next.rows, cols: next.cols },
|
|
98
101
|
grid: next.grid,
|
|
99
102
|
...(next.type === 'CATEGORIZE' ? { categories: next.categories } : {}),
|
|
@@ -239,6 +242,7 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
239
242
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(FindWordsInMatrixClient, { content: {
|
|
240
243
|
type: state.type,
|
|
241
244
|
title: state.title,
|
|
245
|
+
instruction: state.instruction,
|
|
242
246
|
gridSize: { rows: state.rows, cols: state.cols },
|
|
243
247
|
grid: state.grid,
|
|
244
248
|
...(state.type === 'CATEGORIZE' ? { categories: state.categories } : {}),
|
|
@@ -275,7 +279,7 @@ export function FindWordsInMatrixCreator({ initialData, onChange, onUnsavedChang
|
|
|
275
279
|
? 'border-blue-500 bg-blue-50/60 dark:bg-blue-950/30 text-blue-900 dark:text-blue-200 ring-2 ring-blue-400'
|
|
276
280
|
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-blue-100 dark:bg-blue-900 text-blue-600", children: _jsx(ListFilter, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "1. Danh s\u00E1ch t\u1EEB (Word Bank)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm c\u00E1c t\u1EEB c\u00F3 s\u1EB5n trong b\u1EA3ng ch\u1EEF c\u00E1i (VD: Gadgets)." })] })] }), _jsxs("button", { type: "button", onClick: () => updateState((p) => ({ ...p, type: 'CATEGORIZE' })), className: `flex items-center gap-3 p-3 rounded-xl border text-left transition-all ${state.type === 'CATEGORIZE'
|
|
277
281
|
? 'border-emerald-500 bg-emerald-50/60 dark:bg-emerald-950/30 text-emerald-900 dark:text-emerald-200 ring-2 ring-emerald-400'
|
|
278
|
-
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900 text-emerald-600", children: _jsx(Layers, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "2. T\u00ECm t\u1EEB & Ph\u00E2n nh\u00F3m (Categorize)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm t\u1EEB \u1EA9n v\u00E0 x\u1EBFp v\u00E0o c\u00E1c nh\u00F3m ti\u00EAu \u0111\u1EC1 (VD: Plants / Animals)." })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4 pt-2 border-t", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsx(Label, { className: "font-semibold", children: "Ti\u00EAu \u0111\u1EC1 b\u00E0i t\u1EADp" }), _jsx(Input, { value: state.title || '', onChange: (e) => updateState((p) => ({ ...p, title: e.target.value })), placeholder: state.type === 'CATEGORIZE' ? 'VD: Plants and Animals Word Search' : 'VD: Technology Gadgets Word Search', className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { className: "font-semibold", children: "K\u00EDch th\u01B0\u1EDBc ma tr\u1EADn (H\u00E0ng x C\u1ED9t)" }), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx(Input, { type: "number", min: 4, max: 25, value: state.rows, onChange: (e) => handleResize(Number(e.target.value), state.cols), className: "w-20 text-center font-bold" }), _jsx("span", { className: "font-bold text-gray-400", children: "\u00D7" }), _jsx(Input, { type: "number", min: 4, max: 25, value: state.cols, onChange: (e) => handleResize(state.rows, Number(e.target.value)), className: "w-20 text-center font-bold" })] })] })] })] }), state.type === 'CATEGORIZE' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white p-4 space-y-3 shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-bold text-sm text-emerald-900 dark:text-emerald-300 flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-4 w-4 text-emerald-600" }), "C\u00E1c nh\u00F3m danh m\u1EE5c (Categories / Headings)"] }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E1c c\u1ED9t ti\u00EAu \u0111\u1EC1 \u0111\u1EC3 h\u1ECDc sinh ph\u00E2n lo\u1EA1i t\u1EEB v\u00E0o (t\u1ED1i thi\u1EC3u 2 nh\u00F3m)." })] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: handleAddCategory, className: "gap-1 text-xs", children: [_jsx(FolderPlus, { className: "h-3.5 w-3.5" }), " Th\u00EAm nh\u00F3m"] })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3", children: state.categories.map((cat, idx) => (_jsxs("div", { className: "flex items-center gap-2 p-2.5 bg-white dark:bg-gray-900 rounded-lg border shadow-xs", children: [_jsx(Badge, { variant: "secondary", className: "font-mono text-xs", children: idx + 1 }), _jsx(Input, { value: cat.name, onChange: (e) => {
|
|
282
|
+
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 text-gray-700 dark:text-gray-300'}`, children: [_jsx("div", { className: "p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900 text-emerald-600", children: _jsx(Layers, { className: "h-5 w-5" }) }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-sm", children: "2. T\u00ECm t\u1EEB & Ph\u00E2n nh\u00F3m (Categorize)" }), _jsx("div", { className: "text-xs text-gray-500", children: "T\u00ECm t\u1EEB \u1EA9n v\u00E0 x\u1EBFp v\u00E0o c\u00E1c nh\u00F3m ti\u00EAu \u0111\u1EC1 (VD: Plants / Animals)." })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4 pt-2 border-t", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsx(Label, { className: "font-semibold", children: "Ti\u00EAu \u0111\u1EC1 b\u00E0i t\u1EADp" }), _jsx(Input, { value: state.title || '', onChange: (e) => updateState((p) => ({ ...p, title: e.target.value })), placeholder: state.type === 'CATEGORIZE' ? 'VD: Plants and Animals Word Search' : 'VD: Technology Gadgets Word Search', className: "mt-1" })] }), _jsxs("div", { className: "md:col-span-3", children: [_jsx(Label, { className: "font-semibold", children: "H\u01B0\u1EDBng d\u1EABn (Instruction)" }), _jsx(Input, { value: state.instruction || '', onChange: (e) => updateState((p) => ({ ...p, instruction: e.target.value })), placeholder: "\u2605 Find nine food items in the word search.", className: "mt-1" })] }), _jsxs("div", { children: [_jsx(Label, { className: "font-semibold", children: "K\u00EDch th\u01B0\u1EDBc ma tr\u1EADn (H\u00E0ng x C\u1ED9t)" }), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx(Input, { type: "number", min: 4, max: 25, value: state.rows, onChange: (e) => handleResize(Number(e.target.value), state.cols), className: "w-20 text-center font-bold" }), _jsx("span", { className: "font-bold text-gray-400", children: "\u00D7" }), _jsx(Input, { type: "number", min: 4, max: 25, value: state.cols, onChange: (e) => handleResize(state.rows, Number(e.target.value)), className: "w-20 text-center font-bold" })] })] })] })] }), state.type === 'CATEGORIZE' && (_jsxs(Card, { className: "overflow-hidden border-0 bg-white p-4 space-y-3 shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-bold text-sm text-emerald-900 dark:text-emerald-300 flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-4 w-4 text-emerald-600" }), "C\u00E1c nh\u00F3m danh m\u1EE5c (Categories / Headings)"] }), _jsx("p", { className: "text-xs text-gray-500", children: "T\u1EA1o c\u00E1c c\u1ED9t ti\u00EAu \u0111\u1EC1 \u0111\u1EC3 h\u1ECDc sinh ph\u00E2n lo\u1EA1i t\u1EEB v\u00E0o (t\u1ED1i thi\u1EC3u 2 nh\u00F3m)." })] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: handleAddCategory, className: "gap-1 text-xs", children: [_jsx(FolderPlus, { className: "h-3.5 w-3.5" }), " Th\u00EAm nh\u00F3m"] })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3", children: state.categories.map((cat, idx) => (_jsxs("div", { className: "flex items-center gap-2 p-2.5 bg-white dark:bg-gray-900 rounded-lg border shadow-xs", children: [_jsx(Badge, { variant: "secondary", className: "font-mono text-xs", children: idx + 1 }), _jsx(Input, { value: cat.name, onChange: (e) => {
|
|
279
283
|
const val = e.target.value;
|
|
280
284
|
updateState((p) => ({
|
|
281
285
|
...p,
|
|
@@ -4,11 +4,15 @@ export const transformFindWordsInMatrix = (question) => {
|
|
|
4
4
|
let content;
|
|
5
5
|
let correctAnswer;
|
|
6
6
|
const modeType = contentData?.type || (Array.isArray(contentData?.categories) && contentData.categories.length > 0 ? 'CATEGORIZE' : 'WORD_BANK');
|
|
7
|
+
const instruction = typeof contentData?.instruction === 'string' && contentData.instruction.trim()
|
|
8
|
+
? contentData.instruction.trim()
|
|
9
|
+
: undefined;
|
|
7
10
|
if (contentData?.gridSize && Array.isArray(contentData?.grid) && Array.isArray(contentData?.wordBank)) {
|
|
8
11
|
// Already in API shape
|
|
9
12
|
content = {
|
|
10
13
|
type: modeType,
|
|
11
14
|
title: contentData.title || '',
|
|
15
|
+
...(instruction ? { instruction } : {}),
|
|
12
16
|
gridSize: {
|
|
13
17
|
rows: Number(contentData.gridSize.rows) || 14,
|
|
14
18
|
cols: Number(contentData.gridSize.cols) || 14,
|
|
@@ -61,6 +65,7 @@ export const transformFindWordsInMatrix = (question) => {
|
|
|
61
65
|
content = {
|
|
62
66
|
type: modeType,
|
|
63
67
|
title: contentData.title || '',
|
|
68
|
+
...(instruction ? { instruction } : {}),
|
|
64
69
|
gridSize: { rows, cols },
|
|
65
70
|
grid,
|
|
66
71
|
...(modeType === 'CATEGORIZE' ? { categories } : {}),
|
|
@@ -5,6 +5,7 @@ import { cn } from '../../../../shared/lib/utils';
|
|
|
5
5
|
import { Input } from '../../../../components/ui/input';
|
|
6
6
|
import { Lightbulb } from 'lucide-react';
|
|
7
7
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
|
+
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
8
9
|
export function LabelThePictureClient({ questionData, userAnswer = {}, isReviewMode = false, explanation, onSubmit, }) {
|
|
9
10
|
const { imageUrl, wordBank, labels } = questionData;
|
|
10
11
|
// Initialize answers with pre-filled values
|
|
@@ -68,7 +69,7 @@ export function LabelThePictureClient({ questionData, userAnswer = {}, isReviewM
|
|
|
68
69
|
});
|
|
69
70
|
// Sort labels by number
|
|
70
71
|
const sortedLabels = [...labels].sort((a, b) => a.number - b.number);
|
|
71
|
-
return (_jsxs(
|
|
72
|
+
return (_jsxs(KidQuestionCard, { title: "Label the Picture", children: [_jsx(KidInstruction, { children: "Nh\u00ECn tranh v\u00E0 \u0111i\u1EC1n t\u1EEB v\u00E0o c\u00E1c v\u1ECB tr\u00ED t\u01B0\u01A1ng \u1EE9ng" }), _jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsx("div", { className: "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: "h-2.5 w-2.5 rounded-full bg-red-400" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-green-400" }), _jsx("span", { className: "ml-3 text-sm font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex justify-center bg-gradient-to-br from-slate-50 to-gray-100 p-4", children: _jsxs("div", { className: "relative max-h-[400px] w-full max-w-2xl overflow-hidden rounded-lg", children: [isLoadingUrl && (_jsx("div", { className: "flex h-64 items-center justify-center", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500" }) })), !isLoadingUrl && (previewUrl || imageUrl) ? (
|
|
72
73
|
/* eslint-disable-next-line @next/next/no-img-element */
|
|
73
74
|
_jsx("img", { src: previewUrl || imageUrl, alt: "Label the picture", className: "h-full w-full object-contain", onError: (e) => {
|
|
74
75
|
console.error('Failed to load image:', imageUrl, 'previewUrl:', previewUrl);
|
|
@@ -82,8 +83,8 @@ export function LabelThePictureClient({ questionData, userAnswer = {}, isReviewM
|
|
|
82
83
|
return (_jsxs("div", { className: cn('flex items-center gap-1.5 rounded-lg border-2 px-3 py-1.5 text-sm font-semibold transition-all', isUsedAsPreFilled
|
|
83
84
|
? 'border-blue-300 bg-blue-50 text-blue-700'
|
|
84
85
|
: isUsedByUser
|
|
85
|
-
? 'border-
|
|
86
|
-
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:shadow-sm'), children: [word, isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-blue-200 px-1 py-0.5 text-xs", children: "s\u1EB5n" })), isUsedByUser && !isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-
|
|
86
|
+
? 'border-blue-500 bg-blue-50 text-blue-800 ring-2 ring-blue-100'
|
|
87
|
+
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:shadow-sm'), children: [word, isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-blue-200 px-1 py-0.5 text-xs", children: "s\u1EB5n" })), isUsedByUser && !isUsedAsPreFilled && (_jsx("span", { className: "rounded bg-blue-200 px-1 py-0.5 text-xs", children: "\u2713" }))] }, index));
|
|
87
88
|
}) }) })] })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("svg", { className: "h-5 w-5 text-blue-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) }), _jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "\u0110i\u1EC1n t\u1EEB v\u00E0o c\u00E1c v\u1ECB tr\u00ED" })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: sortedLabels.map((label) => {
|
|
88
89
|
const userAnswerValue = answers[label.id] || '';
|
|
89
90
|
const correctAnswer = label.correctAnswer.trim().toLowerCase();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Play, Pause, Volume2, ImageIcon } from 'lucide-react';
|
|
5
5
|
import { Button } from '../../../../components/ui/button';
|
|
6
6
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
7
7
|
import { cn } from '../../../../shared/lib/utils';
|
|
8
|
+
import { KidChoiceRow, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
|
|
8
9
|
// ─── Sub-component: Audio player ─────────────────────────────────────────────
|
|
9
10
|
function QuestionAudioPlayer({ audioUrl }) {
|
|
10
11
|
const { previewUrl } = usePresignedFileUrl(audioUrl);
|
|
@@ -47,32 +48,12 @@ export function ListenAndChooseFromAnswerGroupClient({ questionData, onSubmit, i
|
|
|
47
48
|
const hasAnswered = !!selectedAnswer;
|
|
48
49
|
const selectedOption = questionData.options.find((o) => o.id === selectedAnswer);
|
|
49
50
|
const isAnswerCorrect = isReviewMode && selectedAnswer === resolvedCorrectAnswer;
|
|
50
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
? 'border-red-400 bg-red-50'
|
|
59
|
-
: 'border-gray-200 bg-gray-50'
|
|
60
|
-
: isSelected
|
|
61
|
-
? 'border-blue-500 bg-blue-50 shadow-md ring-2 ring-blue-100'
|
|
62
|
-
: 'border-gray-200 bg-white hover:border-gray-400 hover:shadow-sm cursor-pointer'), children: [_jsx("div", { className: cn('flex h-7 w-7 items-center justify-center rounded-lg text-sm font-bold shadow-sm', isReviewMode
|
|
63
|
-
? isCorrect
|
|
64
|
-
? 'bg-green-500 text-white'
|
|
65
|
-
: isWrong
|
|
66
|
-
? 'bg-red-500 text-white'
|
|
67
|
-
: 'bg-gray-200 text-gray-600'
|
|
68
|
-
: isSelected
|
|
69
|
-
? 'bg-blue-500 text-white'
|
|
70
|
-
: 'bg-gray-100 text-gray-600'), children: String.fromCharCode(65 + optIndex) }), _jsx(OptionImage, { imageUrl: option.imageUrl }), option.label && (_jsx("p", { className: cn('text-xs text-center font-medium', isReviewMode
|
|
71
|
-
? isCorrect ? 'text-green-700' : isWrong ? 'text-red-700' : 'text-gray-600'
|
|
72
|
-
: isSelected ? 'text-blue-700' : 'text-gray-600'), children: option.label })), isReviewMode && (isCorrect || isWrong) && (_jsx("div", { className: cn('absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full shadow-sm', isCorrect ? 'bg-green-500' : 'bg-red-500'), children: isCorrect ? (_jsx(Check, { className: "h-3.5 w-3.5 text-white" })) : (_jsx(X, { className: "h-3.5 w-3.5 text-white" })) })), !isReviewMode && isSelected && (_jsx("div", { className: "absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 shadow-sm", children: _jsx(Check, { className: "h-3.5 w-3.5 text-white" }) }))] }, option.id));
|
|
73
|
-
}) })] }), hasAnswered && selectedOption && (_jsxs("div", { className: cn('flex items-center gap-2 rounded-lg border p-3', isReviewMode
|
|
74
|
-
? isAnswerCorrect
|
|
75
|
-
? 'border-green-200 bg-green-50'
|
|
76
|
-
: 'border-red-200 bg-red-50'
|
|
77
|
-
: 'border-blue-200 bg-blue-50'), children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["\u0110\u00E3 ch\u1ECDn: ", _jsx("strong", { children: selectedOption.label || selectedOption.id })] }), isReviewMode && (_jsx("span", { className: cn('text-xs font-semibold', isAnswerCorrect ? 'text-green-600' : 'text-red-600'), children: isAnswerCorrect ? '✓ Chính xác' : '✗ Không chính xác' }))] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "H\u00E3y ch\u1ECDn m\u1ED9t \u0111\u00E1p \u00E1n" })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-lg border border-indigo-200 bg-indigo-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "mt-1 text-sm text-indigo-700", children: explanation })] }))] })] })] }));
|
|
51
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { icon: _jsx(Volume2, { className: "h-3.5 w-3.5" }), title: "Nghe v\u00E0 ch\u1ECDn trong nh\u00F3m \u0111\u00E1p \u00E1n", review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [questionData.audioUrl && (_jsx(QuestionAudioPlayer, { audioUrl: questionData.audioUrl })), _jsx("div", { className: "space-y-1.5", children: questionData.options.map((option, optIndex) => {
|
|
52
|
+
const { isSelected, isCorrect } = getOptionStatus(option.id);
|
|
53
|
+
return (_jsx(KidChoiceRow, { letter: optionLetter(optIndex), selected: isSelected, isReviewMode: isReviewMode, isCorrect: isCorrect, onClick: () => handleOptionSelect(option.id), children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(OptionImage, { imageUrl: option.imageUrl }), option.label ? _jsx("span", { children: option.label }) : null] }) }, option.id));
|
|
54
|
+
}) }), hasAnswered && selectedOption && (_jsxs("div", { className: cn('flex items-center gap-2 rounded-lg border p-3', isReviewMode
|
|
55
|
+
? isAnswerCorrect
|
|
56
|
+
? 'border-green-200 bg-green-50'
|
|
57
|
+
: 'border-red-200 bg-red-50'
|
|
58
|
+
: 'border-blue-200 bg-blue-50'), children: [_jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["\u0110\u00E3 ch\u1ECDn: ", _jsx("strong", { children: selectedOption.label || selectedOption.id })] }), isReviewMode && (_jsx("span", { className: cn('text-xs font-semibold', isAnswerCorrect ? 'text-green-600' : 'text-red-600'), children: isAnswerCorrect ? '✓ Chính xác' : '✗ Không chính xác' }))] })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-xs font-bold", children: "!" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "H\u00E3y ch\u1ECDn m\u1ED9t \u0111\u00E1p \u00E1n" })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-lg border border-indigo-200 bg-indigo-50 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch:" }), _jsx("p", { className: "mt-1 text-sm text-indigo-700", children: explanation })] }))] })] }));
|
|
78
59
|
}
|
package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Mic, Volume2, CheckCircle2, Play, Pause, Info } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
6
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
import { ArrowIndicatorSVG } from './ArrowIndicator';
|
|
7
8
|
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
8
9
|
function QuestionAudioPlayer({ audioUrl }) {
|
|
@@ -13,10 +14,10 @@ function QuestionAudioPlayer({ audioUrl }) {
|
|
|
13
14
|
// ─── Main Client Component ───────────────────────────────────────────────────
|
|
14
15
|
export function ListenAndSpeakAnswerClient({ questionData, isReviewMode = false, }) {
|
|
15
16
|
const { previewUrl: backgroundUrl } = usePresignedFileUrl(questionData.backgroundImageUrl || '');
|
|
16
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Nghe v\u00E0 tr\u1EA3 l\u1EDDi", icon: _jsx(Mic, { className: "h-3.5 w-3.5" }), children: [questionData.audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: questionData.audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), backgroundUrl && questionData.arrowNode && (_jsxs("div", { className: "relative w-full max-w-3xl mx-auto rounded-xl overflow-hidden shadow-sm border border-gray-200 bg-gray-50", children: [_jsx("img", { src: backgroundUrl, alt: "Background", className: "w-full object-contain pointer-events-none" }), _jsx("div", { className: "absolute", style: {
|
|
18
|
+
left: `${questionData.arrowNode.x}%`,
|
|
19
|
+
top: `${questionData.arrowNode.y}%`,
|
|
20
|
+
width: `${questionData.arrowNode.width}%`,
|
|
21
|
+
height: `${questionData.arrowNode.height}%`,
|
|
22
|
+
}, children: _jsx("div", { className: "flex h-full w-full items-center justify-center overflow-hidden p-[6%]", children: _jsx(ArrowIndicatorSVG, { idSuffix: "-client", animated: true, className: "w-full h-full" }) }) })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh s\u1EBD nh\u1EA5n n\u00FAt ghi \u00E2m v\u00E0 n\u00F3i c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y" })] }), isReviewMode && questionData.expectedAnswers && questionData.expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: questionData.expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && questionData.explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: questionData.explanation })] }))] })] }));
|
|
22
23
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Mic, Volume2, CheckCircle2, Play, Pause, ImageIcon, Info, GitCompareArrows } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
5
6
|
import { useCompareImagesGroup } from '../../_shared/hooks/useCompareImagesGroup';
|
|
6
7
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
7
8
|
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
@@ -28,5 +29,5 @@ export function ListenAndSpeakCompareImagesClient({ questionData, isReviewMode =
|
|
|
28
29
|
// fall back to store values (exam-taking mode populates the store)
|
|
29
30
|
const candidateImageUrl = questionData.candidateImageUrl || storeCandidateImageUrl;
|
|
30
31
|
const examinerImageUrl = questionData.examinerImageUrl || storeExaminerImageUrl;
|
|
31
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
32
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Nghe v\u00E0 n\u00F3i (So s\u00E1nh h\u00ECnh \u1EA3nh)", icon: _jsx(GitCompareArrows, { className: "h-3.5 w-3.5" }), children: [audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), (candidateImageUrl || examinerImageUrl) && (_jsxs("div", { className: "space-y-4 bg-gradient-to-br from-amber-100 to-orange-100 p-5 rounded-2xl border border-amber-200 shadow-sm", children: [candidateImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-amber-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-amber-300/50", children: "Candidate's Copy" }) }), _jsx("div", { className: "w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200", children: _jsx(CompareImagePreview, { imageUrl: candidateImageUrl, label: "Candidate's Copy" }) })] })), examinerImageUrl && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "flex justify-center", children: _jsx("span", { className: "inline-block rounded-lg border-2 border-orange-400 bg-gradient-to-r from-gray-600 to-gray-700 px-8 py-2 text-sm font-bold uppercase tracking-widest text-white shadow-lg ring-1 ring-orange-300/50", children: "Examiner's Copy" }) }), _jsx("div", { className: "w-full rounded-xl overflow-hidden shadow-md bg-white border border-amber-200", children: _jsx(CompareImagePreview, { imageUrl: examinerImageUrl, label: "Examiner's Copy" }) })] }))] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh s\u1EBD nh\u1EA5n n\u00FAt ghi \u00E2m v\u00E0 n\u00F3i c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y" })] }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: explanation })] }))] })] }));
|
|
32
33
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Mic, Volume2, CheckCircle2, Images, Play, Pause, ImageIcon, Info } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
6
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
import { cn } from '../../../../shared/lib/utils';
|
|
7
8
|
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
8
9
|
function QuestionAudioPlayer({ audioUrl }) {
|
|
@@ -18,17 +19,17 @@ function ImageWithPresigned({ url, alt }) {
|
|
|
18
19
|
// ─── Main Client Component ───────────────────────────────────────────────────
|
|
19
20
|
export function ListenAndSpeakImageGroupClient({ questionData, isReviewMode = false, }) {
|
|
20
21
|
const { audioUrl, activeImageId, groupImages = [], expectedAnswers = [], explanation } = questionData;
|
|
21
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Nghe v\u00E0 n\u00F3i (Nh\u00F3m h\u00ECnh \u1EA3nh)", icon: _jsx(Images, { className: "h-3.5 w-3.5" }), children: [audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), groupImages.length > 0 ? (_jsx("div", { className: "rounded-xl border border-gray-100 bg-gradient-to-b from-gray-50/80 to-white p-5", children: _jsx("div", { className: "flex flex-row justify-center gap-4 sm:gap-5 flex-wrap", children: groupImages.map((img, index) => {
|
|
23
|
+
const isActive = img.id === activeImageId;
|
|
24
|
+
const letter = String.fromCharCode(65 + index);
|
|
25
|
+
return (_jsxs("div", { className: cn('flex flex-col items-center gap-2 transition-transform duration-300', isActive && 'scale-[1.03]'), children: [_jsxs("div", { className: cn('relative w-28 h-28 sm:w-36 sm:h-36 rounded-xl overflow-hidden flex items-center justify-center bg-white transition-all duration-300', isActive
|
|
26
|
+
? 'border-[3px] border-blue-400 shadow-lg shadow-blue-100/70 ring-2 ring-blue-200/40'
|
|
27
|
+
: 'border-2 border-gray-200 shadow-sm hover:shadow-md hover:border-gray-300'), children: [img.url ? (_jsx(ImageWithPresigned, { url: img.url, alt: img.label || `Image ${index + 1}` })) : (_jsx("div", { className: "w-full h-full bg-gray-50 flex items-center justify-center text-gray-300", children: _jsx(ImageIcon, { className: "h-10 w-10" }) })), _jsx("div", { className: cn('absolute top-1.5 left-1.5 flex h-6 w-6 items-center justify-center rounded-lg text-xs font-bold shadow-sm', isActive
|
|
28
|
+
? 'bg-blue-500 text-white'
|
|
29
|
+
: 'bg-white/90 text-gray-500 border border-gray-200'), children: letter }), isActive && (_jsx("div", { className: "absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-blue-500 flex items-center justify-center shadow-sm", children: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-white" }) }))] }), _jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [_jsx("span", { className: cn('text-sm font-semibold px-2.5 py-0.5 rounded-md transition-colors', isActive
|
|
30
|
+
? 'text-blue-700 bg-blue-50'
|
|
31
|
+
: 'text-gray-600'), children: img.label || `Hình ${index + 1}` }), isActive && (_jsx("span", { className: "text-[10px] font-medium text-blue-500 animate-pulse", children: "\u0110ang h\u1ECFi" }))] })] }, img.id));
|
|
32
|
+
}) }) })) : (
|
|
33
|
+
/* Empty state for no images */
|
|
34
|
+
_jsxs("div", { className: "flex flex-col items-center justify-center py-10 px-4 rounded-xl border-2 border-dashed border-gray-200 bg-gray-50/50", children: [_jsx(ImageIcon, { className: "h-12 w-12 text-gray-300 mb-3" }), _jsx("p", { className: "text-sm font-medium text-gray-500", children: "Ch\u01B0a c\u00F3 h\u00ECnh \u1EA3nh n\u00E0o" }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "H\u00ECnh \u1EA3nh s\u1EBD hi\u1EC3n th\u1ECB t\u1EA1i \u0111\u00E2y" })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh s\u1EBD nh\u1EA5n n\u00FAt ghi \u00E2m v\u00E0 n\u00F3i c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y" })] }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: explanation })] }))] })] }));
|
|
34
35
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Volume2, Mic, CheckCircle2, Play, Pause, Info, ArrowLeftRight } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
6
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
6
7
|
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
7
8
|
function QuestionAudioPlayer({ audioUrl }) {
|
|
8
9
|
const { previewUrl } = usePresignedFileUrl(audioUrl);
|
|
@@ -32,7 +33,7 @@ export function ListenAndSpeakInfoExchangeClient({ questionData, isReviewMode =
|
|
|
32
33
|
.map((f) => ({ key: f.key, value: f.answer }));
|
|
33
34
|
const examinerRows = (qData.examinerFields || [])
|
|
34
35
|
.map((f) => ({ key: f.key, value: f.answer }));
|
|
35
|
-
return (_jsx("div", { className: "space-y-4", children: _jsxs(
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
return (_jsx("div", { className: "space-y-4", children: _jsxs(KidQuestionCard, { title: `Nghe và nói (Trao đổi thông tin — ${viewMode === 'ANSWER' ? 'Trả lời' : 'Đặt câu hỏi'})`, icon: _jsx(ArrowLeftRight, { className: "h-3.5 w-3.5" }), children: [audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), _jsxs("div", { className: "flex flex-col lg:flex-row gap-4", children: [imageUrl && (_jsx("div", { className: "lg:w-1/2", children: _jsx(ImagePreview, { imageUrl: imageUrl, title: title }) })), _jsxs("div", { className: "flex-1 space-y-4", children: [_jsx(InfoTable, { label: "CANDIDATE'S COPY", title: title, rows: candidateRows, headerColor: "bg-teal-600" }), _jsx(InfoTable, { label: "EXAMINER'S COPY", title: title, rows: examinerRows, headerColor: "bg-amber-600" })] })] }), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: viewMode === 'ANSWER'
|
|
37
|
+
? 'Học sinh sẽ nhấn nút ghi âm và nói câu trả lời tại đây'
|
|
38
|
+
: 'Học sinh sẽ nhấn nút ghi âm và đặt câu hỏi tại đây' })] }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: viewMode === 'ANSWER' ? 'Các đáp án mong muốn' : 'Các câu hỏi mong muốn' })] }), _jsx("div", { className: "space-y-2", children: expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: explanation })] }))] }) }));
|
|
38
39
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { Mic, Volume2, CheckCircle2, Play, Pause, ImageIcon, Info, CircleOff } from 'lucide-react';
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
5
6
|
import { usePresignedFileUrl, useAudioPreview } from '../../../../shared/lib/hooks';
|
|
6
7
|
// ─── Sub-component: Custom Audio Player ──────────────────────────────────────
|
|
7
8
|
function QuestionAudioPlayer({ audioUrl }) {
|
|
@@ -20,7 +21,7 @@ function OddOneOutImage({ imageUrl, index }) {
|
|
|
20
21
|
// ─── Main Client Component ───────────────────────────────────────────────────
|
|
21
22
|
export function ListenAndSpeakOddOneOutClient({ questionData, isReviewMode = false, }) {
|
|
22
23
|
const { audioUrl, images, oddOneOutIndex, expectedAnswers = [], explanation } = questionData;
|
|
23
|
-
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return (_jsxs("div", { className: "space-y-4", children: [questionData.isBackup && (_jsxs("div", { className: "inline-flex self-center items-center gap-1.5 px-3 py-1.5 rounded-md bg-amber-50 text-amber-700 border border-amber-200 text-xs font-bold shadow-sm", children: [_jsx("span", { children: "\uD83D\uDD04" }), " C\u00E2u h\u1ECFi d\u1EF1 ph\u00F2ng"] })), _jsxs(KidQuestionCard, { title: "Nghe v\u00E0 n\u00F3i (T\u00ECm h\u00ECnh kh\u00E1c lo\u1EA1i)", icon: _jsx(CircleOff, { className: "h-3.5 w-3.5" }), children: [images && images.length > 0 && (_jsx("div", { className: "bg-gradient-to-br from-amber-50/80 to-orange-50/60 rounded-2xl p-6 border border-amber-100/80 shadow-sm space-y-4", children: images.length === 4 ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "grid grid-cols-3 gap-4", children: images.slice(0, 3).map((imgUrl, index) => (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1.5 mb-1", children: [_jsx("span", { className: `flex h-6 w-6 items-center justify-center rounded-md text-xs font-bold ${isReviewMode && oddOneOutIndex === index ? 'bg-red-200 text-red-800' : 'bg-amber-200 text-amber-800'}`, children: index + 1 }), isReviewMode && oddOneOutIndex === index && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-red-100 border border-red-200 px-1.5 py-0.5 text-[10px] font-bold text-red-700", children: [_jsx(CircleOff, { className: "h-2.5 w-2.5" }), "Kh\u00E1c lo\u1EA1i"] }))] }), _jsx("div", { className: `relative aspect-square rounded-xl overflow-hidden border-2 bg-white shadow-sm ${isReviewMode && oddOneOutIndex === index ? 'border-red-400 ring-2 ring-red-200' : 'border-gray-200'}`, children: imgUrl ? (_jsx(OddOneOutImage, { imageUrl: imgUrl, index: index })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center text-gray-400 text-sm", children: _jsx(ImageIcon, { className: "h-6 w-6" }) })) })] }, index))) }), _jsx("div", { className: "flex justify-center", children: _jsx("div", { className: "w-1/3", children: _jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1.5 mb-1", children: [_jsx("span", { className: `flex h-6 w-6 items-center justify-center rounded-md text-xs font-bold ${isReviewMode && oddOneOutIndex === 3 ? 'bg-red-200 text-red-800' : 'bg-amber-200 text-amber-800'}`, children: "4" }), isReviewMode && oddOneOutIndex === 3 && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-red-100 border border-red-200 px-1.5 py-0.5 text-[10px] font-bold text-red-700", children: [_jsx(CircleOff, { className: "h-2.5 w-2.5" }), "Kh\u00E1c lo\u1EA1i"] }))] }), _jsx("div", { className: `relative aspect-square rounded-xl overflow-hidden border-2 bg-white shadow-sm ${isReviewMode && oddOneOutIndex === 3 ? 'border-red-400 ring-2 ring-red-200' : 'border-gray-200'}`, children: images[3] ? (_jsx(OddOneOutImage, { imageUrl: images[3], index: 3 })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center text-gray-400 text-sm", children: _jsx(ImageIcon, { className: "h-6 w-6" }) })) })] }) }) })] })) : (
|
|
25
|
+
/* Fallback: equal grid for other counts */
|
|
26
|
+
_jsx("div", { className: "grid grid-cols-2 gap-4", children: images.map((imgUrl, index) => (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1.5 mb-1", children: [_jsx("span", { className: `flex h-6 w-6 items-center justify-center rounded-md text-xs font-bold ${isReviewMode && oddOneOutIndex === index ? 'bg-red-200 text-red-800' : 'bg-amber-200 text-amber-800'}`, children: index + 1 }), isReviewMode && oddOneOutIndex === index && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-red-100 border border-red-200 px-1.5 py-0.5 text-[10px] font-bold text-red-700", children: [_jsx(CircleOff, { className: "h-2.5 w-2.5" }), "Kh\u00E1c lo\u1EA1i"] }))] }), _jsx("div", { className: `relative aspect-square rounded-xl overflow-hidden border-2 bg-white shadow-sm ${isReviewMode && oddOneOutIndex === index ? 'border-red-400 ring-2 ring-red-200' : 'border-gray-200'}`, children: imgUrl ? (_jsx(OddOneOutImage, { imageUrl: imgUrl, index: index })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center text-gray-400 text-sm", children: _jsx(ImageIcon, { className: "h-6 w-6" }) })) })] }, index))) })) })), audioUrl ? (_jsx(QuestionAudioPlayer, { audioUrl: audioUrl })) : (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3", children: [_jsx(Volume2, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 audio c\u00E2u h\u1ECFi" })] })), _jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50/80 px-4 py-3", children: [_jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-indigo-50 ring-2 ring-indigo-100/50", children: _jsx(Mic, { className: "w-4 h-4 text-indigo-400" }) }), _jsx("p", { className: "text-xs text-gray-400", children: "H\u1ECDc sinh s\u1EBD nh\u1EA5n n\u00FAt ghi \u00E2m v\u00E0 n\u00F3i c\u00E2u tr\u1EA3 l\u1EDDi t\u1EA1i \u0111\u00E2y" })] }), isReviewMode && expectedAnswers.length > 0 && (_jsxs("div", { className: "rounded-xl border border-green-200 bg-green-50/80 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(CheckCircle2, { className: "h-4 w-4 text-green-600" }), _jsx("h4", { className: "text-sm font-semibold text-green-800", children: "C\u00E1c \u0111\u00E1p \u00E1n mong mu\u1ED1n" })] }), _jsx("div", { className: "space-y-2", children: expectedAnswers.map((answer, index) => (_jsxs("div", { className: "flex items-center gap-2.5 rounded-lg bg-white/70 border border-green-100 px-3 py-2", children: [_jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 text-xs font-bold", children: index + 1 }), _jsx("span", { className: "text-sm text-green-800", children: answer })] }, index))) })] })), isReviewMode && explanation && (_jsxs("div", { className: "rounded-xl border border-indigo-200 bg-indigo-50 p-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Info, { className: "h-4 w-4 text-indigo-600" }), _jsx("p", { className: "text-sm font-semibold text-indigo-800", children: "Gi\u1EA3i th\u00EDch" })] }), _jsx("p", { className: "text-sm text-indigo-700 leading-relaxed", children: explanation })] }))] })] }));
|
|
26
27
|
}
|