@tinyweb_dev/oe-exam-sdk 1.0.9 → 1.0.11
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/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +7 -7
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchWordToPictureGroupRenderer.js +36 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchingWithLinesGroupRenderer.js +4 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
- package/dist/components/exams/take/utils/answer-transformers.js +17 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +32 -3
- package/dist/components/exams/take/utils/question-transformers.js +51 -2
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +14 -0
- package/dist/components/questions/_shared/types/matching-with-lines-group.type.d.ts +2 -2
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +8 -7
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +5 -4
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -30
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +8 -7
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +50 -32
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
- package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
- package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
- package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
- package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
- package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +8 -6
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
- package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +71 -28
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
- package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +24 -35
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +16 -3
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.d.ts +2 -1
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupImageField.js +25 -6
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesBoard.js +1 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +22 -10
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupCreator.js +60 -18
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.d.ts +2 -1
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesImageField.js +24 -5
- package/dist/components/questions/types/matching-with-lines-group/map-matching-with-lines-group-data.js +8 -4
- package/dist/components/questions/types/matching-with-lines-group/transform.js +5 -4
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
- package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
- package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +21 -23
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +8 -17
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +1 -4
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
- package/dist/components/questions/viewer/QuestionViewer.js +2 -2
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/results/renderers/review-question-body-movers.js +7 -3
- package/dist/components/results/review-data.js +20 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +48 -7
- package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
- package/dist/shared/types/questions/matching-with-lines-group.d.ts +2 -2
- package/package.json +1 -1
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useMemo } from 'react';
|
|
4
4
|
import { Input } from '../../../../components/ui/input';
|
|
5
|
-
import { Check, X, PenLine,
|
|
5
|
+
import { Check, X, PenLine, ClipboardList, Lightbulb, BookOpen, Hash, Volume2, } from 'lucide-react';
|
|
6
6
|
import { ImageLabelWordBank } from '../../groups/ImageLabelWordBank';
|
|
7
7
|
import { hasImageLabels } from '../../_shared/types/question-group.utils';
|
|
8
8
|
import { extractPlaceholderIndices, formatBlankAnswersDisplay, getPrimaryBlankAnswer, isBlankInputCorrect, normalizeBlankAnswers, normalizeFillInBlankAnswers, } from './answer-utils';
|
|
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
13
|
// Sub-component: Resolve presigned URL for group image
|
|
13
14
|
function GroupImage({ src, alt, className }) {
|
|
14
15
|
const { previewUrl, isLoading } = usePresignedFileUrl(src);
|
|
@@ -143,11 +144,11 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
|
|
|
143
144
|
const userInput = answers[placeholderIndex] || '';
|
|
144
145
|
const isCorrect = isReviewMode && isAnswerCorrect(placeholderIndex, userInput);
|
|
145
146
|
const isWrong = isReviewMode && userInput && !isCorrect;
|
|
146
|
-
return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx("span", { className: "flex h-5 w-5 items-center justify-center rounded-md bg-orange-100 text-[10px] font-bold text-orange-600 mr-1 ring-1 ring-orange-200/60", children: placeholderIndex + 1 }), _jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(placeholderIndex, e.target.value), disabled: isReviewMode, className: cn('h-
|
|
147
|
+
return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx("span", { className: "flex h-5 w-5 items-center justify-center rounded-md bg-orange-100 text-[10px] font-bold text-orange-600 mr-1 ring-1 ring-orange-200/60", children: placeholderIndex + 1 }), _jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(placeholderIndex, e.target.value), disabled: isReviewMode, className: cn('h-8 w-28 text-center text-sm font-medium rounded-md border px-3 py-1.5 transition-all duration-200', 'focus:ring-2 focus:ring-offset-1 focus:outline-none', isReviewMode
|
|
147
148
|
? isCorrect
|
|
148
|
-
? 'border-
|
|
149
|
+
? 'border-green-400 bg-green-50 text-green-800'
|
|
149
150
|
: isWrong
|
|
150
|
-
? 'border-red-400 bg-red-50 text-red-
|
|
151
|
+
? 'border-red-400 bg-red-50 text-red-800'
|
|
151
152
|
: 'border-gray-200 bg-gray-50 text-gray-500'
|
|
152
153
|
: 'border-orange-200 bg-white hover:border-orange-300 focus:border-orange-400 focus:ring-orange-200/50'), placeholder: "..." }), isReviewMode && (isCorrect || isWrong) && (_jsx("span", { className: cn('absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full shadow-sm ring-2 ring-white', isCorrect ? 'bg-emerald-500' : 'bg-red-500'), children: isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5 text-white" })) : (_jsx(X, { className: "h-2.5 w-2.5 text-white" })) }))] }, partIndex));
|
|
153
154
|
}
|
|
@@ -162,11 +163,11 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
|
|
|
162
163
|
const userInput = answers[blankIndex] || '';
|
|
163
164
|
const isCorrect = isReviewMode && isAnswerCorrect(blankIndex, userInput);
|
|
164
165
|
const isWrong = isReviewMode && userInput && !isCorrect;
|
|
165
|
-
return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(blankIndex, e.target.value), disabled: isReviewMode, className: cn('h-
|
|
166
|
+
return (_jsxs("span", { className: "relative inline-flex items-center mx-0.5", children: [_jsx(Input, { value: userInput, onChange: (e) => handleAnswerChange(blankIndex, e.target.value), disabled: isReviewMode, className: cn('h-8 w-32 text-center text-sm font-medium rounded-md border px-3 py-1.5 transition-all duration-200', 'focus:ring-2 focus:ring-offset-1 focus:outline-none', isReviewMode
|
|
166
167
|
? isCorrect
|
|
167
|
-
? 'border-
|
|
168
|
+
? 'border-green-400 bg-green-50 text-green-800'
|
|
168
169
|
: isWrong
|
|
169
|
-
? 'border-red-400 bg-red-50 text-red-
|
|
170
|
+
? 'border-red-400 bg-red-50 text-red-800'
|
|
170
171
|
: 'border-gray-200 bg-gray-50 text-gray-500'
|
|
171
172
|
: 'border-orange-200 bg-white hover:border-orange-300 focus:border-orange-400 focus:ring-orange-200/50'), placeholder: "..." }), isReviewMode && (isCorrect || isWrong) && (_jsx("span", { className: cn('absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full shadow-sm ring-2 ring-white', isCorrect ? 'bg-emerald-500' : 'bg-red-500'), children: isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5 text-white" })) : (_jsx(X, { className: "h-2.5 w-2.5 text-white" })) }))] }, partIdx));
|
|
172
173
|
}
|
|
@@ -208,21 +209,17 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
|
|
|
208
209
|
// =====================================================
|
|
209
210
|
if (!isLegacyFormat) {
|
|
210
211
|
const hasAnyAnswer = placeholderIndices.some(idx => (answers[idx] || '').trim());
|
|
211
|
-
return (_jsxs("div", { className: "space-y-4", children: [renderGroupBlock(), _jsxs(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
: hasInput
|
|
223
|
-
? 'border-red-200 bg-red-50/50'
|
|
224
|
-
: 'border-gray-100 bg-gray-50/50'), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-orange-100 text-xs font-bold text-orange-600 ring-1 ring-orange-200/60", children: idx + 1 }), _jsxs("span", { className: "text-sm text-gray-500", children: ["Ch\u1ED7 tr\u1ED1ng ", idx + 1] })] }), _jsx("div", { className: cn('flex items-center gap-1 text-xs font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3.5 w-3.5" }), " \u0110\u00FAng"] })) : hasInput ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-3.5 w-3.5" }), " Sai"] })) : ('Bỏ trống') })] }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white px-3 py-2 border border-gray-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E3 nh\u1EADp" }), _jsx("span", { className: cn('font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 px-3 py-2 border border-emerald-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E1p \u00E1n" }), _jsx("span", { className: "font-semibold text-emerald-600", children: correctAnswer })] })] })] }, idx));
|
|
225
|
-
}) })] })), !isReviewMode && !hasAnyAnswer && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx(AlertTriangle, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi v\u00E0o c\u00E1c ch\u1ED7 tr\u1ED1ng" })] }))] })] }), renderExplanation()] }));
|
|
212
|
+
return (_jsxs("div", { className: "space-y-4", children: [renderGroupBlock(), _jsxs(KidQuestionCard, { icon: _jsx(PenLine, { className: "h-3.5 w-3.5" }), title: "\u0110i\u1EC1n v\u00E0o ch\u1ED7 tr\u1ED1ng", points: questionData.points, isExample: questionData.isExample, review: isReviewMode && hasAnyAnswer ? (score && score.correct === score.total ? 'correct' : 'wrong') : null, headerExtra: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700", children: [_jsx(Hash, { className: "h-3 w-3" }), placeholderIndices.length] }), children: [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: renderQuestionWithBlanks() }), renderQuestionImage(), renderHints(), isReviewMode && (_jsxs("div", { className: "rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-gray-100 bg-gradient-to-r from-slate-50 to-gray-50 px-4 py-2.5", children: [_jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-slate-500 text-white", children: _jsx(ClipboardList, { className: "h-3 w-3" }) }), _jsx("h4", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wider", children: "Chi ti\u1EBFt \u0111\u00E1p \u00E1n" })] }), _jsx("div", { className: "p-4 space-y-2.5", children: placeholderIndices.map((idx) => {
|
|
213
|
+
const userInput = answers[idx] || '';
|
|
214
|
+
const correctAnswer = formatBlankAnswersDisplay(correctAnswersMatrix[idx]) || '—';
|
|
215
|
+
const isCorrect = isAnswerCorrect(idx, userInput);
|
|
216
|
+
const hasInput = userInput.trim().length > 0;
|
|
217
|
+
return (_jsxs("div", { className: cn('rounded-lg border p-3 transition-colors', isCorrect
|
|
218
|
+
? 'border-emerald-200 bg-emerald-50/50'
|
|
219
|
+
: hasInput
|
|
220
|
+
? 'border-red-200 bg-red-50/50'
|
|
221
|
+
: 'border-gray-100 bg-gray-50/50'), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-orange-100 text-xs font-bold text-orange-600 ring-1 ring-orange-200/60", children: idx + 1 }), _jsxs("span", { className: "text-sm text-gray-500", children: ["Ch\u1ED7 tr\u1ED1ng ", idx + 1] })] }), _jsx("div", { className: cn('flex items-center gap-1 text-xs font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3.5 w-3.5" }), " \u0110\u00FAng"] })) : hasInput ? (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-3.5 w-3.5" }), " Sai"] })) : ('Bỏ trống') })] }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white px-3 py-2 border border-gray-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E3 nh\u1EADp" }), _jsx("span", { className: cn('font-semibold', isCorrect ? 'text-emerald-600' : hasInput ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 px-3 py-2 border border-emerald-100", children: [_jsx("span", { className: "text-gray-500 text-xs", children: "\u0110\u00E1p \u00E1n" }), _jsx("span", { className: "font-semibold text-emerald-600", children: correctAnswer })] })] })] }, idx));
|
|
222
|
+
}) })] })), !isReviewMode && !hasAnyAnswer && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi v\u00E0o c\u00E1c ch\u1ED7 tr\u1ED1ng" })] }))] }), renderExplanation()] }));
|
|
226
223
|
}
|
|
227
224
|
// =====================================================
|
|
228
225
|
// LEGACY FORMAT RENDER
|
|
@@ -231,15 +228,13 @@ export function FillInBlankClient({ questionData, questionConfig, onSubmit, isRe
|
|
|
231
228
|
const userInput = answers[index] || '';
|
|
232
229
|
const isCorrect = isReviewMode && isAnswerCorrect(index, userInput);
|
|
233
230
|
const hasAnswered = userInput.trim() !== '';
|
|
234
|
-
return (_jsxs(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
? '
|
|
240
|
-
: '
|
|
241
|
-
? 'bg-emerald-50 border-emerald-200'
|
|
242
|
-
: 'bg-red-50 border-red-200'), children: [_jsx("span", { className: "text-sm text-gray-500", children: "K\u1EBFt qu\u1EA3:" }), _jsx("div", { className: cn('flex items-center gap-1.5 text-sm font-bold', isCorrect ? 'text-emerald-600' : 'text-red-600'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4" }), " Ch\u00EDnh x\u00E1c"] })) : (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-4 w-4" }), " Sai"] })) })] })] }) })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 p-3 flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx(AlertTriangle, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-sm text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi" })] }))] })] }, blank.id));
|
|
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`, 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
|
+
? 'border-emerald-200 bg-emerald-50/50'
|
|
233
|
+
: hasAnswered
|
|
234
|
+
? 'border-red-200 bg-red-50/50'
|
|
235
|
+
: 'border-gray-100 bg-gray-50/50'), children: _jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md bg-white p-3 border border-gray-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E3 nh\u1EADp:" }), _jsx("span", { className: cn('text-sm font-semibold', isCorrect ? 'text-emerald-600' : hasAnswered ? 'text-red-600' : 'text-gray-400 italic'), children: userInput || '—' })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md bg-emerald-50 p-3 border border-emerald-100", children: [_jsx("span", { className: "text-sm text-gray-500", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng:" }), _jsx("span", { className: "text-sm font-semibold text-emerald-600", children: formatBlankAnswersDisplay(blank.correctAnswers ?? normalizeBlankAnswers(blank.correctAnswer)) || '—' })] }), _jsxs("div", { className: cn('flex items-center justify-between rounded-md p-3 border', isCorrect
|
|
236
|
+
? 'bg-emerald-50 border-emerald-200'
|
|
237
|
+
: 'bg-red-50 border-red-200'), children: [_jsx("span", { className: "text-sm text-gray-500", children: "K\u1EBFt qu\u1EA3:" }), _jsx("div", { className: cn('flex items-center gap-1.5 text-sm font-bold', isCorrect ? 'text-emerald-600' : 'text-red-600'), children: isCorrect ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-4 w-4" }), " Ch\u00EDnh x\u00E1c"] })) : (_jsxs(_Fragment, { children: [_jsx(X, { className: "h-4 w-4" }), " Sai"] })) })] })] }) })), !isReviewMode && !hasAnswered && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5", children: [_jsx("div", { className: "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-white", children: _jsx("span", { className: "text-[10px] font-bold", children: "!" }) }), _jsx("span", { className: "text-xs text-amber-800", children: "Vui l\u00F2ng \u0111i\u1EC1n c\u00E2u tr\u1EA3 l\u1EDDi" })] }))] }, blank.id));
|
|
243
238
|
}), isReviewMode && score && (questionData.blanks?.length || 0) > 1 && (_jsx("div", { className: cn('overflow-hidden rounded-xl border shadow-sm', score.correct === score.total
|
|
244
239
|
? 'border-emerald-200 bg-gradient-to-r from-emerald-50 to-green-50'
|
|
245
240
|
: score.correct >= score.total / 2
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
import { acceptedAnswerList } from './blank-utils';
|
|
4
|
+
function AnswerOrList({ answers }) {
|
|
5
|
+
if (answers.length === 0) {
|
|
6
|
+
return _jsx("span", { className: "font-semibold text-green-900", children: "(tr\u1ED1ng)" });
|
|
7
|
+
}
|
|
8
|
+
return (_jsx(_Fragment, { children: answers.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}`))) }));
|
|
9
|
+
}
|
|
10
|
+
export function FillInBlankAnswerKey({ answersByBlank, blankIndices, }) {
|
|
11
|
+
const rows = blankIndices.map((blankIndex, order) => ({
|
|
12
|
+
order: order + 1,
|
|
13
|
+
answers: acceptedAnswerList(answersByBlank[blankIndex]),
|
|
14
|
+
}));
|
|
15
|
+
const manyBlanks = rows.length > 1;
|
|
16
|
+
return (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-xs font-semibold text-green-800", children: "\u0110\u00E1p \u00E1n \u0111\u00FAng" }), _jsx("div", { className: "grid grid-cols-2 gap-1.5", children: rows.map((row) => (_jsxs("div", { className: "min-w-0 rounded-md border border-green-200 bg-white px-2 py-1.5 text-sm", children: [manyBlanks && (_jsxs("div", { className: "text-[10px] font-semibold uppercase tracking-wide text-green-600", children: ["Ch\u1ED7 tr\u1ED1ng ", row.order] })), _jsx("div", { className: "flex flex-wrap items-baseline", children: _jsx(AnswerOrList, { answers: row.answers }) })] }, row.order))) })] }));
|
|
17
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, Lightbulb,
|
|
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 {
|
|
6
|
+
import { KidInstruction, KidPrompt, KidQuestionCard } from '../../_shared/components/compact';
|
|
7
|
+
import { FillInBlankAnswerKey } from './FillInBlankAnswerKey';
|
|
8
|
+
import { blankSlotCount, extractPlaceholderIndices, isBlankMatch, padStudentAnswers, resolveBlankOptions, splitStem, splitStemTextDecorations, toFillInBlankImageUrls, } from './blank-utils';
|
|
7
9
|
const STEM_MARK_STYLE = {
|
|
8
10
|
fontWeight: 700,
|
|
9
11
|
fontSize: 20,
|
|
@@ -64,36 +66,38 @@ export function FillInBlankGroupClient({ questionData, isReviewMode = false, use
|
|
|
64
66
|
next[itemIndex] = { answers: current };
|
|
65
67
|
onAnswerChange(next);
|
|
66
68
|
};
|
|
67
|
-
return (_jsxs("div", { className: "space-y-4", children: [
|
|
69
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(KidInstruction, { children: questionData.instruction }), 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" })] }), _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) => {
|
|
68
70
|
const blankCount = blankSlotCount(item.question || '{0}');
|
|
69
71
|
const placeholderIndices = extractPlaceholderIndices(item.question || '{0}');
|
|
70
72
|
const userValue = padStudentAnswers(userAnswers[itemIndex], blankCount);
|
|
71
73
|
const stem = splitStem(item.question || '{0}');
|
|
72
74
|
const showItemAudio = Boolean(item.audioUrl) && !questionData.audioUrl;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
75
|
+
const hasAnyAnswer = userValue.some((value) => value.trim().length > 0);
|
|
76
|
+
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}`, points: 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
|
+
if (part.type === 'text') {
|
|
79
|
+
return (_jsx("span", { children: _jsx(StemText, { value: part.value }) }, partIndex));
|
|
80
|
+
}
|
|
81
|
+
const alts = item.answers[part.index] || [];
|
|
82
|
+
const student = userValue[part.index] || '';
|
|
83
|
+
const canEdit = !isReviewMode && !item.isExample && Boolean(onAnswerChange);
|
|
84
|
+
const correct = isReviewMode && isBlankMatch(student, alts);
|
|
85
|
+
const wrong = isReviewMode && student.trim().length > 0 && !correct;
|
|
86
|
+
const blankOptions = resolveBlankOptions(part.index, item.wordBank, questionData.wordBank);
|
|
87
|
+
const useOptions = Boolean(blankOptions);
|
|
88
|
+
const fieldClass = cn('mx-1 inline-block rounded-md border px-3 py-1.5 text-center text-sm font-semibold outline-none', useOptions ? 'min-w-[5.5rem]' : 'w-16', item.isExample
|
|
89
|
+
? 'border-amber-300 bg-amber-50 text-amber-800'
|
|
90
|
+
: correct
|
|
91
|
+
? 'border-green-400 bg-green-50 text-green-800'
|
|
92
|
+
: wrong
|
|
93
|
+
? 'border-red-400 bg-red-50 text-red-800'
|
|
94
|
+
: canEdit
|
|
95
|
+
? 'border-blue-300 bg-white text-slate-800 focus:border-blue-500'
|
|
96
|
+
: 'border-blue-200 bg-blue-50 text-blue-900');
|
|
97
|
+
if (useOptions && blankOptions) {
|
|
98
|
+
return (_jsxs("select", { disabled: !canEdit, value: item.isExample ? (alts[0] || '') : student, onChange: (event) => handleBlankChange(itemIndex, part.index, event.target.value), className: fieldClass, children: [!item.isExample && _jsx("option", { value: "", children: "..." }), blankOptions.map((option) => (_jsx("option", { value: option, children: option }, option)))] }, partIndex));
|
|
99
|
+
}
|
|
100
|
+
return (_jsx("input", { type: "text", disabled: !canEdit, value: item.isExample ? (alts[0] || '') : student, onChange: (event) => handleBlankChange(itemIndex, part.index, event.target.value), placeholder: item.isExample ? '' : '...', className: fieldClass }, partIndex));
|
|
101
|
+
}) }) }), isReviewMode && !item.isExample && (_jsx("div", { className: "rounded-md border border-green-100 bg-green-50 px-3 py-2", children: _jsx(FillInBlankAnswerKey, { answersByBlank: item.answers, blankIndices: placeholderIndices }) }))] }, `${item.questionNumber}-${itemIndex}`));
|
|
98
102
|
}) }), 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] }))] }));
|
|
99
103
|
}
|
|
@@ -4,18 +4,18 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { Button } from '../../../../components/ui/button';
|
|
5
5
|
import { Label } from '../../../../components/ui/label';
|
|
6
6
|
import { Input } from '../../../../components/ui/input';
|
|
7
|
-
import { Textarea } from '../../../../components/ui/textarea';
|
|
8
7
|
import { PointsInput } from '../../../../components/ui/points-input';
|
|
9
8
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
10
9
|
import { Switch } from '../../../../components/ui/switch';
|
|
11
10
|
import { RichTextEditor } from '../../../../components/shared/RichTextEditor';
|
|
12
11
|
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
13
|
-
import {
|
|
12
|
+
import { isRichTextEmpty } from '../../../../shared/lib/rich-text';
|
|
13
|
+
import { BookOpen, HelpCircle, Plus, Star, Trash2 } from 'lucide-react';
|
|
14
14
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
15
15
|
import { FillInBlankGroupClient } from './FillInBlankGroupClient';
|
|
16
16
|
import { FillInBlankGroupMediaField } from './FillInBlankGroupMediaField';
|
|
17
17
|
import { createEmptyFillInBlankGroupItem } from './map-fill-in-blank-group-data';
|
|
18
|
-
import { blankSlotCount, extractPlaceholderIndices, fromFillInBlankImageUrls, hasAnyBlankOptions,
|
|
18
|
+
import { blankSlotCount, extractPlaceholderIndices, fromFillInBlankImageUrls, hasAnyBlankOptions, normalizeAnswersMatrix, normalizeWordBank, parseSlashAnswers, toFillInBlankImageUrls, } from './blank-utils';
|
|
19
19
|
import { FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION } from '../../_shared/types/fill-in-blank-group.type';
|
|
20
20
|
function parsePointsValue(raw, allowZero = false) {
|
|
21
21
|
if (raw === '')
|
|
@@ -26,16 +26,69 @@ function parsePointsValue(raw, allowZero = false) {
|
|
|
26
26
|
const min = allowZero ? 0 : 1;
|
|
27
27
|
return Math.max(min, parsed);
|
|
28
28
|
}
|
|
29
|
+
function padEditorWordBank(raw, requiredLength) {
|
|
30
|
+
if (requiredLength <= 0 || !Array.isArray(raw))
|
|
31
|
+
return undefined;
|
|
32
|
+
const next = raw.slice(0, requiredLength);
|
|
33
|
+
while (next.length < requiredLength)
|
|
34
|
+
next.push(null);
|
|
35
|
+
return next.some((entry) => entry && entry.options.length > 0) ? next : undefined;
|
|
36
|
+
}
|
|
37
|
+
function editorOptionSlots(options) {
|
|
38
|
+
if (!options || options.length === 0)
|
|
39
|
+
return ['', ''];
|
|
40
|
+
if (options.length === 1)
|
|
41
|
+
return [options[0], ''];
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
function editorAnswerSlots(alts) {
|
|
45
|
+
if (!alts || alts.length === 0)
|
|
46
|
+
return [''];
|
|
47
|
+
return alts.map((alt) => (alt == null ? '' : String(alt)));
|
|
48
|
+
}
|
|
49
|
+
function writeBlankAnswers(item, blankIndex, alts) {
|
|
50
|
+
const requiredLength = blankSlotCount(item.question);
|
|
51
|
+
const next = [...item.answers];
|
|
52
|
+
while (next.length < requiredLength)
|
|
53
|
+
next.push(['']);
|
|
54
|
+
next[blankIndex] = alts.length > 0 ? alts : [''];
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
function blankHasOptions(options) {
|
|
58
|
+
return Boolean(options?.some((option) => option.trim()));
|
|
59
|
+
}
|
|
60
|
+
function optionEditorKey(itemIndex, blankIndex) {
|
|
61
|
+
return `${itemIndex}-${blankIndex}`;
|
|
62
|
+
}
|
|
63
|
+
function writeBlankOptions(item, blankIndex, options) {
|
|
64
|
+
const requiredLength = blankSlotCount(item.question);
|
|
65
|
+
const next = [...(item.wordBank || [])];
|
|
66
|
+
while (next.length < requiredLength)
|
|
67
|
+
next.push(null);
|
|
68
|
+
next[blankIndex] = { options };
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
function clearBlankOptions(item, blankIndex) {
|
|
72
|
+
const requiredLength = blankSlotCount(item.question);
|
|
73
|
+
const next = [...(item.wordBank || [])];
|
|
74
|
+
while (next.length < requiredLength)
|
|
75
|
+
next.push(null);
|
|
76
|
+
next[blankIndex] = null;
|
|
77
|
+
return next.some((entry) => entry && entry.options.some((option) => option.trim()))
|
|
78
|
+
? next
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
29
81
|
function syncItemAnswers(item) {
|
|
30
82
|
const indices = extractPlaceholderIndices(item.question || '');
|
|
31
83
|
const requiredLength = blankSlotCount(item.question || '');
|
|
32
|
-
const wordBank =
|
|
84
|
+
const wordBank = padEditorWordBank(item.wordBank, requiredLength);
|
|
85
|
+
const rawAnswers = Array.isArray(item.answers) ? item.answers : [];
|
|
33
86
|
return {
|
|
34
87
|
...item,
|
|
35
|
-
answers:
|
|
88
|
+
answers: Array.from({ length: requiredLength }, (_, index) => {
|
|
36
89
|
if (!indices.includes(index))
|
|
37
90
|
return [];
|
|
38
|
-
return
|
|
91
|
+
return editorAnswerSlots(rawAnswers[index]);
|
|
39
92
|
}),
|
|
40
93
|
...(wordBank ? { wordBank } : { wordBank: undefined }),
|
|
41
94
|
};
|
|
@@ -66,6 +119,8 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
66
119
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
67
120
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
68
121
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
122
|
+
const [showPassage, setShowPassage] = useState(!isRichTextEmpty(initialData?.passage));
|
|
123
|
+
const [openOptionKeys, setOpenOptionKeys] = useState(() => new Set());
|
|
69
124
|
const [errors, setErrors] = useState([]);
|
|
70
125
|
const onChangeRef = useRef(onChange);
|
|
71
126
|
const previousPayloadRef = useRef('');
|
|
@@ -84,7 +139,11 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
84
139
|
showHints: nextShowHints,
|
|
85
140
|
hints: parseHints(nextHintsText),
|
|
86
141
|
...(viewMode ? { viewMode } : {}),
|
|
87
|
-
items: nextItems
|
|
142
|
+
items: nextItems.map((item) => ({
|
|
143
|
+
...item,
|
|
144
|
+
answers: normalizeAnswersMatrix(item.answers, blankSlotCount(item.question)),
|
|
145
|
+
wordBank: normalizeWordBank(item.wordBank, blankSlotCount(item.question)),
|
|
146
|
+
})),
|
|
88
147
|
explanation: nextExplanation,
|
|
89
148
|
points: sumPoints(nextItems),
|
|
90
149
|
};
|
|
@@ -122,7 +181,9 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
122
181
|
if (!alts.some((alt) => alt.trim())) {
|
|
123
182
|
nextErrors.push(`Câu ${index + 1}: Blank {${blankIndex}} cần ít nhất một đáp án.`);
|
|
124
183
|
}
|
|
125
|
-
const options = item.wordBank?.[blankIndex]?.options || []
|
|
184
|
+
const options = (item.wordBank?.[blankIndex]?.options || [])
|
|
185
|
+
.map((option) => option.trim())
|
|
186
|
+
.filter(Boolean);
|
|
126
187
|
if (options.length >= 2) {
|
|
127
188
|
const missing = alts
|
|
128
189
|
.map((alt) => alt.trim())
|
|
@@ -172,23 +233,45 @@ export function FillInBlankGroupCreator({ initialData, onChange, externalErrors,
|
|
|
172
233
|
if (isClientMode) {
|
|
173
234
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(FillInBlankGroupClient, { questionData: currentPayload })] }));
|
|
174
235
|
}
|
|
175
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "\u0110i\u1EC1n ch\u1ED7 tr\u1ED1ng (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc (
|
|
236
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsxs(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "\u0110i\u1EC1n ch\u1ED7 tr\u1ED1ng (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: FILL_IN_BLANK_GROUP_DEFAULT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] }), showPassage ? (_jsxs("div", { className: "relative space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { className: "text-sm font-semibold text-gray-800", children: "N\u1ED9i dung b\u00E0i \u0111\u1ECDc" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setShowPassage(false), className: "h-7 px-2 text-xs text-gray-500 hover:text-gray-700", children: "\u1EA8n" })] }), _jsx(RichTextEditor, { value: passage, onChange: setPassage, minHeightClassName: "min-h-[160px]" })] })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setShowPassage(true), className: "h-8 gap-1.5 border-indigo-200 px-2.5 text-xs text-indigo-700 hover:bg-indigo-50 hover:text-indigo-800", children: [_jsx(BookOpen, { className: "h-3.5 w-3.5" }), isRichTextEmpty(passage) ? 'Thêm bài đọc' : 'Hiện bài đọc'] })), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsx(FillInBlankGroupMediaField, { id: "fib-group-audio", label: "Audio chung (optional)", accept: "audio/*", value: audioUrl, onChange: setAudioUrl }), _jsx(FillInBlankGroupMediaField, { id: "fib-group-image", label: "\u1EA2nh chung (optional)", accept: "image/*", value: toFillInBlankImageUrls(imageUrl)[0] || '', onChange: setImageUrl })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "show-hints", checked: showHints, onCheckedChange: setShowHints }), _jsx(Label, { htmlFor: "show-hints", children: "Hi\u1EC7n h\u1ED9p t\u1EEB g\u1EE3i \u00FD chung" })] }), showHints && (_jsx(Input, { value: hintsText, onChange: (e) => setHintsText(e.target.value), placeholder: "am / is / are", className: "h-10 border-gray-200 bg-white" }))] }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["Danh s\u00E1ch c\u00E2u (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u00E2u"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-gray-700", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700", children: item.questionNumber || index + 1 }), "C\u00E2u ", index + 1] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, { isExample: checked, points: checked ? 0 : 1 }) }), _jsxs(Label, { htmlFor: `example-${index}`, className: "flex cursor-pointer items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] })] }), !item.isExample && (_jsx("div", { className: "w-24", children: _jsx(PointsInput, { value: item.points, allowZero: true, onChange: (event) => updateItem(index, {
|
|
176
237
|
points: parsePointsValue(event.target.value, true),
|
|
177
|
-
}) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeItem(index), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: `question-${index}`, children: ["
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
238
|
+
}) }) })), items.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeItem(index), className: "h-7 w-7 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] })] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: `question-${index}`, children: ["N\u1ED9i dung c\u00E2u (d\u00F9ng ", '{0}', ", ", '{1}', "...)"] }), _jsx(Input, { id: `question-${index}`, value: item.question, onChange: (e) => updateItem(index, { question: e.target.value }), placeholder: "{0}", className: "mt-1 h-10 border-gray-200 bg-white" })] }), _jsx("div", { className: "grid grid-cols-2 gap-2.5", children: extractPlaceholderIndices(item.question).map((blankIndex) => {
|
|
239
|
+
const optionKey = optionEditorKey(index, blankIndex);
|
|
240
|
+
const showOptions = openOptionKeys.has(optionKey);
|
|
241
|
+
const slots = editorOptionSlots(item.wordBank?.[blankIndex]?.options);
|
|
242
|
+
const answerSlots = editorAnswerSlots(item.answers[blankIndex]);
|
|
243
|
+
return (_jsxs("div", { className: "min-w-0 space-y-2 rounded-md border border-gray-100 bg-slate-50/70 px-2.5 py-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { children: ["\u0110\u00E1p \u00E1n ", '{' + blankIndex + '}'] }), answerSlots.map((alt, altIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: `answer-${index}-${blankIndex}-${altIndex}`, value: alt, onChange: (e) => {
|
|
244
|
+
const next = [...answerSlots];
|
|
245
|
+
next[altIndex] = e.target.value;
|
|
246
|
+
updateItem(index, { answers: writeBlankAnswers(item, blankIndex, next) });
|
|
247
|
+
}, placeholder: `Đáp án ${altIndex + 1}`, className: "h-10 min-w-0 border-gray-200 bg-white" }), answerSlots.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => updateItem(index, {
|
|
248
|
+
answers: writeBlankAnswers(item, blankIndex, answerSlots.filter((_, slotIndex) => slotIndex !== altIndex)),
|
|
249
|
+
}), className: "h-7 w-7 shrink-0 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `${index}-${blankIndex}-${altIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => updateItem(index, {
|
|
250
|
+
answers: writeBlankAnswers(item, blankIndex, [...answerSlots, '']),
|
|
251
|
+
}), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm \u0111\u00E1p \u00E1n"] })] }), showOptions ? (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs(Label, { children: ["L\u1EF1a ch\u1ECDn ", '{' + blankIndex + '}'] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
252
|
+
setOpenOptionKeys((prev) => {
|
|
253
|
+
const next = new Set(prev);
|
|
254
|
+
next.delete(optionKey);
|
|
255
|
+
return next;
|
|
256
|
+
});
|
|
257
|
+
if (!blankHasOptions(item.wordBank?.[blankIndex]?.options)) {
|
|
258
|
+
updateItem(index, { wordBank: clearBlankOptions(item, blankIndex) });
|
|
259
|
+
}
|
|
260
|
+
}, className: "h-7 px-2 text-xs text-gray-500 hover:text-gray-700", children: "\u1EA8n" })] }), _jsx("p", { className: "text-xs text-slate-500", children: "M\u1ED7i l\u1EF1a ch\u1ECDn m\u1ED9t \u00F4. C\u1EA7n \u00EDt nh\u1EA5t 2 \u0111\u1EC3 hi\u1EC7n dropdown." }), slots.map((option, optionIndex) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: `options-${index}-${blankIndex}-${optionIndex}`, value: option, onChange: (e) => {
|
|
261
|
+
const next = [...slots];
|
|
262
|
+
next[optionIndex] = e.target.value;
|
|
263
|
+
updateItem(index, { wordBank: writeBlankOptions(item, blankIndex, next) });
|
|
264
|
+
}, placeholder: `Lựa chọn ${optionIndex + 1}`, className: "h-10 min-w-0 border-gray-200 bg-white" }), slots.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => updateItem(index, {
|
|
265
|
+
wordBank: writeBlankOptions(item, blankIndex, slots.filter((_, slotIndex) => slotIndex !== optionIndex)),
|
|
266
|
+
}), className: "h-7 w-7 shrink-0 text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }, `${index}-${blankIndex}-${optionIndex}`))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => updateItem(index, {
|
|
267
|
+
wordBank: writeBlankOptions(item, blankIndex, [...slots, '']),
|
|
268
|
+
}), className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm l\u1EF1a ch\u1ECDn"] })] })) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
|
|
269
|
+
setOpenOptionKeys((prev) => new Set(prev).add(optionKey));
|
|
270
|
+
if (!item.wordBank?.[blankIndex]?.options?.length) {
|
|
271
|
+
updateItem(index, { wordBank: writeBlankOptions(item, blankIndex, ['', '']) });
|
|
272
|
+
}
|
|
273
|
+
}, className: "h-8 gap-1.5 border-indigo-200 px-2.5 text-xs text-indigo-700 hover:bg-indigo-50 hover:text-indigo-800", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), blankHasOptions(item.wordBank?.[blankIndex]?.options)
|
|
274
|
+
? 'Hiện lựa chọn'
|
|
275
|
+
: 'Thêm lựa chọn'] }))] }, blankIndex));
|
|
276
|
+
}) }), _jsxs("div", { className: "grid gap-2.5 md:grid-cols-2", children: [_jsx(FillInBlankGroupMediaField, { id: `fib-item-image-${index}`, label: "\u1EA2nh item (optional)", accept: "image/*", value: Array.isArray(item.imageUrl) ? (item.imageUrl[0] || '') : (item.imageUrl || ''), onChange: (url) => updateItem(index, { imageUrl: url }) }), !audioUrl && (_jsx(FillInBlankGroupMediaField, { id: `fib-item-audio-${index}`, label: "Audio item (khi kh\u00F4ng c\u00F3 audio nh\u00F3m)", accept: "audio/*", value: item.audioUrl || '', onChange: (url) => updateItem(index, { audioUrl: url }) }))] })] }, index))), _jsx(CompactExplanationToggle, { value: explanation, onValueChange: setExplanation, defaultVisible: Boolean(initialData?.explanation) })] })] })] }));
|
|
194
277
|
}
|
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { Button } from '../../../../components/ui/button';
|
|
5
4
|
import { FileUpload } from '../../../../components/ui/file-upload';
|
|
6
5
|
import { ImagePreview } from '../../../../components/ui/image-preview';
|
|
7
6
|
import { usePresignedFileUrl } from '../../../../shared/lib/hooks';
|
|
8
|
-
|
|
7
|
+
function isDisplayableUrl(value) {
|
|
8
|
+
return (value.startsWith('http://') ||
|
|
9
|
+
value.startsWith('https://') ||
|
|
10
|
+
value.startsWith('blob:') ||
|
|
11
|
+
value.startsWith('data:'));
|
|
12
|
+
}
|
|
13
|
+
function toStorageKey(value) {
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return '';
|
|
17
|
+
if (isDisplayableUrl(trimmed))
|
|
18
|
+
return trimmed;
|
|
19
|
+
if (trimmed.startsWith('/images/') || trimmed.startsWith('/_next/')) {
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
return trimmed.replace(/^\/+/, '');
|
|
23
|
+
}
|
|
9
24
|
function resolveDisplayUrl(fileUrl, previewOverride, fetchedPreviewUrl) {
|
|
10
25
|
if (previewOverride)
|
|
11
26
|
return previewOverride;
|
|
12
27
|
if (fetchedPreviewUrl)
|
|
13
28
|
return fetchedPreviewUrl;
|
|
14
|
-
if (
|
|
29
|
+
if (isDisplayableUrl(fileUrl))
|
|
15
30
|
return fileUrl;
|
|
16
31
|
return '';
|
|
17
32
|
}
|
|
18
33
|
export function FillInBlankGroupMediaField({ id, label, accept, value, onChange, }) {
|
|
19
34
|
const [previewOverride, setPreviewOverride] = useState('');
|
|
20
|
-
const
|
|
35
|
+
const storageKey = toStorageKey(value);
|
|
36
|
+
const { previewUrl: fetchedPreviewUrl, isLoading } = usePresignedFileUrl(storageKey);
|
|
21
37
|
const displayUrl = resolveDisplayUrl(value, previewOverride, fetchedPreviewUrl);
|
|
22
38
|
const isImage = accept === 'image/*';
|
|
23
|
-
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: accept === 'image/*' ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (
|
|
24
|
-
setPreviewOverride('');
|
|
25
|
-
onChange('');
|
|
26
|
-
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })), value && !isImage && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("audio", { controls: true, src: displayUrl || value, className: "h-8 max-w-full" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
27
|
-
setPreviewOverride('');
|
|
28
|
-
onChange('');
|
|
29
|
-
}, className: "text-red-500 hover:bg-red-50", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }))] }));
|
|
39
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FileUpload, { id: id, label: label, accept: accept, value: value, onChange: onChange, onPresignedUrlChange: setPreviewOverride, maxSize: accept === 'image/*' ? 5 : 20, placeholder: isImage ? 'Upload ảnh hoặc paste URL' : 'Upload audio hoặc paste URL', autoUpload: true, prefix: "questions" }), value && isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-20 w-20 animate-pulse rounded-md bg-slate-100" })) : (_jsx(ImagePreview, { src: displayUrl, alt: label, className: "h-20 w-20 rounded-md object-contain" }))), value && !isImage && (isLoading && !displayUrl ? (_jsx("div", { className: "h-8 w-48 animate-pulse rounded-md bg-slate-100" })) : (_jsx("audio", { controls: true, src: displayUrl, className: "h-8 max-w-full" })))] }));
|
|
30
40
|
}
|
|
@@ -13,6 +13,7 @@ export declare function extractPlaceholderIndices(question: string): number[];
|
|
|
13
13
|
export declare function blankSlotCount(question: string): number;
|
|
14
14
|
export declare function normalizeBlankAlts(value: unknown): string[];
|
|
15
15
|
export declare function normalizeAnswersMatrix(raw: unknown, requiredLength?: number): string[][];
|
|
16
|
+
export declare function acceptedAnswerList(alts: string[] | undefined): string[];
|
|
16
17
|
export declare function joinSlashAnswers(alts: string[] | undefined): string;
|
|
17
18
|
export declare function parseSlashAnswers(text: string): string[];
|
|
18
19
|
export declare function splitStem(question: string): FillBlankStemPart[];
|