@tinyweb_dev/oe-exam-sdk 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +3 -1
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +3 -1
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.d.ts +8 -0
- package/dist/components/questions/_shared/components/compact/KidCollapsibleBlock.js +16 -0
- package/dist/components/questions/_shared/components/compact/KidPassage.d.ts +2 -1
- package/dist/components/questions/_shared/components/compact/KidPassage.js +2 -2
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +1 -1
- package/dist/components/questions/_shared/components/compact/index.d.ts +2 -0
- package/dist/components/questions/_shared/components/compact/index.js +2 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.d.ts +3 -0
- package/dist/components/questions/_shared/components/compact/reviewItemCard.js +11 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +3 -3
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +2 -2
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +19 -8
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +2 -2
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +2 -2
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +2 -2
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +53 -50
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +2 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +8 -2
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +5 -3
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +18 -15
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +2 -2
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +16 -8
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +2 -2
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +2 -2
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +3 -1
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +4 -2
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +2 -2
- package/dist/components/questions/viewer/QuestionViewer.js +2 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
7
7
|
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
8
8
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
9
|
+
import { COLLAPSE_CHAR_THRESHOLD, KidCollapsibleBlock, } from '../../../../questions/_shared/components/compact';
|
|
9
10
|
// Single Question Component (1 Line / Full Width Row)
|
|
10
11
|
function QuestionItem({ question, answer, onAnswerChange, isReviewMode, correctAnswerTextMap, id, }) {
|
|
11
12
|
const isDisabled = isReviewMode || question.isExample;
|
|
@@ -47,5 +48,6 @@ export function MoversTrueFalseRenderer({ partNumber, partName, questionCount, i
|
|
|
47
48
|
// Separate example questions from regular questions
|
|
48
49
|
const exampleQuestions = questions.filter((q) => q.isExample);
|
|
49
50
|
const regularQuestions = questions.filter((q) => !q.isExample);
|
|
50
|
-
return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-4 overflow-y-auto sm:gap-5 pb-6", children: [_jsxs("div", { className: "flex flex-col gap-3.5 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: "max-h-[300px] rounded-xl object-contain shadow-md" }) })), groupContent && (_jsx(
|
|
51
|
+
return (_jsxs("div", { className: "custom-scrollbar flex h-full flex-col gap-4 overflow-y-auto sm:gap-5 pb-6", children: [_jsxs("div", { className: "flex flex-col gap-3.5 p-3 sm:px-6 sm:pt-2", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Question illustration", className: "max-h-[300px] rounded-xl object-contain shadow-md" }) })), groupContent && (_jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
|
|
52
|
+
groupContent.replace(/<[^>]+>/g, ' ').length > COLLAPSE_CHAR_THRESHOLD, children: _jsx("div", { className: "rounded-xl border border-amber-200 bg-amber-50/60 p-4 text-[15.5px] leading-relaxed text-slate-800 shadow-sm", dangerouslySetInnerHTML: { __html: groupContent } }) })), exampleQuestions.length > 0 && (_jsx("div", { className: "mt-1 flex flex-col gap-3", children: exampleQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) }))] }), exampleQuestions.length > 0 && regularQuestions.length > 0 && (_jsx("div", { className: "px-3 sm:px-6", children: _jsx("hr", { className: "border-t border-dashed border-slate-200" }) })), _jsx("div", { className: "flex flex-col gap-3 px-3 sm:px-6", children: regularQuestions.map((question) => (_jsx(QuestionItem, { id: `question-${question.id}`, question: question, answer: answers[question.id], onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, correctAnswerTextMap: correctAnswerTextMap }, question.id))) })] }));
|
|
51
53
|
}
|
package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js
CHANGED
|
@@ -6,6 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
import { ResolvedImage } from '../../../../../components/common/ResolvedImage';
|
|
7
7
|
import CambridgeYlePartBanner from '../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner';
|
|
8
8
|
import CambridgeYleInstructionBanner from '../../../../../components/themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
9
|
+
import { COLLAPSE_CHAR_THRESHOLD, KidCollapsibleBlock, } from '../../../../questions/_shared/components/compact';
|
|
9
10
|
export function MoversWriteSentencesRenderer({ partNumber, partName, questionCount, instruction, imageUrl, title, groupContent, questions, answers, onAnswerChange, isReviewMode = false, }) {
|
|
10
|
-
return (_jsxs("div", { className: `grid h-full grid-cols-1 gap-4 sm:gap-6 ${!isReviewMode ? 'lg:grid-cols-2' : ''}`, children: [_jsxs("div", { className: "flex flex-col gap-4 overflow-y-auto p-3 sm:pb-6 sm:pr-6 sm:pt-6", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Illustration", className: "max-h-[300px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsx(
|
|
11
|
+
return (_jsxs("div", { className: `grid h-full grid-cols-1 gap-4 sm:gap-6 ${!isReviewMode ? 'lg:grid-cols-2' : ''}`, children: [_jsxs("div", { className: "flex flex-col gap-4 overflow-y-auto p-3 sm:pb-6 sm:pr-6 sm:pt-6", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode }), imageUrl && (_jsx("div", { className: "flex items-center justify-center", children: _jsx(ResolvedImage, { src: imageUrl, alt: "Illustration", className: "max-h-[300px] rounded-lg object-contain shadow-md" }) })), groupContent && (_jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
|
|
12
|
+
groupContent.replace(/<[^>]+>/g, ' ').length > COLLAPSE_CHAR_THRESHOLD, children: _jsx("div", { className: "rounded-lg bg-amber-50 p-4 text-sm leading-relaxed text-gray-700", children: groupContent }) }))] }), _jsxs("div", { className: "flex flex-col gap-4 overflow-y-auto p-3 sm:gap-5 sm:pb-6 sm:pl-6 sm:pt-6", children: [title && (_jsx("h2", { className: "text-center text-2xl font-bold uppercase text-coral-500", children: title })), questions.map((question) => (_jsxs("div", { id: `question-${question.id}`, className: "flex items-start gap-3", children: [_jsx("span", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-sm font-bold text-white", children: question.questionNumber }), _jsx("textarea", { value: answers[question.id] || '', onChange: (e) => onAnswerChange(question.id, e.target.value), disabled: isReviewMode, placeholder: "Write your sentence...", rows: 2, className: "flex-1 resize-none rounded-lg border-2 border-gray-300 bg-white px-3 py-2 text-base outline-none transition-colors focus:border-blue-500 disabled:opacity-60" })] }, question.id)))] })] }));
|
|
11
13
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare const COLLAPSE_CHAR_THRESHOLD = 480;
|
|
3
|
+
export declare function KidCollapsibleBlock({ collapse, children, className, previewClassName, }: {
|
|
4
|
+
collapse: boolean;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
previewClassName?: string;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useT } from '../../../../../shared/lib/i18n';
|
|
5
|
+
import { cn } from '../../../../../shared/lib/utils';
|
|
6
|
+
export const COLLAPSE_CHAR_THRESHOLD = 480;
|
|
7
|
+
export function KidCollapsibleBlock({ collapse, children, className, previewClassName = 'max-h-40 overflow-hidden', }) {
|
|
8
|
+
const t = useT();
|
|
9
|
+
const [expanded, setExpanded] = useState(!collapse);
|
|
10
|
+
if (!collapse) {
|
|
11
|
+
return _jsx("div", { className: className, children: children });
|
|
12
|
+
}
|
|
13
|
+
return (_jsxs("div", { className: className, children: [_jsx("div", { className: cn(!expanded && previewClassName), children: children }), _jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "mt-3 text-sm font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
|
|
14
|
+
? t('question.viewer.collapsePassage')
|
|
15
|
+
: t('question.viewer.expandPassage') })] }));
|
|
16
|
+
}
|
|
@@ -8,14 +8,14 @@ const COLLAPSE_CHAR_THRESHOLD = 480;
|
|
|
8
8
|
function plainTextLength(html) {
|
|
9
9
|
return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().length;
|
|
10
10
|
}
|
|
11
|
-
export function KidPassage({ html, className, }) {
|
|
11
|
+
export function KidPassage({ html, className, isReviewMode = false, }) {
|
|
12
12
|
const t = useT();
|
|
13
13
|
const isLong = plainTextLength(html) > COLLAPSE_CHAR_THRESHOLD;
|
|
14
14
|
const [expanded, setExpanded] = useState(!isLong);
|
|
15
15
|
if (!html) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
-
return (_jsxs("div", { className: cn('rounded-xl border border-gray-200 bg-white p-5 shadow-xs', expanded && isLong && 'md:sticky md:top-24 md:z-10', className), children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: t('question.viewer.readingPassage') })] }), _jsx("div", { className: cn('prose prose-sm max-w-none leading-relaxed text-gray-800', !expanded && 'max-h-40 overflow-hidden'), dangerouslySetInnerHTML: { __html: html } }), isLong ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "mt-3 text-sm font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
|
|
18
|
+
return (_jsxs("div", { className: cn('rounded-xl border border-gray-200 bg-white p-5 shadow-xs', expanded && isLong && !isReviewMode && 'md:sticky md:top-24 md:z-10', className), children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: t('question.viewer.readingPassage') })] }), _jsx("div", { className: cn('prose prose-sm max-w-none leading-relaxed text-gray-800', !expanded && 'max-h-40 overflow-hidden'), dangerouslySetInnerHTML: { __html: html } }), isLong ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "mt-3 text-sm font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
|
|
19
19
|
? t('question.viewer.collapsePassage')
|
|
20
20
|
: t('question.viewer.expandPassage') })) : null] }));
|
|
21
21
|
}
|
|
@@ -5,5 +5,5 @@ import { useT } from '../../../../../shared/lib/i18n';
|
|
|
5
5
|
import { cn } from '../../../../../shared/lib/utils';
|
|
6
6
|
export function KidQuestionCard({ icon, hideIcon = false, title, titleClassName, points, isExample = false, review, headerExtra, children, className, contentClassName, }) {
|
|
7
7
|
const t = useT();
|
|
8
|
-
return (_jsxs("div", { className: cn('overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [!hideIcon && (_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) })), _jsx("span", { className: cn('text-sm font-medium text-gray-700', titleClassName ?? 'truncate'), children: title }), isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), t('question.viewer.example')] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsx("span", { className: "text-xs text-gray-500", children: t('question.viewer.points', { count: points }) })), review === 'correct' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700", children: [_jsx(Check, { className: "h-3 w-3" }), t('question.viewer.correct')] })), review === 'wrong' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: [_jsx(X, { className: "h-3 w-3" }), t('question.viewer.wrong')] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
|
|
8
|
+
return (_jsxs("div", { className: cn('min-w-0 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [!hideIcon && (_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-indigo-600 text-white", children: icon ?? _jsx(CircleHelp, { className: "h-3.5 w-3.5" }) })), _jsx("span", { className: cn('text-sm font-medium text-gray-700', titleClassName ?? 'truncate'), children: title }), isExample && (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700", children: [_jsx(Star, { className: "h-3 w-3" }), t('question.viewer.example')] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerExtra, typeof points === 'number' && (_jsx("span", { className: "text-xs text-gray-500", children: t('question.viewer.points', { count: points }) })), review === 'correct' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700", children: [_jsx(Check, { className: "h-3 w-3" }), t('question.viewer.correct')] })), review === 'wrong' && (_jsxs("div", { className: "flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: [_jsx(X, { className: "h-3 w-3" }), t('question.viewer.wrong')] }))] })] }), _jsx("div", { className: cn('space-y-3 p-3', contentClassName), children: children })] }));
|
|
9
9
|
}
|
|
@@ -6,7 +6,9 @@ export { CompactFieldCard } from './CompactFieldCard';
|
|
|
6
6
|
export { CompactExplanationToggle } from './CompactExplanationToggle';
|
|
7
7
|
export { KidInstruction } from './KidInstruction';
|
|
8
8
|
export { KidPassage } from './KidPassage';
|
|
9
|
+
export { KidCollapsibleBlock, COLLAPSE_CHAR_THRESHOLD } from './KidCollapsibleBlock';
|
|
9
10
|
export { KidPrompt } from './KidPrompt';
|
|
10
11
|
export { KidQuestionCard } from './KidQuestionCard';
|
|
12
|
+
export { reviewItemTitle, reviewItemPoints } from './reviewItemCard';
|
|
11
13
|
export { KidChoiceRow, optionLetter } from './KidChoiceRow';
|
|
12
14
|
export { SpeakingReplyMic } from './SpeakingReplyMic';
|
|
@@ -6,7 +6,9 @@ export { CompactFieldCard } from './CompactFieldCard';
|
|
|
6
6
|
export { CompactExplanationToggle } from './CompactExplanationToggle';
|
|
7
7
|
export { KidInstruction } from './KidInstruction';
|
|
8
8
|
export { KidPassage } from './KidPassage';
|
|
9
|
+
export { KidCollapsibleBlock, COLLAPSE_CHAR_THRESHOLD } from './KidCollapsibleBlock';
|
|
9
10
|
export { KidPrompt } from './KidPrompt';
|
|
10
11
|
export { KidQuestionCard } from './KidQuestionCard';
|
|
12
|
+
export { reviewItemTitle, reviewItemPoints } from './reviewItemCard';
|
|
11
13
|
export { KidChoiceRow, optionLetter } from './KidChoiceRow';
|
|
12
14
|
export { SpeakingReplyMic } from './SpeakingReplyMic';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Review chrome: item index only — homework already shows Exercise N. */
|
|
2
|
+
export declare function reviewItemTitle(n: number, isReviewMode: boolean, takingLabel: string): string;
|
|
3
|
+
export declare function reviewItemPoints(isReviewMode: boolean, points?: number): number | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Review chrome: item index only — homework already shows Exercise N. */
|
|
2
|
+
export function reviewItemTitle(n, isReviewMode, takingLabel) {
|
|
3
|
+
if (isReviewMode)
|
|
4
|
+
return String(n);
|
|
5
|
+
return takingLabel;
|
|
6
|
+
}
|
|
7
|
+
export function reviewItemPoints(isReviewMode, points) {
|
|
8
|
+
if (isReviewMode)
|
|
9
|
+
return undefined;
|
|
10
|
+
return points;
|
|
11
|
+
}
|
package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js
CHANGED
|
@@ -4,7 +4,7 @@ import { CircleHelp, Lightbulb, Sparkles, Volume2 } from 'lucide-react';
|
|
|
4
4
|
import { useT } from '../../../../shared/lib/i18n';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { toAnswerTheQuestionGroupImageUrls } from '../../_shared/types/answer-the-question-group.type';
|
|
7
|
-
import { KidInstruction, KidPassage, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
7
|
+
import { KidInstruction, KidPassage, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
8
8
|
function isAbsoluteMediaUrl(value) {
|
|
9
9
|
return (value.startsWith('http://')
|
|
10
10
|
|| value.startsWith('https://')
|
|
@@ -46,14 +46,14 @@ export function AnswerTheQuestionGroupClient({ questionData, isReviewMode = fals
|
|
|
46
46
|
const items = questionData.items || [];
|
|
47
47
|
const instruction = questionData.instruction?.trim()
|
|
48
48
|
|| t('question.viewer.atqgDefaultInstruction');
|
|
49
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: instruction }), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.imageAlt', { n: imageIndex + 1 }) }, `group-image-${imageIndex}`))) })), questionData.passage ? _jsx(KidPassage, { html: questionData.passage }) : null, items.map((item, itemIndex) => {
|
|
49
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: instruction }), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.imageAlt', { n: imageIndex + 1 }) }, `group-image-${imageIndex}`))) })), questionData.passage ? (_jsx(KidPassage, { html: questionData.passage, isReviewMode: isReviewMode })) : null, items.map((item, itemIndex) => {
|
|
50
50
|
const userValue = userAnswers[itemIndex] || '';
|
|
51
51
|
const expectedAnswers = Array.isArray(item.expectedAnswers)
|
|
52
52
|
? item.expectedAnswers.filter((answer) => answer.trim() !== '')
|
|
53
53
|
: [];
|
|
54
54
|
const questionNumber = item.questionNumber || itemIndex + 1;
|
|
55
55
|
const answerId = `atqg-answer-${questionNumber}`;
|
|
56
|
-
return (_jsxs(KidQuestionCard, { icon: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }), title: t('question.viewer.questionN', { n: questionNumber }), points: item.points || 0, isExample: item.isExample, headerExtra: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] }), children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.itemImageAlt', {
|
|
56
|
+
return (_jsxs(KidQuestionCard, { icon: _jsx(CircleHelp, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(questionNumber, isReviewMode, t('question.viewer.questionN', { n: questionNumber })), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, headerExtra: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-700", children: [_jsx(Sparkles, { className: "h-3 w-3" }), "AI"] }), children: [toAnswerTheQuestionGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toAnswerTheQuestionGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, alt: t('question.viewer.itemImageAlt', {
|
|
57
57
|
n: questionNumber,
|
|
58
58
|
m: imageIndex + 1,
|
|
59
59
|
}) }, `${item.questionNumber}-${imageIndex}`))) })), _jsx(KidPrompt, { children: _jsx("label", { htmlFor: answerId, children: item.question || t('question.viewer.missingPrompt') }) }), isReviewMode || item.isExample || !onAnswerChange ? (userValue ? (_jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-700", children: userValue })) : null) : (_jsx("textarea", { id: answerId, name: answerId, value: userValue, onChange: (event) => {
|
package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { Check, X } from 'lucide-react';
|
|
5
|
-
import { KidQuestionCard } from '../../_shared/components/compact';
|
|
5
|
+
import { KidQuestionCard, reviewItemTitle } from '../../_shared/components/compact';
|
|
6
6
|
// Fisher-Yates shuffle algorithm
|
|
7
7
|
function shuffleArray(array) {
|
|
8
8
|
const shuffled = [...array];
|
|
@@ -128,7 +128,7 @@ export function ArrangeLettersIntoWordsClient({ questionData, onSubmit, isReview
|
|
|
128
128
|
const available = availableLetters[subQuestion.id] || [];
|
|
129
129
|
const arranged = arrangedLetters[subQuestion.id] || [];
|
|
130
130
|
const imagePreviewUrl = getImagePreviewUrl(subQuestion);
|
|
131
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${sqIndex + 1}
|
|
131
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(sqIndex + 1, isReviewMode, `Câu ${sqIndex + 1}`), review: isReviewMode
|
|
132
132
|
? isCorrect
|
|
133
133
|
? 'correct'
|
|
134
134
|
: userAnswerValue.trim() !== ''
|
package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js
CHANGED
|
@@ -7,7 +7,7 @@ import { cn } from '../../../../shared/lib/utils';
|
|
|
7
7
|
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
8
8
|
import { isRichTextEmpty, richTextToPlainText } from '../../../../shared/lib/rich-text';
|
|
9
9
|
import { normalizeChooseAnswerCorrectAnswer, normalizeChooseAnswerOptions, optionToString } from './normalize';
|
|
10
|
-
import { KidChoiceRow, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
|
|
10
|
+
import { COLLAPSE_CHAR_THRESHOLD, KidChoiceRow, KidCollapsibleBlock, KidPrompt, KidQuestionCard, optionLetter, reviewItemPoints } from '../../_shared/components/compact';
|
|
11
11
|
// Helper component to resolve presigned URL for each image option
|
|
12
12
|
function OptionImage({ src, alt, className }) {
|
|
13
13
|
const { previewUrl, isLoading } = usePresignedFileUrl(src);
|
|
@@ -105,13 +105,24 @@ export function ChooseTheCorrectAnswerClient({ questionData, onSubmit, isReviewM
|
|
|
105
105
|
: typeof questionData.glossary === 'string' && questionData.glossary.trim()
|
|
106
106
|
? [questionData.glossary]
|
|
107
107
|
: [];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
const groupStemPlain = [
|
|
109
|
+
String(questionData.groupContent || ''),
|
|
110
|
+
...(hasArticles
|
|
111
|
+
? questionData.articles.map((article) => article.content || '')
|
|
112
|
+
: []),
|
|
113
|
+
]
|
|
114
|
+
.join(' ')
|
|
115
|
+
.replace(/<[^>]+>/g, ' ');
|
|
116
|
+
const collapseGroupStem = isReviewMode && groupStemPlain.length > COLLAPSE_CHAR_THRESHOLD;
|
|
117
|
+
return (_jsxs("div", { className: "space-y-4", children: [passageList.length > 0 && (_jsxs("div", { className: "rounded-xl border border-indigo-100 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
|
|
118
|
+
passageList.join(' ').replace(/<[^>]+>/g, ' ').length >
|
|
119
|
+
COLLAPSE_CHAR_THRESHOLD, children: _jsx("div", { className: "space-y-3", children: passageList.map((p, i) => (_jsx("div", { className: "prose prose-sm max-w-none text-gray-800 leading-relaxed", dangerouslySetInnerHTML: { __html: p } }, i))) }) })] })), glossaryList.length > 0 && (_jsxs("div", { className: "rounded-xl border border-amber-200 bg-amber-50/60 p-4 shadow-xs", children: [_jsx("div", { className: "mb-2 text-xs font-bold uppercase tracking-wider text-amber-800", children: "T\u1EEB v\u1EF1ng (Glossary)" }), _jsx("div", { className: "space-y-1.5 text-xs text-amber-900 leading-relaxed", children: glossaryList.map((g, i) => (_jsx("div", { dangerouslySetInnerHTML: { __html: g } }, i))) })] })), questionData.isGroup && (questionData.groupTitle || questionData.groupSubtitle || questionData.groupContent || questionData.groupImageUrl || hasArticles) && (_jsxs("div", { className: "relative overflow-hidden rounded-2xl border border-indigo-100 bg-white shadow-sm ring-1 ring-black/5 transition-all", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-indigo-50/80 via-white to-blue-50/30 opacity-70 pointer-events-none" }), _jsx("div", { className: "absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-indigo-500 to-blue-600 rounded-l-2xl" }), _jsxs("div", { className: "relative p-6 sm:p-7", children: [_jsxs("div", { className: "flex items-start gap-4 mb-5 border-b border-indigo-50/60 pb-4", children: [_jsx("div", { className: "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-100 to-blue-100 shadow-inner ring-1 ring-indigo-200/50", children: _jsx(BookOpen, { className: "h-5 w-5 text-indigo-600" }) }), _jsxs("div", { className: "flex-1 pt-1", children: [_jsx("div", { className: "flex items-center gap-2 mb-1", children: questionData.groupNumber && (_jsxs("span", { className: "inline-flex items-center rounded-md bg-indigo-50 px-2 py-1 text-xs font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-700/10", children: ["B\u00E0i \u0111\u1ECDc ", questionData.groupNumber] })) }), questionData.groupTitle && (_jsx("h4", { className: "text-xl font-bold text-slate-800 leading-tight tracking-tight", children: questionData.groupTitle })), questionData.groupSubtitle && (_jsx("p", { className: "mt-1 text-sm text-slate-500 leading-snug", children: questionData.groupSubtitle }))] })] }), _jsxs(KidCollapsibleBlock, { collapse: collapseGroupStem, children: [hasArticles && (_jsx("div", { className: "flex flex-col gap-4", children: questionData.articles.map((article, i) => (_jsxs("div", { className: "group relative overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-all hover:shadow-md hover:border-indigo-200", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-slate-100 bg-gradient-to-r from-indigo-50/60 to-blue-50/40 px-4 py-2.5", children: [_jsx("div", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-blue-600 text-xs font-bold text-white shadow-sm", children: String.fromCharCode(65 + i) }), _jsx("h5", { className: "flex-1 truncate text-sm font-bold text-slate-800", children: article.name || `Bài viết ${i + 1}` })] }), _jsxs("div", { className: "flex flex-col gap-3 p-4 sm:flex-row sm:items-start", children: [article.imageUrl && (_jsx("div", { className: "flex flex-shrink-0 items-center justify-center rounded-md bg-slate-50 p-2 sm:w-40", children: _jsx(OptionImage, { src: article.imageUrl, alt: article.name || `Article ${i + 1}`, className: "max-h-40 w-auto rounded-md object-contain" }) })), article.content && (_jsx("div", { className: "prose prose-sm prose-slate max-w-none flex-1 text-[14px] leading-relaxed text-slate-700", children: article.content.split('\n').map((line, j) => (_jsx("p", { className: "mb-2 last:mb-0", children: line }, j))) }))] })] }, i))) })), !hasArticles && (_jsxs("div", { className: "space-y-6 text-slate-700", children: [questionData.groupContent && (_jsx("div", { className: "prose prose-slate prose-p:leading-relaxed prose-p:mb-3 max-w-none text-[15px]", dangerouslySetInnerHTML: {
|
|
120
|
+
__html: questionData.groupContent
|
|
121
|
+
.replace(/\{(\d+)\}/g, (_m, num) => `<span style="display:inline-block;min-width:100px;padding:1px 5px;margin:0 5px;background:#eef2ff;border-bottom:2px solid #6366f1;color:#4f46e5;font-weight:700;font-size:0.85em;text-align:center;vertical-align:baseline;letter-spacing:2px">———(${Number(num) + 1})———</span>`)
|
|
122
|
+
.split('\n')
|
|
123
|
+
.map((line) => `<p class="last:mb-0">${line}</p>`)
|
|
124
|
+
.join(''),
|
|
125
|
+
} })), questionData.groupImageUrl && (_jsxs("div", { className: "mt-5 relative overflow-hidden rounded-xl border border-slate-200/60 bg-slate-50 shadow-sm transition-all hover:shadow-md", children: [_jsx("div", { className: "flex items-center justify-between border-b border-slate-200/60 bg-slate-100/50 px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("div", { className: "h-2.5 w-2.5 rounded-full bg-slate-300" }), _jsx("span", { className: "ml-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: "T\u00E0i li\u1EC7u \u0111\u00EDnh k\u00E8m" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4 min-h-[160px]", children: _jsx(OptionImage, { src: questionData.groupImageUrl, alt: "Group illustration", className: "max-h-64 w-auto rounded-lg object-contain" }) })] }))] }))] })] })] })), _jsxs(KidQuestionCard, { title: isMultipleAnswers ? 'Chọn các đáp án đúng' : 'Chọn đáp án đúng', points: reviewItemPoints(isReviewMode, questionData.points), review: isReviewMode && hasAnswered ? (isAnswerCorrect ? 'correct' : 'wrong') : null, children: [(questionData.askerName || questionData.answererName) && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 p-2.5 text-xs text-gray-600", children: [_jsx(Users, { className: "h-4 w-4 text-gray-400" }), questionData.askerName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi h\u1ECFi:" }), " ", questionData.askerName] })), questionData.askerName && questionData.answererName && _jsx("span", { children: "\u2022" }), questionData.answererName && (_jsxs("span", { children: [_jsx("strong", { children: "Ng\u01B0\u1EDDi tr\u1EA3 l\u1EDDi:" }), " ", questionData.answererName] }))] })), showQuestion && (_jsx(KidPrompt, { children: isRichTextEmpty(questionData.question) ? (_jsx("h3", { className: "text-sm font-semibold text-gray-800", children: "C\u00E2u h\u1ECFi ch\u01B0a \u0111\u01B0\u1EE3c nh\u1EADp" })) : (_jsx(RichTextHtml, { html: questionData.question, className: "text-sm font-semibold text-gray-800" })) })), showImage && (imagePreviewUrl || cleanImageUrl) && (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 bg-gradient-to-br from-slate-50 to-gray-100", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-2 w-2 rounded-full bg-red-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-400" }), _jsx("div", { className: "h-2 w-2 rounded-full bg-green-400" }), _jsx("span", { className: "ml-2 text-xs font-medium text-gray-500", children: "H\u00ECnh \u1EA3nh" })] }) }), _jsx("div", { className: "flex items-center justify-center p-4", children: isLoading ? (_jsx("div", { className: "flex h-48 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" }) })) : (
|
|
115
126
|
/* eslint-disable-next-line @next/next/no-img-element */
|
|
116
127
|
_jsx("img", { src: imagePreviewUrl || cleanImageUrl, alt: "Question illustration", className: "max-h-56 w-auto object-contain rounded-md", onError: (e) => {
|
|
117
128
|
const target = e.target;
|
|
@@ -5,7 +5,7 @@ import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
|
5
5
|
import { RichTextHtml } from '../../../../components/shared/RichTextHtml';
|
|
6
6
|
import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
|
|
7
7
|
import { getChooseAnswerGroupOptionImageSrc, toChooseAnswerGroupImageUrls, } from '../../_shared/types/choose-the-correct-answer-group.type';
|
|
8
|
-
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter } from '../../_shared/components/compact';
|
|
8
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
9
9
|
function ClientImageItem({ url, alt }) {
|
|
10
10
|
const { previewUrl, isLoading } = usePresignedFileUrl(url);
|
|
11
11
|
const src = previewUrl ||
|
|
@@ -38,7 +38,7 @@ export function ChooseTheCorrectAnswerGroupClient({ questionData, isReviewMode =
|
|
|
38
38
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), toChooseAnswerGroupImageUrls(questionData.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `group-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh nhóm (${imageIndex + 1})` }, `group-image-${imageIndex}`))) })), questionData.passage && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc (Reading Passage)" })] }), _jsx("div", { className: "overflow-x-auto", children: _jsx(RichTextHtml, { html: questionData.passage, className: "text-gray-800 leading-relaxed" }) })] })), items.map((item, itemIndex) => {
|
|
39
39
|
const selectedId = userAnswers[itemIndex];
|
|
40
40
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
41
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
41
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, children: [!isRichTextEmpty(item.question) && (_jsx(KidPrompt, { children: _jsx(RichTextHtml, { html: item.question, className: "text-sm font-semibold leading-relaxed text-gray-800" }) })), toChooseAnswerGroupImageUrls(item.audioUrl).map((url, audioIndex) => (_jsx(ClientAudio, { url: url }, `${item.questionNumber}-audio-${audioIndex}`))), toChooseAnswerGroupImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-3", children: toChooseAnswerGroupImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1} (${imageIndex + 1})` }, `${item.questionNumber}-${imageIndex}`))) })), _jsx("div", { className: "space-y-1.5", children: item.options.map((option, optionIndex) => {
|
|
42
42
|
const isCorrect = option.id === item.correctAnswer;
|
|
43
43
|
const isSelected = option.id === selectedId;
|
|
44
44
|
const optionImageSrc = getChooseAnswerGroupOptionImageSrc(option);
|
package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Check, Lightbulb, X } from 'lucide-react';
|
|
4
4
|
import { useT } from '../../../../shared/lib/i18n';
|
|
5
5
|
import { cn } from '../../../../shared/lib/utils';
|
|
6
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
7
7
|
function emptyAnswer() {
|
|
8
8
|
return { optionId: '', text: '' };
|
|
9
9
|
}
|
|
@@ -43,7 +43,7 @@ export function ChooseThenAnswerGroupClient({ questionData, isReviewMode = false
|
|
|
43
43
|
text: next.text ?? current.text,
|
|
44
44
|
});
|
|
45
45
|
};
|
|
46
|
-
return (_jsx(KidQuestionCard, { title: t('question.viewer.questionN', { n: item.questionNumber || itemIndex + 1 }), points: points, isExample: item.isExample, children: _jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "min-w-0 space-y-2.5", children: [(parts[0] || parts.slice(1).join('{0}')) ? (_jsxs("p", { className: "text-sm font-semibold leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "mx-1 inline-block min-w-[3rem] border-b-2 border-dashed border-slate-300 align-baseline" }), _jsx("span", { children: parts.slice(1).join('{0}') })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-2.5", role: "group", "aria-label": t('question.viewer.questionN', {
|
|
46
|
+
return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, t('question.viewer.questionN', { n: item.questionNumber || itemIndex + 1 })), points: reviewItemPoints(isReviewMode, points), isExample: item.isExample, children: _jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "min-w-0 space-y-2.5", children: [(parts[0] || parts.slice(1).join('{0}')) ? (_jsxs("p", { className: "text-sm font-semibold leading-7 text-slate-800", children: [_jsx("span", { children: parts[0] || '' }), _jsx("span", { className: "mx-1 inline-block min-w-[3rem] border-b-2 border-dashed border-slate-300 align-baseline" }), _jsx("span", { children: parts.slice(1).join('{0}') })] })) : null, _jsx("div", { className: "flex flex-wrap items-center gap-2.5", role: "group", "aria-label": t('question.viewer.questionN', {
|
|
47
47
|
n: item.questionNumber || itemIndex + 1,
|
|
48
48
|
}), children: item.options.map((option) => {
|
|
49
49
|
const selected = current.optionId === option.id;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
6
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
7
7
|
function normalizeWord(word) {
|
|
8
8
|
return word.replace(/[^\p{L}\p{N}'-]/gu, '').toLowerCase();
|
|
9
9
|
}
|
|
@@ -48,7 +48,7 @@ export function CrossOutWordGroupClient({ questionData, isReviewMode = false, us
|
|
|
48
48
|
const selectedWord = userValue?.crossOut || '';
|
|
49
49
|
const replaceWith = userValue?.replaceWith || '';
|
|
50
50
|
const correct = item.correctAnswer;
|
|
51
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
51
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, headerExtra: isReviewMode ? (_jsx("span", { className: cn('rounded-full px-2 py-0.5 text-xs font-semibold', isReplace
|
|
52
52
|
? 'bg-violet-100 text-violet-700'
|
|
53
53
|
: 'bg-sky-100 text-sky-700'), children: isReplace ? 'Gạch + thay' : 'Chỉ gạch' })) : null, 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, {
|
|
54
54
|
type: item.type,
|
|
@@ -32,7 +32,9 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
32
32
|
}
|
|
33
33
|
return { cellMap: cMap, clueNumberMap: numMap };
|
|
34
34
|
}, [acrossClues, downClues]);
|
|
35
|
-
return (_jsxs("div", { className: "flex flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [
|
|
35
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [!isReviewMode ? (_jsxs("span", { className: "font-bold", children: [displayNumber, "."] })) : null, instructionText
|
|
36
|
+
? `${!isReviewMode ? ' ' : ''}${instructionText}`
|
|
37
|
+
: ''] }), _jsxs("div", { className: "flex w-full min-w-0 flex-col gap-4", children: [_jsxs("div", { className: "grid min-w-0 grid-cols-1 gap-4 md:grid-cols-2", 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) => {
|
|
36
38
|
const answer = correctAnswer?.answers?.[`across-${c.number}`];
|
|
37
39
|
const userAns = userAnswers[`across-${c.number}`];
|
|
38
40
|
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: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_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}`));
|
|
@@ -40,57 +42,58 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
40
42
|
const answer = correctAnswer?.answers?.[`down-${c.number}`];
|
|
41
43
|
const userAns = userAnswers[`down-${c.number}`];
|
|
42
44
|
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: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_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}`));
|
|
43
|
-
}) })] })] }), _jsx("div", { className: "
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const isMarked = content?.markedColumn && c === content.markedColumn;
|
|
56
|
-
const isExample = Boolean(cellInfo.across?.isExample || cellInfo.down?.isExample);
|
|
57
|
-
// If in review mode or cell is an example, display correct answer letter
|
|
58
|
-
let previewLetter = '';
|
|
59
|
-
if (isReviewMode || isExample) {
|
|
60
|
-
if (cellInfo.across && correctAnswer?.answers) {
|
|
61
|
-
const word = correctAnswer.answers[`across-${cellInfo.across.number}`] || '';
|
|
62
|
-
const offset = c - cellInfo.across.startCol;
|
|
63
|
-
previewLetter = word[offset] || '';
|
|
45
|
+
}) })] })] }), _jsx("div", { className: "w-full min-w-0 overflow-x-auto rounded-xl border border-gray-200 bg-gray-50", children: _jsx("div", { className: "flex w-max min-w-full justify-center p-4", children: _jsx("div", { className: "inline-grid shrink-0 gap-1 [--cw-cell:2rem] sm:[--cw-cell:2.25rem] md:[--cw-cell:2.5rem]", style: {
|
|
46
|
+
gridTemplateColumns: `repeat(${cols}, var(--cw-cell))`,
|
|
47
|
+
gridAutoRows: 'var(--cw-cell)',
|
|
48
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
49
|
+
const r = rIdx + 1;
|
|
50
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
51
|
+
const c = cIdx + 1;
|
|
52
|
+
const key = `${r}-${c}`;
|
|
53
|
+
const cellInfo = cellMap.get(key);
|
|
54
|
+
const clueNum = clueNumberMap.get(key);
|
|
55
|
+
if (!cellInfo) {
|
|
56
|
+
return (_jsx("div", { className: "size-[var(--cw-cell)] bg-transparent" }, key));
|
|
64
57
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
const isMarked = content?.markedColumn && c === content.markedColumn;
|
|
59
|
+
const isExample = Boolean(cellInfo.across?.isExample || cellInfo.down?.isExample);
|
|
60
|
+
// If in review mode or cell is an example, display correct answer letter
|
|
61
|
+
let previewLetter = '';
|
|
62
|
+
if (isReviewMode || isExample) {
|
|
63
|
+
if (cellInfo.across && correctAnswer?.answers) {
|
|
64
|
+
const word = correctAnswer.answers[`across-${cellInfo.across.number}`] || '';
|
|
65
|
+
const offset = c - cellInfo.across.startCol;
|
|
66
|
+
previewLetter = word[offset] || '';
|
|
67
|
+
}
|
|
68
|
+
else if (cellInfo.down && correctAnswer?.answers) {
|
|
69
|
+
const word = correctAnswer.answers[`down-${cellInfo.down.number}`] || '';
|
|
70
|
+
const offset = r - cellInfo.down.startRow;
|
|
71
|
+
previewLetter = word[offset] || '';
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
else {
|
|
75
|
+
if (cellInfo.across) {
|
|
76
|
+
const word = userAnswers[`across-${cellInfo.across.number}`] || '';
|
|
77
|
+
previewLetter = word[c - cellInfo.across.startCol] || '';
|
|
78
|
+
}
|
|
79
|
+
if (!previewLetter && cellInfo.down) {
|
|
80
|
+
const word = userAnswers[`down-${cellInfo.down.number}`] || '';
|
|
81
|
+
previewLetter = word[r - cellInfo.down.startRow] || '';
|
|
82
|
+
}
|
|
83
|
+
if (!previewLetter) {
|
|
84
|
+
previewLetter = userAnswers[key] || '';
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
|
|
88
|
+
let textColor = isExample
|
|
89
|
+
? 'text-orange-600 font-bold italic'
|
|
90
|
+
: isMarked
|
|
91
|
+
? 'text-gray-950 font-bold'
|
|
92
|
+
: 'text-gray-800 font-bold';
|
|
93
|
+
if (isReviewMode && previewLetter && !isExample) {
|
|
94
|
+
textColor = 'text-green-700 font-extrabold';
|
|
79
95
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
|
|
85
|
-
let textColor = isExample
|
|
86
|
-
? 'text-orange-600 font-bold italic'
|
|
87
|
-
: isMarked
|
|
88
|
-
? 'text-gray-950 font-bold'
|
|
89
|
-
: 'text-gray-800 font-bold';
|
|
90
|
-
if (isReviewMode && previewLetter && !isExample) {
|
|
91
|
-
textColor = 'text-green-700 font-extrabold';
|
|
92
|
-
}
|
|
93
|
-
return (_jsxs("div", { className: `relative flex items-center justify-center rounded border-2 font-bold shadow-sm h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 text-sm sm:text-base md:text-lg ${cellBg} ${textColor}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
|
|
94
|
-
});
|
|
95
|
-
}) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_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: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
|
|
96
|
+
return (_jsxs("div", { className: `relative flex size-[var(--cw-cell)] items-center justify-center rounded border-2 font-bold shadow-sm text-sm sm:text-base md:text-lg ${cellBg} ${textColor}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
|
|
97
|
+
});
|
|
98
|
+
}) }) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_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: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
|
|
96
99
|
}
|
|
@@ -9,7 +9,7 @@ import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAn
|
|
|
9
9
|
import { parseFillBlankSegments } from '../../../../shared/lib/utils/fill-in-blank';
|
|
10
10
|
import { cn } from '../../../../shared/lib/utils';
|
|
11
11
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
12
|
-
import { KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
12
|
+
import { KidPrompt, KidQuestionCard, reviewItemTitle } from '../../_shared/components/compact';
|
|
13
13
|
// Sub-component: Resolve presigned URL for group image
|
|
14
14
|
function GroupImage({ src, alt, className }) {
|
|
15
15
|
const { previewUrl, isLoading } = usePresignedFileUrl(src);
|
|
@@ -228,7 +228,7 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
|
|
|
228
228
|
const userInput = answers[index] || '';
|
|
229
229
|
const isCorrect = isReviewMode && isAnswerCorrect(index, userInput);
|
|
230
230
|
const hasAnswered = userInput.trim() !== '';
|
|
231
|
-
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${index + 1}: Điền vào chỗ trống
|
|
231
|
+
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(index + 1, isReviewMode, `Câu ${index + 1}: Điền vào chỗ trống`), review: isReviewMode && hasAnswered ? (isCorrect ? 'correct' : 'wrong') : null, children: [index === 0 && showAudio && cleanAudioUrl && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-blue-50 p-3", children: [_jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-100", children: _jsx(Volume2, { className: "h-4 w-4 text-indigo-600" }) }), _jsx("audio", { controls: true, className: "h-9 w-full", src: audioPreviewUrl || cleanAudioUrl, preload: "metadata", children: "Tr\u00ECnh duy\u1EC7t kh\u00F4ng h\u1ED7 tr\u1EE3 audio." })] })), _jsx(KidPrompt, { children: renderLegacyBlank(blank.text, blank.id, index) }), index === 0 && renderQuestionImage(), index === 0 && renderHints(), isReviewMode && (_jsx("div", { className: cn('rounded-xl border p-3 transition-colors', isCorrect
|
|
232
232
|
? 'border-emerald-200 bg-emerald-50/50'
|
|
233
233
|
: hasAnswered
|
|
234
234
|
? 'border-red-200 bg-red-50/50'
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment } from 'react';
|
|
2
|
+
import { Fragment, useState } from 'react';
|
|
3
|
+
import { useT } from '../../../../shared/lib/i18n';
|
|
3
4
|
import { acceptedAnswerList } from './blank-utils';
|
|
4
5
|
function AnswerOrList({ answers }) {
|
|
6
|
+
const t = useT();
|
|
7
|
+
const [expanded, setExpanded] = useState(false);
|
|
5
8
|
if (answers.length === 0) {
|
|
6
9
|
return _jsx("span", { className: "font-semibold text-green-900", children: "(tr\u1ED1ng)" });
|
|
7
10
|
}
|
|
8
|
-
|
|
11
|
+
const visible = expanded ? answers : answers.slice(0, 1);
|
|
12
|
+
return (_jsxs(_Fragment, { children: [visible.map((answer, index) => (_jsxs(Fragment, { children: [index > 0 && (_jsx("span", { className: "mx-1 text-xs font-normal italic text-green-600", children: "ho\u1EB7c" })), _jsx("span", { className: "font-semibold text-green-900", children: answer })] }, `${answer}-${index}`))), answers.length > 1 ? (_jsx("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "ml-1 text-xs font-semibold text-indigo-600 hover:text-indigo-800", children: expanded
|
|
13
|
+
? t('question.viewer.collapsePassage')
|
|
14
|
+
: t('question.viewer.expandPassage') })) : null] }));
|
|
9
15
|
}
|
|
10
16
|
export function FillInBlankAnswerKey({ answersByBlank, blankIndices, }) {
|
|
11
17
|
const rows = blankIndices.map((blankIndex, order) => ({
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { BookOpen, Lightbulb, PenLine, Volume2 } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { COLLAPSE_CHAR_THRESHOLD, KidCollapsibleBlock, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
7
7
|
import { FillInBlankAnswerKey } from './FillInBlankAnswerKey';
|
|
8
8
|
import { blankSlotCount, extractPlaceholderIndices, isBlankMatch, padStudentAnswers, resolveBlankOptions, splitStem, splitStemTextDecorations, toFillInBlankImageUrls, } from './blank-utils';
|
|
9
9
|
const STEM_MARK_STYLE = {
|
|
@@ -66,7 +66,9 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
|
|
|
66
66
|
next[itemIndex] = { answers: current };
|
|
67
67
|
onAnswerChange(next);
|
|
68
68
|
};
|
|
69
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), questionData.passage && (
|
|
69
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), questionData.passage && (_jsx(KidCollapsibleBlock, { collapse: isReviewMode &&
|
|
70
|
+
questionData.passage.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().length >
|
|
71
|
+
COLLAPSE_CHAR_THRESHOLD, children: _jsxs("div", { className: "rounded-xl border border-gray-200 bg-white p-5 shadow-xs", children: [_jsxs("div", { className: "mb-2.5 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-indigo-700", children: [_jsx(BookOpen, { className: "h-4 w-4" }), _jsx("span", { children: "B\u00E0i \u0111\u1ECDc" })] }), _jsx("div", { className: "prose prose-sm max-w-none leading-relaxed text-gray-800", dangerouslySetInnerHTML: { __html: questionData.passage } })] }) })), questionData.audioUrl && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2", children: [_jsx(Volume2, { className: "h-4 w-4 text-slate-500" }), _jsx(MediaPreview, { src: questionData.audioUrl, kind: "audio", alt: "Group audio" })] })), toFillInBlankImageUrls(questionData.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: toFillInBlankImageUrls(questionData.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Group image ${imageIndex + 1}` }, `group-image-${imageIndex}`))) })), groupHints.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: groupHints.map((hint) => (_jsx("span", { className: "rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700", children: hint }, hint))) })), _jsx("div", { className: "grid grid-cols-1 gap-3", children: items.map((item, itemIndex) => {
|
|
70
72
|
const blankCount = blankSlotCount(item.question || '{0}');
|
|
71
73
|
const placeholderIndices = extractPlaceholderIndices(item.question || '{0}');
|
|
72
74
|
const userValue = padStudentAnswers(userAnswers[itemIndex], blankCount);
|
|
@@ -74,7 +76,7 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
|
|
|
74
76
|
const showItemAudio = Boolean(item.audioUrl) && !questionData.audioUrl;
|
|
75
77
|
const hasAnyAnswer = userValue.some((value) => value.trim().length > 0);
|
|
76
78
|
const allBlanksCorrect = placeholderIndices.every((index) => isBlankMatch(userValue[index] || '', item.answers[index] || []));
|
|
77
|
-
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
79
|
+
return (_jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnyAnswer ? (allBlanksCorrect ? 'correct' : 'wrong') : null, children: [toFillInBlankImageUrls(item.imageUrl).length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-2", children: toFillInBlankImageUrls(item.imageUrl).map((url, imageIndex) => (_jsx(MediaPreview, { src: url, kind: "image", alt: `Item ${item.questionNumber} image ${imageIndex + 1}` }, `${item.questionNumber}-${imageIndex}`))) })), showItemAudio && item.audioUrl && (_jsx(MediaPreview, { src: item.audioUrl, kind: "audio", alt: `Item ${item.questionNumber} audio` })), _jsx(KidPrompt, { children: _jsx("div", { className: "min-w-0 text-sm leading-relaxed text-slate-800", children: stem.map((part, partIndex) => {
|
|
78
80
|
if (part.type === 'text') {
|
|
79
81
|
return (_jsx("span", { children: _jsx(StemText, { value: part.value }) }, partIndex));
|
|
80
82
|
}
|
|
@@ -193,19 +193,22 @@ export function FindWordsInMatrixClient({ content, correctAnswer, showSolution =
|
|
|
193
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) => {
|
|
194
194
|
const wordsInCat = wordBank.filter((word) => word.categoryId === cat.id);
|
|
195
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));
|
|
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: "
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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: "w-full min-w-0 overflow-x-auto pb-2", children: _jsx("div", { className: "flex w-max min-w-full justify-center", children: _jsx("div", { className: "inline-grid shrink-0 gap-1 rounded-xl border border-gray-200 bg-white p-3 shadow-sm [--fw-cell:2.25rem] sm:[--fw-cell:2.75rem]", style: {
|
|
197
|
+
gridTemplateColumns: `repeat(${cols}, var(--fw-cell))`,
|
|
198
|
+
gridAutoRows: 'var(--fw-cell)',
|
|
199
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
200
|
+
const r = rIdx + 1;
|
|
201
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
202
|
+
const c = cIdx + 1;
|
|
203
|
+
const char = grid[rIdx]?.[cIdx] || '';
|
|
204
|
+
const highlight = cellColorMap[`${r}-${c}`];
|
|
205
|
+
const colorConfig = highlight ? WORD_COLORS[highlight.colorIdx] : null;
|
|
206
|
+
const isTapped = tappedPath.some((cell) => cell.row === r && cell.col === c);
|
|
207
|
+
return (_jsx("button", { type: "button", disabled: !canSelect, onClick: () => handleCellClick(r, c), className: `flex size-[var(--fw-cell)] items-center justify-center rounded-md border text-sm font-bold sm:text-base ${colorConfig
|
|
208
|
+
? `${colorConfig.bg} shadow-sm`
|
|
209
|
+
: isTapped
|
|
210
|
+
? 'border-blue-500 bg-blue-50 text-blue-800'
|
|
211
|
+
: 'border-gray-100 bg-gray-50/50 text-gray-700'}`, title: highlight ? `${highlight.word} (${r},${c})` : `(${r},${c})`, children: char || '·' }, `${r}-${c}`));
|
|
212
|
+
});
|
|
213
|
+
}) }) }) })] }));
|
|
211
214
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { assembleMatchColumnsSentence, getMatchColumnsChoiceColumns, getMatchColumnsStemColumn, } from './map-match-columns-to-make-sentences-data';
|
|
6
|
-
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, } from '../../_shared/components/compact';
|
|
6
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
7
7
|
function emptyAnswer(stemFragmentId) {
|
|
8
8
|
return { fragmentIds: stemFragmentId ? [stemFragmentId] : [], sentence: '' };
|
|
9
9
|
}
|
|
@@ -59,7 +59,7 @@ export function MatchColumnsToMakeSentencesClient({ questionData, isReviewMode =
|
|
|
59
59
|
|| '—';
|
|
60
60
|
const isCorrect = isReviewMode &&
|
|
61
61
|
current.sentence.trim().toLowerCase() === item.sentence.trim().toLowerCase();
|
|
62
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
62
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: [_jsx(KidPrompt, { children: stemText }), choiceColumns.map((column, choiceIndex) => {
|
|
63
63
|
const selectedId = current.fragmentIds[choiceIndex + 1] || '';
|
|
64
64
|
const correctId = item.fragmentIds[choiceIndex + 1] || '';
|
|
65
65
|
return (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wider text-gray-500", children: column.label || column.id }), column.fragments.map((fragment, fragmentIndex) => {
|
package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
|
-
import { KidChoiceRow, KidInstruction, KidQuestionCard, optionLetter, } from '../../_shared/components/compact';
|
|
6
|
+
import { KidChoiceRow, KidInstruction, KidQuestionCard, optionLetter, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
7
7
|
function normalizeWord(value) {
|
|
8
8
|
return value.trim().toLowerCase();
|
|
9
9
|
}
|
|
@@ -53,7 +53,7 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
53
53
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
54
54
|
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
55
55
|
Boolean(item.correctAnswer);
|
|
56
|
-
return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
56
|
+
return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [_jsx(ClientImageItem, { url: item.imageUrl, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}` }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), wordBank.map((word, wordIndex) => {
|
|
57
57
|
const isSelected = normalizeWord(word) === normalizeWord(selectedWord);
|
|
58
58
|
const isUsedByOther = usedWords.has(normalizeWord(word)) && !isSelected;
|
|
59
59
|
const isWordCorrect = normalizeWord(word) === normalizeWord(item.correctAnswer);
|
package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js
CHANGED
|
@@ -4,7 +4,7 @@ import { BookOpen, Check, ChevronDown, Lightbulb, Volume2, X } from 'lucide-reac
|
|
|
4
4
|
import { cn } from '../../../../shared/lib/utils';
|
|
5
5
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
6
6
|
import { toMatchWordToPictureImageUrls, toMatchWordToPictureStorageKey, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
7
|
-
import { KidInstruction, KidPrompt, KidQuestionCard, } from '../../_shared/components/compact';
|
|
7
|
+
import { KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle, } from '../../_shared/components/compact';
|
|
8
8
|
function normalizeWord(value) {
|
|
9
9
|
return value.trim().toLowerCase();
|
|
10
10
|
}
|
|
@@ -76,7 +76,7 @@ export function MatchWordToPictureGroupClient({ questionData, isReviewMode = fal
|
|
|
76
76
|
const isCorrect = normalizeWord(selectedWord) === normalizeWord(item.correctAnswer) &&
|
|
77
77
|
Boolean(item.correctAnswer);
|
|
78
78
|
const itemImages = toMatchWordToPictureImageUrls(item.imageUrl);
|
|
79
|
-
return (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
79
|
+
return (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample ? (isCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [itemImages.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-2", children: itemImages.map((url, imageIndex) => (_jsx(ClientImageItem, { url: url, alt: `Hình ảnh câu ${item.questionNumber || itemIndex + 1}${imageIndex > 0 ? ` (${imageIndex + 1})` : ''}` }, `${item.questionNumber}-${imageIndex}`))) })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: "Ch\u1ECDn t\u1EEB ph\u00F9 h\u1EE3p" }), item.isExample ? (_jsx("span", { className: "text-sm italic text-gray-800 underline decoration-gray-400", children: item.correctAnswer })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(WordDropdown, { value: selectedWord, wordBank: wordBank, usedWords: usedWords, disabled: !canSelect, onChange: (value) => handleSelect(itemIndex, value) }), isReviewMode && (isCorrect
|
|
80
80
|
? _jsx(Check, { className: "h-5 w-5 text-emerald-600" })
|
|
81
81
|
: _jsx(X, { className: "h-5 w-5 text-red-500" }))] })), isReviewMode && !item.isExample && !isCorrect && (_jsxs("p", { className: "text-sm text-green-700", children: ["\u0110\u00E1p \u00E1n \u0111\u00FAng: ", _jsx("strong", { children: item.correctAnswer || '—' })] }))] })] }) }, `${item.questionNumber}-${itemIndex}`));
|
|
82
82
|
}), 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] }))] }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { Mic, Volume2 } from 'lucide-react';
|
|
4
4
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
5
5
|
import { KidInstruction, KidPrompt, KidQuestionCard, SpeakingReplyMic, } from '../../_shared/components/compact';
|
|
6
6
|
function QuestionAudio({ url }) {
|
|
@@ -44,12 +44,20 @@ export function SpontaneousQaGroupClient({ questionData, isReviewMode = false, u
|
|
|
44
44
|
const detail = findGradeDetail(gradeDetails, currentIndex);
|
|
45
45
|
const grade = readGradeUser(detail);
|
|
46
46
|
const showMic = !isReviewMode && !question.isExample;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
const itemNumber = question.questionNumber || questionIndex + 1;
|
|
48
|
+
const scoreLabel = detail?.partialScore != null
|
|
49
|
+
? `${detail.partialScore} / ${question.points || 0}`
|
|
50
|
+
: null;
|
|
51
|
+
if (isReviewMode) {
|
|
52
|
+
return (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-slate-200 bg-white px-3 py-2.5", children: [_jsx("span", { className: "mt-0.5 w-6 shrink-0 text-sm font-extrabold text-sky-800", children: itemNumber }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx("p", { className: "text-sm font-medium leading-snug text-slate-800", children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, detail && !question.isExample ? (_jsx("p", { className: "line-clamp-2 text-xs text-slate-600", children: [
|
|
53
|
+
grade.feedback,
|
|
54
|
+
grade.contentScore != null
|
|
55
|
+
? `Content ${Math.round(grade.contentScore)}`
|
|
56
|
+
: null,
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(' · ') })) : null] }), scoreLabel ? (_jsx("span", { className: "shrink-0 text-xs font-semibold text-slate-500", children: scoreLabel })) : null] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs(KidQuestionCard, { title: `Câu ${itemNumber}`, points: detail?.partialScore != null ? undefined : (question.points || 0), isExample: question.isExample, headerExtra: detail?.partialScore != null ? (_jsxs("span", { className: "text-xs text-gray-500", children: [detail.partialScore, " / ", question.points || 0, " \u0111i\u1EC3m"] })) : undefined, children: [_jsx(KidPrompt, { children: question.questionText || 'Câu hỏi chưa được nhập' }), question.audioUrl ? _jsx(QuestionAudio, { url: question.audioUrl }) : null, showMic && (_jsx(SpeakingReplyMic, { flattenIndex: currentIndex, audioKey: typeof audioKey === 'string' ? audioKey : null, disabled: disabled, uploading: uploadingIndex === currentIndex, onRecorded: onItemRecorded }))] }, `${part.partNumber}-${question.questionNumber}-${questionIndex}`));
|
|
54
62
|
})] }, `part-${part.partNumber}-${partIndex}`)))] }));
|
|
55
63
|
}
|
package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
|
-
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
4
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
5
5
|
function emptyAnswer() {
|
|
6
6
|
return { isTrue: null, correction: '' };
|
|
7
7
|
}
|
|
@@ -25,7 +25,7 @@ export function TrueFalseCorrectGroupClient({ questionData, isReviewMode = false
|
|
|
25
25
|
const showStudentCorrection = userValue?.isTrue === false || (isReviewMode && !correctIsTrue);
|
|
26
26
|
const hasAnswered = userValue?.isTrue === true || userValue?.isTrue === false;
|
|
27
27
|
const isAnswerCorrect = userValue?.isTrue === correctIsTrue;
|
|
28
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
28
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
|
|
29
29
|
? (isAnswerCorrect ? 'correct' : 'wrong')
|
|
30
30
|
: null, children: [_jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(KidChoiceRow, { letter: "T", selected: userValue?.isTrue === true, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctIsTrue, disabled: !canSelect, onClick: canSelect
|
|
31
31
|
? () => emit(itemIndex, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
|
-
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
4
|
+
import { KidChoiceRow, KidInstruction, KidPrompt, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
5
5
|
export function TrueFalseGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
6
6
|
const items = questionData.items || [];
|
|
7
7
|
const handleSelect = (itemIndex, value) => {
|
|
@@ -19,7 +19,7 @@ export function TrueFalseGroupClient({ questionData, isReviewMode = false, userA
|
|
|
19
19
|
const canSelect = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
20
20
|
const hasAnswered = userValue !== undefined;
|
|
21
21
|
const isAnswerCorrect = userValue === correctAnswer;
|
|
22
|
-
return (_jsxs(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
22
|
+
return (_jsxs(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, review: isReviewMode && !item.isExample && hasAnswered
|
|
23
23
|
? (isAnswerCorrect ? 'correct' : 'wrong')
|
|
24
24
|
: null, children: [_jsx(KidPrompt, { children: item.question || 'Câu hỏi chưa được nhập' }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(KidChoiceRow, { letter: "T", selected: userValue === true, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctAnswer === true, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, true) : undefined, children: "\u0110\u00FAng" }), _jsx(KidChoiceRow, { letter: "F", selected: userValue === false, isReviewMode: isReviewMode, isCorrect: isReviewMode && correctAnswer === false, disabled: !canSelect, onClick: canSelect ? () => handleSelect(itemIndex, false) : undefined, children: "Sai" })] })] }, `${item.questionNumber}-${itemIndex}`));
|
|
25
25
|
}), 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] }))] }));
|
package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js
CHANGED
|
@@ -132,7 +132,9 @@ export function WordFillStructuredFormClient({ questionData, isReviewMode = fals
|
|
|
132
132
|
const rawExplanation = String(explanation || questionData.explanation || '').trim();
|
|
133
133
|
const headerInstruction = String(instruction || dataInstruction || rawExplanation || '').trim();
|
|
134
134
|
// Header reads "Bài x: rubric" — the question type name is never shown.
|
|
135
|
-
const numberLabel =
|
|
135
|
+
const numberLabel = !isReviewMode &&
|
|
136
|
+
typeof questionNumber === 'number' &&
|
|
137
|
+
questionNumber > 0
|
|
136
138
|
? t('question.viewer.partN', { n: questionNumber })
|
|
137
139
|
: '';
|
|
138
140
|
const headerTitle = numberLabel ? (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "font-bold", children: [numberLabel, headerInstruction ? ':' : ''] }), headerInstruction ? ` ${headerInstruction}` : ''] })) : (headerInstruction || t('question.viewer.wfsfDefaultTitle'));
|
package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { X, RotateCcw } from 'lucide-react';
|
|
5
5
|
import { cn } from '../../../../shared/lib/utils';
|
|
6
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
6
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
7
7
|
export function WordOrderAndMatchGroupClient({ questionData, isReviewMode = false, userAnswers, onAnswerChange, }) {
|
|
8
8
|
const { instruction, matchOptions = ['A', 'B', 'C', 'D', 'E', 'F'], items = [] } = questionData;
|
|
9
9
|
// Initialize interactive state for each item
|
|
@@ -152,7 +152,9 @@ export function WordOrderAndMatchGroupClient({ questionData, isReviewMode = fals
|
|
|
152
152
|
const isPhraseCorrect = normalizeStr(userPhrase) === normalizeStr(item.orderedPhrase || '');
|
|
153
153
|
const isLabelCorrect = (userLabel || '').trim().toUpperCase() === (item.matchLabel || '').trim().toUpperCase();
|
|
154
154
|
const isItemAllCorrect = isPhraseCorrect && isLabelCorrect;
|
|
155
|
-
return (_jsx(KidQuestionCard, { title: isExample
|
|
155
|
+
return (_jsx(KidQuestionCard, { title: isExample
|
|
156
|
+
? 'Ví dụ (Example)'
|
|
157
|
+
: reviewItemTitle(item.questionNumber || itemIdx + 1, isReviewMode, `Câu ${item.questionNumber || itemIdx + 1}`), points: reviewItemPoints(isReviewMode, !isExample && typeof item.points === 'number' ? item.points : undefined), isExample: isExample, review: isReviewMode && !isExample ? (isItemAllCorrect ? 'correct' : 'wrong') : null, children: _jsxs("div", { className: "grid grid-cols-1 gap-4 lg:grid-cols-12", children: [_jsxs("div", { className: "space-y-3 lg:col-span-8 xl:col-span-9", children: [_jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wider", children: "1. S\u1EAFp x\u1EBFp c\u00E1c t\u1EEB th\u00E0nh c\u00E2u ho\u00E0n ch\u1EC9nh:" }), _jsxs("div", { className: cn('min-h-[52px] rounded-xl border-2 border-dashed p-2.5 transition-colors flex flex-wrap items-center gap-1.5', state.placedIndices.length > 0
|
|
156
158
|
? 'border-blue-300 bg-blue-50/40'
|
|
157
159
|
: 'border-gray-300 bg-gray-50/60'), children: [state.placedIndices.length === 0 ? (_jsx("span", { className: "text-xs italic text-gray-400 select-none", children: "Nh\u1EA5n v\u00E0o c\u00E1c t\u1EEB b\u00EAn d\u01B0\u1EDBi \u0111\u1EC3 gh\u00E9p c\u00E2u..." })) : (state.placedIndices.map((wordIdx, orderIdx) => {
|
|
158
160
|
const word = item.scrambledWords[wordIdx];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Lightbulb } from 'lucide-react';
|
|
4
4
|
import { WordOrderingClient } from '../word-ordering/WordOrderingClient';
|
|
5
|
-
import { KidInstruction, KidQuestionCard } from '../../_shared/components/compact';
|
|
5
|
+
import { KidInstruction, KidQuestionCard, reviewItemPoints, reviewItemTitle } from '../../_shared/components/compact';
|
|
6
6
|
export function WordOrderingGroupClient({ questionData, isReviewMode = false, userAnswers = [], onAnswerChange, }) {
|
|
7
7
|
const items = questionData.items || [];
|
|
8
8
|
const handleItemSubmit = (itemIndex, ordered) => {
|
|
@@ -17,7 +17,7 @@ export function WordOrderingGroupClient({ questionData, isReviewMode = false, us
|
|
|
17
17
|
});
|
|
18
18
|
onAnswerChange(next);
|
|
19
19
|
};
|
|
20
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => (_jsx(KidQuestionCard, { title: `Câu ${item.questionNumber || itemIndex + 1}
|
|
20
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), items.map((item, itemIndex) => (_jsx(KidQuestionCard, { title: reviewItemTitle(item.questionNumber || itemIndex + 1, isReviewMode, `Câu ${item.questionNumber || itemIndex + 1}`), points: reviewItemPoints(isReviewMode, item.points || 0), isExample: item.isExample, children: _jsx(WordOrderingClient, { questionData: {
|
|
21
21
|
words: item.words,
|
|
22
22
|
answer: item.answer,
|
|
23
23
|
isExample: item.isExample,
|
|
@@ -631,7 +631,8 @@ export function QuestionViewer({ question, onSubmitAnswer, isReviewMode = false,
|
|
|
631
631
|
question.type === 'ANSWER_THE_QUESTION') {
|
|
632
632
|
return (_jsx("div", { className: "space-y-4", children: renderQuestion() }));
|
|
633
633
|
}
|
|
634
|
-
|
|
634
|
+
const showViewerHeader = Boolean(cardTitle) && !isReviewMode;
|
|
635
|
+
return (_jsxs(Card, { children: [showViewerHeader ? (_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center justify-between", children: [_jsx("span", { className: "min-w-0 pr-2 text-base font-semibold leading-snug", children: cardTitle }), _jsx("span", { className: "shrink-0 text-sm font-normal text-gray-500", children: translate('question.viewer.points', { count: question.points }) })] }) })) : null, _jsx(CardContent, { className: showViewerHeader ? 'space-y-4' : 'space-y-4 pt-4', children: renderQuestion() })] }));
|
|
635
636
|
}
|
|
636
637
|
/** Drop lightweight markdown markers for plain header labels (not full token parse). */
|
|
637
638
|
function stripLightweightMarkdown(text) {
|