@tinyweb_dev/oe-exam-sdk 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
  2. package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
  3. package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
  4. package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
  5. package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
  6. package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
  7. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
  8. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -15
  9. package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +2 -0
  10. package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
  11. package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
  12. package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +18 -20
  13. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
  14. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +75 -11
  15. package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
  16. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
  17. package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
  18. package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
  19. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
  20. package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +54 -3
  21. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +1 -1
  22. package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +15 -5
  23. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
  24. package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +144 -45
  25. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
  26. package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +26 -4
  27. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
  28. package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +13 -3
  29. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
  30. package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
  31. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
  32. package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
  33. package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
  34. package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +15 -2
  35. package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
  36. package/dist/components/questions/viewer/QuestionViewer.js +22 -9
  37. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
  38. package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
  39. package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
  40. package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
  41. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
  42. package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
  43. package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
  44. package/dist/shared/lib/utils/question-reverse-transform.js +8 -0
  45. package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
  46. package/dist/shared/types/questions/answer-the-question-group.js +5 -0
  47. package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
  48. package/package.json +1 -1
@@ -28,6 +28,16 @@ export function createEmptyOption(existingIds) {
28
28
  text: '',
29
29
  };
30
30
  }
31
+ export function normalizeExpectedAnswers(value) {
32
+ if (!Array.isArray(value)) {
33
+ return [''];
34
+ }
35
+ const filled = value
36
+ .filter((item) => typeof item === 'string')
37
+ .map((item) => item.trim())
38
+ .filter((item) => item !== '');
39
+ return filled.length > 0 ? filled : [''];
40
+ }
31
41
  export function createEmptyItem(questionNumber) {
32
42
  const first = createEmptyOption([]);
33
43
  const second = createEmptyOption([first.id]);
@@ -35,6 +45,7 @@ export function createEmptyItem(questionNumber) {
35
45
  question: '',
36
46
  options: [first, second],
37
47
  optionId: first.id,
48
+ expectedAnswers: [''],
38
49
  isExample: false,
39
50
  points: 1,
40
51
  questionNumber,
@@ -42,8 +53,11 @@ export function createEmptyItem(questionNumber) {
42
53
  }
43
54
  export function parseItemAnswer(raw) {
44
55
  if (!isRecord(raw))
45
- return { optionId: '' };
46
- return { optionId: asString(raw.optionId) };
56
+ return { optionId: '', expectedAnswers: [''] };
57
+ return {
58
+ optionId: asString(raw.optionId),
59
+ expectedAnswers: normalizeExpectedAnswers(raw.expectedAnswers),
60
+ };
47
61
  }
48
62
  export function parseStudentAnswer(raw) {
49
63
  if (!isRecord(raw))
@@ -78,6 +92,7 @@ export function mapApiItem(item, index, fallbackAnswer) {
78
92
  question: asString(content.question) || asString(record.question),
79
93
  options,
80
94
  optionId: parsed.optionId || options[0]?.id || '',
95
+ expectedAnswers: parsed.expectedAnswers,
81
96
  isExample,
82
97
  points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
83
98
  questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
@@ -125,3 +140,25 @@ export function mapQuestionToChooseThenAnswerGroupData(question) {
125
140
  points: 1,
126
141
  };
127
142
  }
143
+ export function toCtaItemResults(details, items, userAnswers) {
144
+ if (!details?.length) {
145
+ return undefined;
146
+ }
147
+ const byId = new Map(details.map((detail) => [detail.itemId, detail]));
148
+ return items.map((item, index) => {
149
+ const detail = byId.get(`item-${index}`);
150
+ const mappingCorrect = Boolean(item.optionId) && userAnswers[index]?.optionId === item.optionId;
151
+ const half = (typeof item.points === 'number' ? item.points : 1) * 0.5;
152
+ let aiCorrect;
153
+ if (detail?.partialScore != null) {
154
+ const mappingHalf = mappingCorrect ? half : 0;
155
+ aiCorrect = detail.partialScore - mappingHalf > 1e-9;
156
+ }
157
+ return {
158
+ mappingCorrect,
159
+ aiCorrect,
160
+ partialScore: detail?.partialScore,
161
+ feedback: detail?.feedback,
162
+ };
163
+ });
164
+ }
@@ -1,5 +1,8 @@
1
1
  import { CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION, CHOOSE_THEN_ANSWER_ITEM_TYPE, } from '../../_shared/types/choose-then-answer-group.type';
2
2
  import { sumGradablePoints } from './map-choose-then-answer-group-data';
3
+ function filledExpectedAnswers(values) {
4
+ return (values || []).map((value) => value.trim()).filter((value) => value !== '');
5
+ }
3
6
  export const transformChooseThenAnswerGroup = (question) => {
4
7
  const answerData = question.answer;
5
8
  const items = Array.isArray(answerData?.items) ? answerData.items : [];
@@ -7,25 +10,35 @@ export const transformChooseThenAnswerGroup = (question) => {
7
10
  return { apiContent: { items: [] }, apiCorrectAnswer: { answer: [] } };
8
11
  }
9
12
  const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
10
- const apiItems = items.map((item, index) => ({
11
- questionType: CHOOSE_THEN_ANSWER_ITEM_TYPE,
12
- ...(item.isExample ? { isExample: true } : {}),
13
- content: {
14
- question: item.question || '',
15
- options: (item.options || []).map((option) => ({
16
- id: option.id || '',
17
- text: option.text || '',
18
- })),
19
- },
20
- correctAnswer: { answer: { optionId: item.optionId || '' } },
21
- points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
22
- questionNumber: item.questionNumber || index + 1,
23
- }));
13
+ const apiItems = items.map((item, index) => {
14
+ const expectedAnswers = filledExpectedAnswers(item.expectedAnswers);
15
+ return {
16
+ questionType: CHOOSE_THEN_ANSWER_ITEM_TYPE,
17
+ ...(item.isExample ? { isExample: true } : {}),
18
+ content: {
19
+ question: item.question || '',
20
+ options: (item.options || []).map((option) => ({
21
+ id: option.id || '',
22
+ text: option.text || '',
23
+ })),
24
+ },
25
+ correctAnswer: {
26
+ answer: {
27
+ optionId: item.optionId || '',
28
+ ...(expectedAnswers.length > 0 ? { expectedAnswers } : {}),
29
+ },
30
+ },
31
+ points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
32
+ questionNumber: item.questionNumber || index + 1,
33
+ };
34
+ });
24
35
  return {
25
36
  apiContent: {
26
37
  meta: {
27
38
  instruction: answerData?.instruction || CHOOSE_THEN_ANSWER_GROUP_DEFAULT_INSTRUCTION,
28
39
  ...(title ? { title } : {}),
40
+ gradingType: 'AI',
41
+ isAiGraded: true,
29
42
  },
30
43
  items: apiItems,
31
44
  },
@@ -1,2 +1,2 @@
1
1
  import type { CrossOutWordGroupClientProps } from '../../_shared/types/cross-out-word-group.type';
2
- export declare function CrossOutWordGroupClient({ questionData, isReviewMode, userAnswers, }: CrossOutWordGroupClientProps): import("react").JSX.Element;
2
+ export declare function CrossOutWordGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: CrossOutWordGroupClientProps): import("react").JSX.Element;
@@ -2,6 +2,12 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { CircleHelp, Lightbulb, Star } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
+ function tokenize(sentence) {
6
+ return sentence.trim().split(/\s+/).filter(Boolean);
7
+ }
8
+ function normalizeWord(word) {
9
+ return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
10
+ }
5
11
  function formatAnswer(answer) {
6
12
  if (!answer)
7
13
  return '—';
@@ -15,13 +21,58 @@ function formatAnswer(answer) {
15
21
  return 'câu đúng (không gạch)';
16
22
  return `gạch "${answer.crossOut}"${posInfo}`;
17
23
  }
18
- export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
24
+ export function CrossOutWordGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
19
25
  const items = questionData.items || [];
26
+ const emit = (itemIndex, nextItem) => {
27
+ const item = items[itemIndex];
28
+ if (isReviewMode || item?.isExample || !onAnswerChange) {
29
+ return;
30
+ }
31
+ const next = items.map((_, index) => userAnswers[index] ?? null);
32
+ next[itemIndex] = nextItem;
33
+ onAnswerChange(next);
34
+ };
20
35
  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) => {
21
- const userValue = userAnswers[itemIndex];
22
36
  const isReplace = item.type === 'CROSS_OUT_AND_REPLACE';
37
+ const userValue = item.isExample ? item.correctAnswer : userAnswers[itemIndex];
38
+ const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
39
+ const tokens = tokenize(item.question || '');
40
+ const selectedIndex = userValue?.wordIndex;
41
+ const selectedWord = userValue?.crossOut || '';
42
+ const replaceWith = userValue?.replaceWith || '';
43
+ const correct = item.correctAnswer;
23
44
  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"] })), _jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
24
45
  ? 'bg-violet-100 text-violet-700'
25
- : 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })] }), _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' }), _jsxs("div", { className: "rounded-lg border border-gray-100 bg-gray-50 px-3 py-2 text-sm text-gray-700", children: [_jsx("span", { className: "font-medium text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), formatAnswer(item.correctAnswer)] }), isReviewMode && (_jsxs("div", { className: "rounded-lg border border-blue-100 bg-blue-50 px-3 py-2 text-sm text-blue-800", children: [_jsx("span", { className: "font-medium", children: "H\u1ECDc sinh: " }), formatAnswer(userValue)] }))] })] }, `${item.questionNumber}-${itemIndex}`));
46
+ : 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [isReplace ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs font-semibold text-slate-600", children: "T\u1EEB \u0111\u00FAng:" }), _jsx("input", { type: "text", value: replaceWith, disabled: !canSelect, onChange: (event) => emit(itemIndex, {
47
+ type: item.type,
48
+ wordIndex: selectedIndex ?? null,
49
+ crossOut: selectedWord,
50
+ replaceWith: event.target.value,
51
+ }), placeholder: "Nh\u1EADp t\u1EEB thay th\u1EBF...", className: "h-8 min-w-[10rem] rounded-lg border border-slate-200 px-2.5 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 disabled:bg-slate-50" })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-sm font-medium leading-relaxed text-slate-800", children: tokens.map((token, tokenIndex) => {
52
+ const plain = normalizeWord(token);
53
+ const isSelected = typeof selectedIndex === 'number'
54
+ ? selectedIndex === tokenIndex
55
+ : Boolean(selectedWord) && normalizeWord(selectedWord) === plain && plain !== '';
56
+ const isCorrectTarget = isReviewMode && !item.isExample && correct?.wordIndex === tokenIndex;
57
+ return (_jsx("button", { type: "button", disabled: !canSelect || plain === '', onClick: () => {
58
+ const already = typeof selectedIndex === 'number'
59
+ ? selectedIndex === tokenIndex
60
+ : selectedWord === token;
61
+ emit(itemIndex, {
62
+ type: item.type,
63
+ wordIndex: already ? null : tokenIndex,
64
+ crossOut: already
65
+ ? ''
66
+ : token.replace(/[^\p{L}\p{N}'-]/gu, '') || token,
67
+ ...(isReplace ? { replaceWith } : {}),
68
+ });
69
+ }, className: cn('rounded-lg px-2 py-1 transition-colors', isSelected
70
+ ? 'border border-blue-200 bg-blue-100 font-bold text-blue-700 line-through decoration-blue-600'
71
+ : isCorrectTarget
72
+ ? 'border-2 border-dashed border-emerald-500 bg-emerald-50 font-bold text-emerald-800'
73
+ : canSelect
74
+ ? 'cursor-pointer hover:bg-blue-50 hover:text-blue-700'
75
+ : 'cursor-default'), children: token }, `${item.questionNumber}-token-${tokenIndex}`));
76
+ }) }), isReviewMode ? (_jsxs("div", { className: "rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800", children: [_jsx("span", { className: "font-medium", children: "\u0110\u00E1p \u00E1n: " }), formatAnswer(item.correctAnswer)] })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
26
77
  }), 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] }))] }));
27
78
  }
@@ -7,5 +7,5 @@ interface CrosswordPuzzleClientProps {
7
7
  userAnswers?: Record<string, string>;
8
8
  onAnswerChange?: (key: string, value: string) => void;
9
9
  }
10
- export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, }: CrosswordPuzzleClientProps): React.JSX.Element;
10
+ export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, onAnswerChange, }: CrosswordPuzzleClientProps): React.JSX.Element;
11
11
  export {};
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useMemo } from 'react';
4
- export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, }) {
4
+ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, onAnswerChange, }) {
5
5
  const rows = content?.gridSize?.rows || 12;
6
6
  const cols = content?.gridSize?.cols || 12;
7
7
  const acrossClues = useMemo(() => content?.clues?.across || [], [content?.clues?.across]);
@@ -33,11 +33,11 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
33
33
  return (_jsxs("div", { className: "flex flex-col gap-3 p-3 bg-white rounded-xl border border-gray-200 shadow-sm", children: [content?.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 text-center", children: content.title })), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("div", { className: "lg:col-span-5 flex flex-col gap-4", children: [_jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-semibold", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: acrossClues.map((c) => {
34
34
  const answer = correctAnswer?.answers?.[`across-${c.number}`];
35
35
  const userAns = userAnswers[`across-${c.number}`];
36
- return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-blue-100", children: [_jsxs("span", { className: "font-bold text-blue-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-gray-800", children: c.clue }), isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `across-${c.number}`));
36
+ return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-blue-100", children: [_jsxs("span", { className: "font-bold text-blue-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-gray-800", children: c.clue }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`across-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-blue-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-blue-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `across-${c.number}`));
37
37
  }) })] }), _jsxs("div", { className: "rounded-lg border border-purple-200 bg-purple-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-purple-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-purple-600 text-white text-xs font-semibold", children: "Down" }), _jsx("span", { children: "H\u00E0ng d\u1ECDc" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: downClues.map((c) => {
38
38
  const answer = correctAnswer?.answers?.[`down-${c.number}`];
39
39
  const userAns = userAnswers[`down-${c.number}`];
40
- return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-purple-100", children: [_jsxs("span", { className: "font-bold text-purple-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-gray-800", children: c.clue }), isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `down-${c.number}`));
40
+ return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-purple-100", children: [_jsxs("span", { className: "font-bold text-purple-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-gray-800", children: c.clue }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`down-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-purple-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-purple-500" })) : null, isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `down-${c.number}`));
41
41
  }) })] })] }), _jsx("div", { className: "lg:col-span-7 flex justify-center overflow-x-auto p-4 bg-gray-50 rounded-xl border border-gray-200", children: _jsx("div", { className: "inline-grid gap-1", style: {
42
42
  gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
43
43
  }, children: Array.from({ length: rows }).map((_, rIdx) => {
@@ -66,8 +66,18 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
66
66
  previewLetter = word[offset] || '';
67
67
  }
68
68
  }
69
- else if (userAnswers?.[key]) {
70
- previewLetter = userAnswers[key];
69
+ else {
70
+ if (cellInfo.across) {
71
+ const word = userAnswers[`across-${cellInfo.across.number}`] || '';
72
+ previewLetter = word[c - cellInfo.across.startCol] || '';
73
+ }
74
+ if (!previewLetter && cellInfo.down) {
75
+ const word = userAnswers[`down-${cellInfo.down.number}`] || '';
76
+ previewLetter = word[r - cellInfo.down.startRow] || '';
77
+ }
78
+ if (!previewLetter) {
79
+ previewLetter = userAnswers[key] || '';
80
+ }
71
81
  }
72
82
  const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
73
83
  let textColor = isExample
@@ -1,9 +1,19 @@
1
1
  import React from 'react';
2
- import type { FindWordsInMatrixContent, FindWordsInMatrixCorrectAnswer } from '../../../../shared/types/questions/find-words-in-matrix';
2
+ import type { FindWordsInMatrixContent, FindWordsInMatrixCorrectAnswer, MatrixCellCoord } from '../../../../shared/types/questions/find-words-in-matrix';
3
+ type WordSelection = {
4
+ wordId?: string;
5
+ word?: string;
6
+ categoryId?: string;
7
+ path: MatrixCellCoord[];
8
+ };
3
9
  interface FindWordsInMatrixClientProps {
4
10
  content: FindWordsInMatrixContent;
5
11
  correctAnswer?: FindWordsInMatrixCorrectAnswer;
6
12
  showSolution?: boolean;
13
+ userAnswer?: unknown;
14
+ onAnswerChange?: (payload: {
15
+ words: WordSelection[];
16
+ }) => void;
7
17
  }
8
- export declare function FindWordsInMatrixClient({ content, correctAnswer, showSolution, }: FindWordsInMatrixClientProps): React.JSX.Element;
18
+ export declare function FindWordsInMatrixClient({ content, correctAnswer, showSolution, userAnswer, onAnswerChange, }: FindWordsInMatrixClientProps): React.JSX.Element;
9
19
  export {};
@@ -1,69 +1,168 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from 'react';
3
4
  import { Card } from '../../../../components/ui/card';
4
5
  import { Badge } from '../../../../components/ui/badge';
5
- // 10 Distinct pastel highlighter colors for word selections
6
6
  const WORD_COLORS = [
7
- { bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white', stroke: '#f59e0b' },
8
- { bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white', stroke: '#10b981' },
9
- { bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white', stroke: '#0ea5e9' },
10
- { bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white', stroke: '#a855f7' },
11
- { bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white', stroke: '#f43f5e' },
12
- { bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white', stroke: '#6366f1' },
13
- { bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white', stroke: '#14b8a6' },
14
- { bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white', stroke: '#eab308' },
15
- { bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white', stroke: '#ec4899' },
16
- { bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white', stroke: '#06b6d4' },
7
+ { bg: 'bg-amber-100 text-amber-900 border-amber-400', badge: 'bg-amber-500 text-white' },
8
+ { bg: 'bg-emerald-100 text-emerald-900 border-emerald-400', badge: 'bg-emerald-500 text-white' },
9
+ { bg: 'bg-sky-100 text-sky-900 border-sky-400', badge: 'bg-sky-500 text-white' },
10
+ { bg: 'bg-purple-100 text-purple-900 border-purple-400', badge: 'bg-purple-500 text-white' },
11
+ { bg: 'bg-rose-100 text-rose-900 border-rose-400', badge: 'bg-rose-500 text-white' },
12
+ { bg: 'bg-indigo-100 text-indigo-900 border-indigo-400', badge: 'bg-indigo-500 text-white' },
13
+ { bg: 'bg-teal-100 text-teal-900 border-teal-400', badge: 'bg-teal-500 text-white' },
14
+ { bg: 'bg-yellow-100 text-yellow-900 border-yellow-400', badge: 'bg-yellow-500 text-white' },
15
+ { bg: 'bg-pink-100 text-pink-900 border-pink-400', badge: 'bg-pink-500 text-white' },
16
+ { bg: 'bg-cyan-100 text-cyan-900 border-cyan-400', badge: 'bg-cyan-500 text-white' },
17
17
  ];
18
- export function FindWordsInMatrixClient({ content, correctAnswer, showSolution = false, }) {
18
+ function cleanWord(value) {
19
+ return (value || '').replace(/[\s\-_]/g, '').toUpperCase();
20
+ }
21
+ function getStraightLinePath(start, end) {
22
+ const dr = end.row - start.row;
23
+ const dc = end.col - start.col;
24
+ const stepR = dr === 0 ? 0 : dr > 0 ? 1 : -1;
25
+ const stepC = dc === 0 ? 0 : dc > 0 ? 1 : -1;
26
+ if (dr === 0 && dc !== 0) {
27
+ return Array.from({ length: Math.abs(dc) + 1 }, (_, i) => ({
28
+ row: start.row,
29
+ col: start.col + i * stepC,
30
+ }));
31
+ }
32
+ if (dc === 0 && dr !== 0) {
33
+ return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
34
+ row: start.row + i * stepR,
35
+ col: start.col,
36
+ }));
37
+ }
38
+ if (Math.abs(dr) === Math.abs(dc) && dr !== 0) {
39
+ return Array.from({ length: Math.abs(dr) + 1 }, (_, i) => ({
40
+ row: start.row + i * stepR,
41
+ col: start.col + i * stepC,
42
+ }));
43
+ }
44
+ return dr === 0 && dc === 0 ? [start] : [];
45
+ }
46
+ function readSelections(value) {
47
+ if (Array.isArray(value)) {
48
+ return value;
49
+ }
50
+ if (value && typeof value === 'object' && Array.isArray(value.words)) {
51
+ return value.words;
52
+ }
53
+ return [];
54
+ }
55
+ function pathLetters(grid, path) {
56
+ return path
57
+ .map((cell) => (grid[cell.row - 1]?.[cell.col - 1] || '').toUpperCase())
58
+ .join('');
59
+ }
60
+ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution = false, userAnswer, onAnswerChange, }) {
19
61
  const rows = content.gridSize?.rows || content.grid?.length || 14;
20
62
  const cols = content.gridSize?.cols || content.grid?.[0]?.length || 14;
21
63
  const grid = content.grid || [];
22
64
  const wordBank = content.wordBank || [];
23
65
  const isCategorize = content.type === 'CATEGORIZE' && Array.isArray(content.categories) && content.categories.length > 0;
24
66
  const categories = content.categories || [];
25
- // Build cell highlight map
26
- const cellColorMap = {};
27
- // Example selection
28
- if (content.exampleSelection?.path) {
29
- for (const c of content.exampleSelection.path) {
30
- cellColorMap[`${c.row}-${c.col}`] = { colorIdx: 0, word: 'Example' };
67
+ const canSelect = !showSolution && Boolean(onAnswerChange);
68
+ const selections = readSelections(userAnswer);
69
+ const [activeWordId, setActiveWordId] = useState(null);
70
+ const [anchor, setAnchor] = useState(null);
71
+ const playableWords = wordBank.filter((word) => !word.isExample);
72
+ const activeWord = playableWords.find((word) => word.id === activeWordId) ||
73
+ playableWords.find((word) => !selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word))) ||
74
+ null;
75
+ const cellColorMap = useMemo(() => {
76
+ const map = {};
77
+ if (content.exampleSelection?.path) {
78
+ for (const cell of content.exampleSelection.path) {
79
+ map[`${cell.row}-${cell.col}`] = { colorIdx: 0, word: 'Example' };
80
+ }
31
81
  }
32
- }
33
- // Solution paths if visible
34
- if (showSolution && correctAnswer?.words) {
35
- correctAnswer.words.forEach((item, idx) => {
36
- const colorIdx = (idx + 1) % WORD_COLORS.length;
37
- item.path?.forEach((c) => {
38
- cellColorMap[`${c.row}-${c.col}`] = { colorIdx, word: item.word || item.wordId };
82
+ const source = showSolution && correctAnswer?.words ? correctAnswer.words : selections;
83
+ source.forEach((item, index) => {
84
+ const colorIdx = (index + 1) % WORD_COLORS.length;
85
+ item.path?.forEach((cell) => {
86
+ map[`${cell.row}-${cell.col}`] = {
87
+ colorIdx,
88
+ word: item.word || item.wordId || '',
89
+ };
39
90
  });
40
91
  });
41
- }
42
- return (_jsxs("div", { className: "space-y-3", children: [content.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 dark:text-gray-100", children: content.title })), isCategorize ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4", children: categories.map((cat) => {
43
- const wordsInCat = wordBank.filter((w) => w.categoryId === cat.id);
44
- return (_jsxs(Card, { className: "p-3.5 bg-gray-50/70 dark:bg-gray-800/40 border", children: [_jsxs("div", { className: "font-bold text-sm text-gray-800 dark:text-gray-200 border-b pb-1.5 mb-2.5 flex items-center justify-between", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((w, idx) => {
45
- const isExample = Boolean(w.isExample);
46
- return (_jsxs(Badge, { variant: "outline", className: `px-2.5 py-1 text-xs font-medium ${isExample
47
- ? 'line-through bg-amber-50 text-amber-800 border-amber-300'
48
- : 'bg-white dark:bg-gray-800'}`, children: [w.word, isExample && _jsx("span", { className: "ml-1 text-amber-600 font-bold", children: "(Ex)" })] }, w.id || idx));
49
- }) })] }, cat.id));
50
- }) })) : (_jsxs(Card, { className: "p-4 bg-gray-50 dark:bg-gray-800/50 border-dashed border-2", children: [_jsx("div", { className: "text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3 flex items-center gap-2", children: _jsxs("span", { children: ["Word Bank (", wordBank.length, " words)"] }) }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((w, idx) => {
51
- const isExample = Boolean(w.isExample);
52
- return (_jsxs(Badge, { variant: "outline", className: `px-3 py-1.5 text-sm font-medium transition-all ${isExample
53
- ? 'line-through bg-amber-50 text-amber-800 border-amber-300'
54
- : 'bg-white dark:bg-gray-800 hover:shadow-sm'}`, children: [w.word, isExample && (_jsx("span", { className: "ml-1.5 text-xs text-amber-600 font-bold no-underline", children: "(Example)" }))] }, w.id || idx));
55
- }) })] })), _jsx("div", { className: "overflow-x-auto pb-2 flex justify-center", children: _jsx("div", { className: "inline-grid gap-1 p-3 bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800", style: {
56
- gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
57
- }, children: Array.from({ length: rows }).map((_, rIdx) => {
92
+ return map;
93
+ }, [content.exampleSelection, correctAnswer?.words, selections, showSolution]);
94
+ const emit = (next) => {
95
+ onAnswerChange?.({ words: next });
96
+ };
97
+ const handleWordClick = (wordId) => {
98
+ if (!canSelect)
99
+ return;
100
+ const already = selections.find((selection) => selection.wordId === wordId);
101
+ if (already) {
102
+ emit(selections.filter((selection) => selection.wordId !== wordId));
103
+ setActiveWordId(null);
104
+ setAnchor(null);
105
+ return;
106
+ }
107
+ setActiveWordId(wordId);
108
+ setAnchor(null);
109
+ };
110
+ const handleCellClick = (row, col) => {
111
+ if (!canSelect || !activeWord)
112
+ return;
113
+ const cell = { row, col };
114
+ if (!anchor) {
115
+ setAnchor(cell);
116
+ return;
117
+ }
118
+ const path = getStraightLinePath(anchor, cell);
119
+ const letters = pathLetters(grid, path);
120
+ const target = cleanWord(activeWord.word);
121
+ const matched = letters === target || [...letters].reverse().join('') === target;
122
+ setAnchor(null);
123
+ if (!matched || path.length === 0) {
124
+ setAnchor(cell);
125
+ return;
126
+ }
127
+ emit([
128
+ ...selections.filter((selection) => selection.wordId !== activeWord.id && cleanWord(selection.word) !== target),
129
+ {
130
+ wordId: activeWord.id,
131
+ word: activeWord.word,
132
+ categoryId: activeWord.categoryId,
133
+ path,
134
+ },
135
+ ]);
136
+ setActiveWordId(null);
137
+ };
138
+ const renderWordBadge = (word, index) => {
139
+ const isExample = Boolean(word.isExample);
140
+ const isFound = selections.some((selection) => selection.wordId === word.id || cleanWord(selection.word) === cleanWord(word.word));
141
+ const isActive = activeWord?.id === word.id;
142
+ return (_jsxs("button", { type: "button", disabled: !canSelect || isExample, onClick: () => handleWordClick(word.id), className: `rounded-full border px-3 py-1.5 text-sm font-medium ${isExample
143
+ ? 'border-amber-300 bg-amber-50 text-amber-800 line-through'
144
+ : isFound
145
+ ? 'border-emerald-400 bg-emerald-50 text-emerald-800'
146
+ : isActive
147
+ ? 'border-blue-500 bg-blue-50 text-blue-800'
148
+ : 'border-gray-200 bg-white'}`, children: [word.word, isExample ? _jsx("span", { className: "ml-1 font-bold", children: "(Ex)" }) : null] }, word.id || index));
149
+ };
150
+ return (_jsxs("div", { className: "space-y-3", children: [content.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 dark:text-gray-100", children: content.title })), canSelect ? (_jsx("p", { className: "text-xs text-slate-500", children: "Ch\u1ECDn t\u1EEB trong word bank, r\u1ED3i b\u1EA5m \u00F4 \u0111\u1EA7u v\u00E0 \u00F4 cu\u1ED1i tr\u00EAn l\u01B0\u1EDBi." })) : null, isCategorize ? (_jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3", children: categories.map((cat) => {
151
+ const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
152
+ return (_jsxs(Card, { className: "border bg-gray-50/70 p-3.5", children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between border-b pb-1.5 text-sm font-bold", children: [_jsx("span", { children: cat.name }), _jsx(Badge, { variant: "outline", className: "text-xs", children: wordsInCat.length })] }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: wordsInCat.map((word, index) => renderWordBadge(word, index)) })] }, cat.id));
153
+ }) })) : (_jsxs(Card, { className: "border-2 border-dashed bg-gray-50 p-4", children: [_jsxs("div", { className: "mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500", children: ["Word Bank (", wordBank.length, " words)"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: wordBank.map((word, index) => renderWordBadge(word, index)) })] })), _jsx("div", { className: "flex justify-center overflow-x-auto pb-2", children: _jsx("div", { className: "inline-grid gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm", style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }, children: Array.from({ length: rows }).map((_, rIdx) => {
58
154
  const r = rIdx + 1;
59
155
  return Array.from({ length: cols }).map((_, cIdx) => {
60
156
  const c = cIdx + 1;
61
157
  const char = grid[rIdx]?.[cIdx] || '';
62
158
  const highlight = cellColorMap[`${r}-${c}`];
63
159
  const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
64
- return (_jsx("div", { className: `h-9 w-9 sm:h-11 sm:w-11 flex items-center justify-center font-bold text-sm sm:text-base select-none rounded-md transition-colors border ${colorConfig
65
- ? `${colorConfig.bg} font-extrabold shadow-sm scale-95`
66
- : 'border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/30 text-gray-700 dark:text-gray-300'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
160
+ const isAnchor = anchor?.row === r && anchor?.col === c;
161
+ return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex h-9 w-9 items-center justify-center rounded-md border text-sm font-bold sm:h-11 sm:w-11 sm:text-base ${colorConfig
162
+ ? `${colorConfig.bg} shadow-sm`
163
+ : isAnchor
164
+ ? 'border-blue-500 bg-blue-50 text-blue-800'
165
+ : 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
67
166
  });
68
167
  }) }) })] }));
69
168
  }
@@ -1,2 +1,2 @@
1
1
  import type { TrueFalseCorrectGroupClientProps } from '../../_shared/types/true-false-correct-group.type';
2
- export declare function TrueFalseCorrectGroupClient({ questionData, isReviewMode, userAnswers, }: TrueFalseCorrectGroupClientProps): import("react").JSX.Element;
2
+ export declare function TrueFalseCorrectGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: TrueFalseCorrectGroupClientProps): import("react").JSX.Element;
@@ -2,24 +2,46 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, CircleHelp, Lightbulb, Star, X } from 'lucide-react';
4
4
  import { cn } from '../../../../shared/lib/utils';
5
- export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false, userAnswers = [], }) {
5
+ function emptyAnswer() {
6
+ return { isTrue: null, correction: '' };
7
+ }
8
+ export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
6
9
  const items = questionData.items || [];
10
+ const emit = (itemIndex, nextItem) => {
11
+ const item = items[itemIndex];
12
+ if (isReviewMode || item?.isExample || !onAnswerChange) {
13
+ return;
14
+ }
15
+ const next = items.map((_, index) => userAnswers[index] ?? emptyAnswer());
16
+ next[itemIndex] = nextItem;
17
+ onAnswerChange(next);
18
+ };
7
19
  return (_jsxs("div", { className: "space-y-4", children: [questionData.instruction && (_jsx("div", { className: "rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm font-medium text-indigo-800", children: questionData.instruction })), items.map((item, itemIndex) => {
8
- const userValue = userAnswers[itemIndex];
9
20
  const correctIsTrue = item.isTrue === true;
21
+ const userValue = item.isExample
22
+ ? { isTrue: item.isTrue, correction: item.correction }
23
+ : userAnswers[itemIndex];
24
+ const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
25
+ const showStudentCorrection = userValue?.isTrue === false || (isReviewMode && !correctIsTrue);
10
26
  return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", item.questionNumber || itemIndex + 1] }), item.isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), "Example"] }))] }), _jsxs("span", { className: "text-xs text-gray-500", children: [item.points || 0, " \u0111i\u1EC3m"] })] }), _jsxs("div", { className: "space-y-3 p-3", children: [_jsx("p", { className: "text-sm font-semibold leading-relaxed text-gray-800", children: item.question || 'Câu hỏi chưa được nhập' }), _jsx("div", { className: "flex items-center gap-3", children: [
11
27
  { value: true, label: 'True' },
12
28
  { value: false, label: 'False' },
13
29
  ].map((opt) => {
14
30
  const isCorrect = opt.value === correctIsTrue;
15
31
  const isSelected = userValue?.isTrue === opt.value;
16
- return (_jsxs("div", { className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', isReviewMode && isCorrect
32
+ return (_jsxs("button", { type: "button", disabled: !canSelect, onClick: () => emit(itemIndex, {
33
+ isTrue: opt.value,
34
+ correction: opt.value ? '' : (userValue?.correction || ''),
35
+ }), className: cn('flex flex-1 items-center justify-center gap-2 rounded-lg border-2 px-4 py-2 text-sm font-semibold transition-colors', canSelect ? 'cursor-pointer hover:border-blue-300' : 'cursor-default', isReviewMode && isCorrect
17
36
  ? 'border-green-500 bg-green-50 text-green-700'
18
37
  : isReviewMode && isSelected && !isCorrect
19
38
  ? 'border-red-500 bg-red-50 text-red-700'
20
39
  : isSelected
21
40
  ? 'border-blue-500 bg-blue-50 text-blue-700'
22
41
  : 'border-gray-200 bg-white text-gray-700'), children: [opt.value ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(X, { className: "h-4 w-4" }), _jsx("span", { children: opt.label })] }, String(opt.value)));
23
- }) }), !correctIsTrue && (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50/70 px-3 py-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-amber-800", children: "C\u00E2u \u0111\u00FAng" }), _jsx("p", { className: "mt-1 text-sm text-amber-900", children: item.correction || 'Chưa nhập câu sửa' }), isReviewMode && userValue && userValue.isTrue === false && (_jsxs("p", { className: "mt-2 text-xs text-slate-600", children: ["H\u1ECDc sinh: ", userValue.correction || '(trống)'] }))] }))] })] }, `${item.questionNumber}-${itemIndex}`));
42
+ }) }), showStudentCorrection && canSelect ? (_jsxs("div", { className: "rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx("label", { className: "mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500", children: "Correct the false sentence" }), _jsx("textarea", { value: userValue?.correction || '', onChange: (event) => emit(itemIndex, {
43
+ isTrue: false,
44
+ correction: event.target.value,
45
+ }), placeholder: "Write the correct sentence...", rows: 2, className: "w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none ring-blue-200 focus:border-blue-400 focus:ring-2" })] })) : null, isReviewMode && !correctIsTrue ? (_jsxs("div", { className: "rounded-lg border border-amber-100 bg-amber-50/70 px-3 py-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-amber-800", children: "C\u00E2u \u0111\u00FAng" }), _jsx("p", { className: "mt-1 text-sm text-amber-900", children: item.correction || 'Chưa nhập câu sửa' }), userValue && userValue.isTrue === false ? (_jsxs("p", { className: "mt-2 text-xs text-slate-600", children: ["H\u1ECDc sinh: ", userValue.correction || '(trống)'] })) : null] })) : null] })] }, `${item.questionNumber}-${itemIndex}`));
24
46
  }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
25
47
  }
@@ -1,2 +1,2 @@
1
1
  import type { TrueFalseGroupClientProps } from '../../_shared/types/true-false-group.type';
2
- export declare function TrueFalseGroupClient({ questionData, isReviewMode, userAnswers, }: TrueFalseGroupClientProps): import("react").JSX.Element;
2
+ export declare function TrueFalseGroupClient({ questionData, isReviewMode, userAnswers, onAnswerChange, }: TrueFalseGroupClientProps): import("react").JSX.Element;