@thanh01.pmt/interactive-quiz-kit 1.0.63 → 1.0.65
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/ai.cjs +2 -1
- package/dist/ai.d.cts +45 -0
- package/dist/ai.d.ts +45 -0
- package/dist/ai.mjs +2 -1
- package/dist/authoring.cjs +88 -224
- package/dist/authoring.d.cts +1 -1
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.mjs +88 -224
- package/dist/react-ui.cjs +1706 -1844
- package/dist/react-ui.d.cts +1 -1
- package/dist/react-ui.d.ts +1 -1
- package/dist/react-ui.mjs +1706 -1844
- package/dist/{toaster-FX787Evl.d.cts → toaster-CKS4zoRY.d.cts} +7 -2
- package/dist/{toaster-BTfAUpDp.d.ts → toaster-DLJ_2W9u.d.ts} +7 -2
- package/package.json +1 -1
package/dist/react-ui.mjs
CHANGED
|
@@ -99468,7 +99468,7 @@ var EditQuestionModal = ({
|
|
|
99468
99468
|
if (!isOpen || !editedQuestion) return null;
|
|
99469
99469
|
return /* @__PURE__ */ React169__default.createElement(Dialog2, { open: isOpen, onOpenChange: (open) => {
|
|
99470
99470
|
if (!open) onClose();
|
|
99471
|
-
} }, /* @__PURE__ */ React169__default.createElement(DialogContent2, { className: "
|
|
99471
|
+
} }, /* @__PURE__ */ React169__default.createElement(DialogContent2, { className: "sm:max-w-[600px] md:max-w-[800px] lg:max-w-[1000px] max-h-[90vh]" }, /* @__PURE__ */ React169__default.createElement(DialogHeader, null, /* @__PURE__ */ React169__default.createElement(DialogTitle2, { className: "font-headline text-2xl" }, questionData?.id && !questionData.id.startsWith("new_") && !questionData.id.startsWith("temp_") ? "Edit Question" : "Add New Question"), /* @__PURE__ */ React169__default.createElement(DialogDescription2, null, "Configure the details for this question. Current type:", " ", /* @__PURE__ */ React169__default.createElement("span", { className: "font-semibold" }, editedQuestion.questionType))), /* @__PURE__ */ React169__default.createElement(ScrollArea2, { className: "max-h-[calc(80vh-150px)] p-1 pr-6" }, /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-6 p-4" }, /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "prompt", className: "font-semibold" }, "Question Prompt"), /* @__PURE__ */ React169__default.createElement(SimpleMarkdownEditor, { value: editedQuestion.prompt, onChange: (htmlContent) => handleSpecificFieldChange({ prompt: htmlContent }) })), renderSpecificForm(), /* @__PURE__ */ React169__default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t" }, /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "points" }, "Points"), /* @__PURE__ */ React169__default.createElement(Input, { id: "points", type: "number", value: editedQuestion.points || 0, onChange: (e3) => handleSpecificFieldChange({ points: parseInt(e3.target.value, 10) || 0 }), min: "0" })), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "difficulty" }, "Difficulty"), /* @__PURE__ */ React169__default.createElement(Select2, { value: editedQuestion.difficulty || "medium", onValueChange: (value) => handleSpecificFieldChange({ difficulty: value }) }, /* @__PURE__ */ React169__default.createElement(SelectTrigger2, null, /* @__PURE__ */ React169__default.createElement(SelectValue2, null)), /* @__PURE__ */ React169__default.createElement(SelectContent2, null, /* @__PURE__ */ React169__default.createElement(SelectItem2, { value: "easy" }, "Easy"), /* @__PURE__ */ React169__default.createElement(SelectItem2, { value: "medium" }, "Medium"), /* @__PURE__ */ React169__default.createElement(SelectItem2, { value: "hard" }, "Hard"))))), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "explanation" }, "Explanation (Optional)"), /* @__PURE__ */ React169__default.createElement(SimpleMarkdownEditor, { value: editedQuestion.explanation || "", onChange: (htmlContent) => handleSpecificFieldChange({ explanation: htmlContent }), minHeight: "100px" })), /* @__PURE__ */ React169__default.createElement("details", { className: "group", open: hasDropdownMetadata }, /* @__PURE__ */ React169__default.createElement("summary", { className: "cursor-pointer font-semibold text-primary hover:underline" }, "Advanced Metadata (Optional)"), renderMetadataFields()))), /* @__PURE__ */ React169__default.createElement(DialogFooter, { className: "pt-4 border-t" }, /* @__PURE__ */ React169__default.createElement(DialogClose2, { asChild: true }, /* @__PURE__ */ React169__default.createElement(Button, { type: "button", variant: "outline" }, "Cancel")), /* @__PURE__ */ React169__default.createElement(Button, { type: "button", onClick: handleSaveClick }, /* @__PURE__ */ React169__default.createElement(Save, { className: "mr-2 h-4 w-4" }), " Save Question"))));
|
|
99472
99472
|
};
|
|
99473
99473
|
|
|
99474
99474
|
// src/react-ui/components/authoring/AIQuestionGeneratorModal.tsx
|
|
@@ -99618,7 +99618,8 @@ var QuizContextSchema = z.object({
|
|
|
99618
99618
|
originalSubject: z.string().optional(),
|
|
99619
99619
|
originalCategory: z.string().optional(),
|
|
99620
99620
|
originalTopic: z.string().optional(),
|
|
99621
|
-
loDescription: z.string().optional().describe("The full description of the learning objective for deep context.")
|
|
99621
|
+
loDescription: z.string().optional().describe("The full description of the learning objective for deep context."),
|
|
99622
|
+
gradeBand: z.string().optional()
|
|
99622
99623
|
});
|
|
99623
99624
|
var BaseQuestionGenerationClientInputSchema = z.object({
|
|
99624
99625
|
language: z.string().optional().default("English"),
|
|
@@ -100063,1003 +100064,899 @@ async function generateMCQQuestion(clientInput, apiKey) {
|
|
|
100063
100064
|
return { error: errorMessage };
|
|
100064
100065
|
}
|
|
100065
100066
|
|
|
100066
|
-
// src/
|
|
100067
|
+
// src/react-ui/components/authoring/AIQuestionGeneratorModal.tsx
|
|
100068
|
+
var supportedQuestionTypesForAI = [
|
|
100069
|
+
{ value: "true_false", label: "True/False" },
|
|
100070
|
+
{ value: "multiple_choice", label: "Multiple Choice" },
|
|
100071
|
+
{ value: "multiple_response", label: "Multiple Response" },
|
|
100072
|
+
{ value: "short_answer", label: "Short Answer" },
|
|
100073
|
+
{ value: "numeric", label: "Numeric" },
|
|
100074
|
+
{ value: "fill_in_the_blanks", label: "Fill In The Blanks" },
|
|
100075
|
+
{ value: "sequence", label: "Sequence" },
|
|
100076
|
+
{ value: "matching", label: "Matching" }
|
|
100077
|
+
];
|
|
100078
|
+
var AIQuestionGeneratorModal = ({
|
|
100079
|
+
isOpen,
|
|
100080
|
+
onClose,
|
|
100081
|
+
onQuestionGenerated,
|
|
100082
|
+
language: language3,
|
|
100083
|
+
questionType: questionTypeProp,
|
|
100084
|
+
subjects = [],
|
|
100085
|
+
topics = [],
|
|
100086
|
+
gradeLevels = [],
|
|
100087
|
+
bloomLevels = []
|
|
100088
|
+
}) => {
|
|
100089
|
+
const [prompt, setPrompt] = useState("");
|
|
100090
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
100091
|
+
const [error, setError] = useState(null);
|
|
100092
|
+
const { toast: toast2 } = useToast();
|
|
100093
|
+
const [subjectCode, setSubjectCode] = useState("");
|
|
100094
|
+
const [topicCode, setTopicCode] = useState("");
|
|
100095
|
+
const [gradeBand, setGradeBand] = useState("");
|
|
100096
|
+
const [bloomLevelCode, setBloomLevelCode] = useState("");
|
|
100097
|
+
const [selectedQuestionType, setSelectedQuestionType] = useState("multiple_choice");
|
|
100098
|
+
const [isApiKeyManagerModalOpen, setIsApiKeyManagerModalOpen] = useState(false);
|
|
100099
|
+
const [geminiApiKeyExists, setGeminiApiKeyExists] = useState(false);
|
|
100100
|
+
const finalQuestionType = questionTypeProp || selectedQuestionType;
|
|
100101
|
+
useEffect(() => {
|
|
100102
|
+
if (isOpen) {
|
|
100103
|
+
setPrompt("");
|
|
100104
|
+
setError(null);
|
|
100105
|
+
setIsLoading(false);
|
|
100106
|
+
setSubjectCode("");
|
|
100107
|
+
setTopicCode("");
|
|
100108
|
+
setGradeBand("");
|
|
100109
|
+
setBloomLevelCode("");
|
|
100110
|
+
setSelectedQuestionType(questionTypeProp || "multiple_choice");
|
|
100111
|
+
setGeminiApiKeyExists(APIKeyService.hasAPIKey(GEMINI_API_KEY_SERVICE_NAME));
|
|
100112
|
+
}
|
|
100113
|
+
}, [isOpen, questionTypeProp]);
|
|
100114
|
+
const filteredTopics = useMemo(() => {
|
|
100115
|
+
if (!subjectCode) return [];
|
|
100116
|
+
return topics.filter((t4) => t4.subjectCode === subjectCode);
|
|
100117
|
+
}, [subjectCode, topics]);
|
|
100118
|
+
const handleApiKeyModalClose = () => {
|
|
100119
|
+
setIsApiKeyManagerModalOpen(false);
|
|
100120
|
+
setGeminiApiKeyExists(APIKeyService.hasAPIKey(GEMINI_API_KEY_SERVICE_NAME));
|
|
100121
|
+
};
|
|
100122
|
+
const handleSubmit = async () => {
|
|
100123
|
+
if (!prompt.trim()) {
|
|
100124
|
+
setError("Please provide a prompt for the question.");
|
|
100125
|
+
return;
|
|
100126
|
+
}
|
|
100127
|
+
const geminiKey = APIKeyService.getAPIKey(GEMINI_API_KEY_SERVICE_NAME);
|
|
100128
|
+
if (!geminiKey) {
|
|
100129
|
+
setError("Gemini API Key is not set. Please configure it.");
|
|
100130
|
+
setGeminiApiKeyExists(false);
|
|
100131
|
+
return;
|
|
100132
|
+
}
|
|
100133
|
+
setGeminiApiKeyExists(true);
|
|
100134
|
+
setError(null);
|
|
100135
|
+
setIsLoading(true);
|
|
100136
|
+
try {
|
|
100137
|
+
const quizContext = {
|
|
100138
|
+
plannedTopic: prompt,
|
|
100139
|
+
originalSubject: subjectCode,
|
|
100140
|
+
originalTopic: topicCode,
|
|
100141
|
+
gradeBand,
|
|
100142
|
+
plannedBloomLevel: bloomLevelCode
|
|
100143
|
+
};
|
|
100144
|
+
const baseClientInput = {
|
|
100145
|
+
language: language3,
|
|
100146
|
+
difficulty: "medium",
|
|
100147
|
+
quizContext
|
|
100148
|
+
};
|
|
100149
|
+
let generatedResult = {};
|
|
100150
|
+
switch (finalQuestionType) {
|
|
100151
|
+
case "true_false":
|
|
100152
|
+
generatedResult = await generateTrueFalseQuestion(baseClientInput, geminiKey);
|
|
100153
|
+
break;
|
|
100154
|
+
case "multiple_choice":
|
|
100155
|
+
generatedResult = await generateMCQQuestion({ ...baseClientInput, numberOfOptions: 4 }, geminiKey);
|
|
100156
|
+
break;
|
|
100157
|
+
// Add other cases as needed
|
|
100158
|
+
default:
|
|
100159
|
+
throw new Error(`AI generation for '${finalQuestionType}' is not implemented in this flow.`);
|
|
100160
|
+
}
|
|
100161
|
+
if (generatedResult.error) {
|
|
100162
|
+
throw new Error(generatedResult.error);
|
|
100163
|
+
}
|
|
100164
|
+
if (generatedResult.question) {
|
|
100165
|
+
const completeQuestion = generatedResult.question;
|
|
100166
|
+
completeQuestion.subject = subjectCode;
|
|
100167
|
+
completeQuestion.topic = topicCode;
|
|
100168
|
+
completeQuestion.gradeBand = gradeBand;
|
|
100169
|
+
completeQuestion.bloomLevel = bloomLevelCode;
|
|
100170
|
+
if (completeQuestion.points === void 0) completeQuestion.points = 10;
|
|
100171
|
+
onQuestionGenerated(completeQuestion);
|
|
100172
|
+
toast2({ title: "AI Question Generated", description: "Review and edit the generated question." });
|
|
100173
|
+
onClose();
|
|
100174
|
+
} else {
|
|
100175
|
+
throw new Error("AI did not return a valid question object.");
|
|
100176
|
+
}
|
|
100177
|
+
} catch (e3) {
|
|
100178
|
+
console.error("AI Question Generation Error:", e3);
|
|
100179
|
+
let errorMessage = e3.message || "An unknown error occurred.";
|
|
100180
|
+
if (typeof errorMessage === "string" && errorMessage.includes("API key not valid")) {
|
|
100181
|
+
errorMessage = "The provided Google Gemini API Key is invalid. Please check it.";
|
|
100182
|
+
setGeminiApiKeyExists(false);
|
|
100183
|
+
}
|
|
100184
|
+
setError(errorMessage);
|
|
100185
|
+
toast2({ title: "AI Generation Failed", description: errorMessage, variant: "destructive" });
|
|
100186
|
+
} finally {
|
|
100187
|
+
setIsLoading(false);
|
|
100188
|
+
}
|
|
100189
|
+
};
|
|
100190
|
+
const comboboxOptions = {
|
|
100191
|
+
subjects: subjects.map((s4) => ({ value: s4.code, label: s4.name })),
|
|
100192
|
+
topics: filteredTopics.map((t4) => ({ value: t4.code, label: t4.name })),
|
|
100193
|
+
gradeLevels: gradeLevels.map((g) => ({ value: g.code, label: g.name })),
|
|
100194
|
+
bloomLevels: bloomLevels.map((b2) => ({ value: b2.code, label: b2.name }))
|
|
100195
|
+
};
|
|
100196
|
+
return /* @__PURE__ */ React169__default.createElement(React169__default.Fragment, null, /* @__PURE__ */ React169__default.createElement(Dialog2, { open: isOpen, onOpenChange: (open) => {
|
|
100197
|
+
if (!open) onClose();
|
|
100198
|
+
} }, /* @__PURE__ */ React169__default.createElement(DialogContent2, { className: "sm:max-w-[600px]" }, /* @__PURE__ */ React169__default.createElement(DialogHeader, null, /* @__PURE__ */ React169__default.createElement(DialogTitle2, { className: "flex items-center font-headline text-2xl" }, /* @__PURE__ */ React169__default.createElement(WandSparkles, { className: "mr-2 h-6 w-6 text-primary" }), " AI Question Generator"), /* @__PURE__ */ React169__default.createElement(DialogDescription2, null, "Provide a prompt and metadata to generate a '", finalQuestionType, "' question.")), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-4 py-4 max-h-[60vh] overflow-y-auto px-2" }, !geminiApiKeyExists && /* @__PURE__ */ React169__default.createElement("div", { className: "p-3 mb-4 border border-amber-500 bg-amber-50 rounded-md text-amber-700" }), !questionTypeProp && /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-q-type-select" }, "Question Type"), /* @__PURE__ */ React169__default.createElement(Select2, { value: selectedQuestionType, onValueChange: (v) => setSelectedQuestionType(v) }, /* @__PURE__ */ React169__default.createElement(SelectTrigger2, { id: "ai-q-type-select" }, /* @__PURE__ */ React169__default.createElement(SelectValue2, null)), /* @__PURE__ */ React169__default.createElement(SelectContent2, null, supportedQuestionTypesForAI.map((type) => /* @__PURE__ */ React169__default.createElement(SelectItem2, { key: type.value, value: type.value }, type.label))))), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-prompt" }, "Prompt / Topic"), /* @__PURE__ */ React169__default.createElement(
|
|
100199
|
+
Textarea,
|
|
100200
|
+
{
|
|
100201
|
+
id: "ai-prompt",
|
|
100202
|
+
value: prompt,
|
|
100203
|
+
onChange: (e3) => setPrompt(e3.target.value),
|
|
100204
|
+
placeholder: "e.g., The process of photosynthesis, The causes of World War II, Basic Algebra Equations",
|
|
100205
|
+
className: "min-h-[100px]"
|
|
100206
|
+
}
|
|
100207
|
+
)), /* @__PURE__ */ React169__default.createElement("div", { className: "grid grid-cols-2 gap-4" }, /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, null, "Subject"), /* @__PURE__ */ React169__default.createElement(EditableCombobox, { options: comboboxOptions.subjects, value: subjectCode, onChange: setSubjectCode, placeholder: "Select or type a Subject..." })), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, null, "Topic"), /* @__PURE__ */ React169__default.createElement(EditableCombobox, { options: comboboxOptions.topics, value: topicCode, onChange: setTopicCode, placeholder: "Select or type a Topic...", disabled: !subjectCode })), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, null, "Grade Level"), /* @__PURE__ */ React169__default.createElement(EditableCombobox, { options: comboboxOptions.gradeLevels, value: gradeBand, onChange: setGradeBand, placeholder: "Select or type a Grade..." })), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, null, "Bloom's Level"), /* @__PURE__ */ React169__default.createElement(EditableCombobox, { options: comboboxOptions.bloomLevels, value: bloomLevelCode, onChange: setBloomLevelCode, placeholder: "Select a Bloom's Level..." }))), error && /* @__PURE__ */ React169__default.createElement("p", { className: "text-sm text-destructive flex items-center mt-2" }, /* @__PURE__ */ React169__default.createElement(TriangleAlert, { className: "mr-1 h-4 w-4" }), " ", error)), /* @__PURE__ */ React169__default.createElement(DialogFooter, null, /* @__PURE__ */ React169__default.createElement(DialogClose2, { asChild: true }, /* @__PURE__ */ React169__default.createElement(Button, { type: "button", variant: "outline" }, "Cancel")), /* @__PURE__ */ React169__default.createElement(Button, { type: "button", onClick: handleSubmit, disabled: isLoading || !prompt.trim() || !geminiApiKeyExists }, isLoading ? /* @__PURE__ */ React169__default.createElement(LoaderCircle, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ React169__default.createElement(WandSparkles, { className: "mr-2 h-4 w-4" }), "Generate Question")))), /* @__PURE__ */ React169__default.createElement(APIKeyManagerModal, { isOpen: isApiKeyManagerModalOpen, onClose: handleApiKeyModalClose }));
|
|
100208
|
+
};
|
|
100209
|
+
|
|
100210
|
+
// src/react-ui/components/authoring/AIFullQuizGeneratorModal.tsx
|
|
100067
100211
|
init_react_shim();
|
|
100068
100212
|
|
|
100069
|
-
// src/ai/flows/
|
|
100213
|
+
// src/ai/flows/generate-quiz-plan.ts
|
|
100070
100214
|
init_react_shim();
|
|
100071
|
-
|
|
100072
|
-
|
|
100073
|
-
|
|
100074
|
-
|
|
100215
|
+
|
|
100216
|
+
// src/ai/flows/generate-quiz-plan-types.ts
|
|
100217
|
+
init_react_shim();
|
|
100218
|
+
var TopicWithMetadataSchema = z.object({
|
|
100219
|
+
topic: z.string().min(1),
|
|
100220
|
+
ratio: z.number().min(0).max(100),
|
|
100221
|
+
originalLoId: z.string().optional(),
|
|
100222
|
+
originalSubject: z.string().optional(),
|
|
100223
|
+
originalCategory: z.string().optional(),
|
|
100224
|
+
originalTopic: z.string().optional(),
|
|
100225
|
+
commonMisconceptions: z.array(z.string()).optional()
|
|
100075
100226
|
});
|
|
100076
|
-
var
|
|
100077
|
-
|
|
100078
|
-
|
|
100079
|
-
|
|
100080
|
-
|
|
100081
|
-
|
|
100082
|
-
|
|
100083
|
-
|
|
100084
|
-
|
|
100227
|
+
var BloomLevelStringsEnum = z.enum(["remembering", "understanding", "applying", "analyzing", "evaluating", "creating"]);
|
|
100228
|
+
var fullQuizSupportedQuestionTypesArray = [
|
|
100229
|
+
"true_false",
|
|
100230
|
+
"multiple_choice",
|
|
100231
|
+
"multiple_response",
|
|
100232
|
+
"short_answer",
|
|
100233
|
+
"numeric",
|
|
100234
|
+
"fill_in_the_blanks",
|
|
100235
|
+
"sequence",
|
|
100236
|
+
"matching",
|
|
100237
|
+
"drag_and_drop",
|
|
100238
|
+
"coding"
|
|
100239
|
+
];
|
|
100240
|
+
z.object({
|
|
100241
|
+
language: z.string().optional().default("English"),
|
|
100242
|
+
totalQuestions: z.number().int().min(1).max(50),
|
|
100243
|
+
numCodingQuestions: z.number().optional().default(0),
|
|
100244
|
+
topics: z.array(TopicWithMetadataSchema).min(1),
|
|
100245
|
+
bloomLevels: z.array(z.object({
|
|
100246
|
+
level: BloomLevelStringsEnum,
|
|
100247
|
+
ratio: z.number().min(0).max(100)
|
|
100248
|
+
})).min(1),
|
|
100249
|
+
selectedContextIds: z.array(z.string()).optional(),
|
|
100250
|
+
selectedQuestionTypes: z.array(z.enum(fullQuizSupportedQuestionTypesArray)).min(1),
|
|
100251
|
+
imageContexts: z.array(z.custom()).optional().describe("Library of available image contexts for the AI to use.")
|
|
100252
|
+
});
|
|
100253
|
+
var PlannedQuestionSchema = z.object({
|
|
100254
|
+
plannedTopic: z.string().min(1).describe("The specific, assessable topic for this question."),
|
|
100255
|
+
plannedQuestionType: z.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
|
|
100256
|
+
plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned."),
|
|
100257
|
+
plannedContextId: z.string().optional().describe("The specific context ID chosen for this question."),
|
|
100258
|
+
imageId: z.string().nullable().optional().describe("The ID of the image from the context library to be used for this question."),
|
|
100259
|
+
targetMisconception: z.string().optional().describe("A specific common misconception this question should target."),
|
|
100260
|
+
difficultyReason: z.string().optional().describe("Strategic explanation of difficulty choice and placement."),
|
|
100261
|
+
topicSpecificity: z.enum(["broad", "focused", "specific"]).optional().describe("How specific the topic coverage should be."),
|
|
100262
|
+
originalLoId: z.string().optional(),
|
|
100263
|
+
originalSubject: z.string().optional(),
|
|
100264
|
+
originalCategory: z.string().optional(),
|
|
100265
|
+
originalTopic: z.string().optional()
|
|
100266
|
+
});
|
|
100267
|
+
var GenerateQuizPlanOutputSchema = z.object({
|
|
100268
|
+
quizPlan: z.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz."),
|
|
100269
|
+
diversityMetrics: z.object({
|
|
100270
|
+
questionTypeDistribution: z.record(z.number()).optional(),
|
|
100271
|
+
bloomLevelDistribution: z.record(z.number()).optional(),
|
|
100272
|
+
maxConsecutiveSameType: z.number().optional()
|
|
100273
|
+
}).optional().describe("Metrics showing the diversity achieved in the plan."),
|
|
100274
|
+
planningStrategy: z.object({
|
|
100275
|
+
overallApproach: z.string().optional(),
|
|
100276
|
+
keyDecisions: z.array(z.string()).optional()
|
|
100277
|
+
}).optional()
|
|
100085
100278
|
});
|
|
100086
100279
|
|
|
100087
|
-
// src/ai/flows/
|
|
100088
|
-
var
|
|
100089
|
-
|
|
100090
|
-
|
|
100091
|
-
|
|
100092
|
-
|
|
100093
|
-
|
|
100094
|
-
|
|
100095
|
-
|
|
100280
|
+
// src/ai/flows/generate-quiz-plan.ts
|
|
100281
|
+
var QuizPlanLogger = class {
|
|
100282
|
+
constructor() {
|
|
100283
|
+
this.logs = [];
|
|
100284
|
+
this.startTime = Date.now();
|
|
100285
|
+
}
|
|
100286
|
+
log(phase, data, duration) {
|
|
100287
|
+
this.logs.push({
|
|
100288
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
100289
|
+
phase,
|
|
100290
|
+
data,
|
|
100291
|
+
duration
|
|
100292
|
+
});
|
|
100293
|
+
if (duration !== void 0) {
|
|
100294
|
+
console.log(`[${phase}] Completed in ${duration}ms:`, data);
|
|
100295
|
+
} else {
|
|
100296
|
+
console.log(`[${phase}]:`, data);
|
|
100297
|
+
}
|
|
100298
|
+
}
|
|
100299
|
+
getLogs() {
|
|
100300
|
+
return this.logs;
|
|
100301
|
+
}
|
|
100302
|
+
getTotalDuration() {
|
|
100303
|
+
return Date.now() - this.startTime;
|
|
100304
|
+
}
|
|
100305
|
+
};
|
|
100306
|
+
function generateQuestionTypeSelectionGuidance() {
|
|
100307
|
+
return `
|
|
100308
|
+
QUESTION TYPE SELECTION BEST PRACTICES:
|
|
100096
100309
|
|
|
100097
|
-
|
|
100098
|
-
|
|
100099
|
-
|
|
100100
|
-
|
|
100101
|
-
|
|
100102
|
-
imageContextInstruction,
|
|
100103
|
-
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
100104
|
-
quizContext?.targetMisconception && `**Target Misconception:** Use this to create plausible incorrect answers (distractors). The misconception is: "${quizContext.targetMisconception}"`,
|
|
100105
|
-
quizContext?.difficultyReason && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
|
|
100106
|
-
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
100107
|
-
const exampleJson = JSON.stringify({
|
|
100108
|
-
prompt: "Which of the following are considered programming paradigms?",
|
|
100109
|
-
options: [
|
|
100110
|
-
{ "tempId": "A", "text": "Object-Oriented" },
|
|
100111
|
-
{ "tempId": "B", "text": "Assembly" },
|
|
100112
|
-
{ "tempId": "C", "text": "Functional" },
|
|
100113
|
-
{ "tempId": "D", "text": "Procedural" },
|
|
100114
|
-
{ "tempId": "E", "text": "Middleware" }
|
|
100115
|
-
],
|
|
100116
|
-
correctTempOptionIds: ["A", "C", "D"],
|
|
100117
|
-
explanation: "Object-Oriented, Functional, and Procedural are all major programming paradigms. Assembly is a low-level language, and Middleware is a type of software, not a paradigm.",
|
|
100118
|
-
points: 10,
|
|
100119
|
-
difficulty: "medium",
|
|
100120
|
-
topic: "Programming Paradigms",
|
|
100121
|
-
verifiedCategory: category
|
|
100122
|
-
}, null, 2);
|
|
100123
|
-
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in the programming language: ${category}.
|
|
100124
|
-
Your sole mission is to create a high-quality, technically accurate Multiple Response Question. You must adhere to the following rules at all times.
|
|
100310
|
+
**TRUE_FALSE (true_false)**
|
|
100311
|
+
- Best for: Binary concepts, fact verification, common misconceptions
|
|
100312
|
+
- Bloom levels: Primarily Remembering, Understanding
|
|
100313
|
+
- Use when: Testing definitive statements, clarifying misconceptions
|
|
100314
|
+
- Example: "Photosynthesis only occurs during daytime" (targets timing misconception)
|
|
100125
100315
|
|
|
100126
|
-
|
|
100127
|
-
|
|
100128
|
-
|
|
100129
|
-
|
|
100316
|
+
**MULTIPLE CHOICE (multiple_choice)**
|
|
100317
|
+
- Best for: Concept selection, process understanding, comparison
|
|
100318
|
+
- Bloom levels: All levels, especially Understanding and Applying
|
|
100319
|
+
- Use when: Testing conceptual understanding with clear alternatives
|
|
100320
|
+
- Example: "Which factor most affects enzyme activity?" (applying knowledge)
|
|
100130
100321
|
|
|
100131
|
-
|
|
100132
|
-
|
|
100322
|
+
**MULTIPLE RESPONSE (multiple_response)**
|
|
100323
|
+
- Best for: Identifying multiple correct factors, comprehensive understanding
|
|
100324
|
+
- Bloom levels: Understanding, Analyzing, Evaluating
|
|
100325
|
+
- Use when: Multiple correct answers exist, testing thorough knowledge
|
|
100326
|
+
- Example: "Select all factors that influence plant growth" (analyzing components)
|
|
100133
100327
|
|
|
100134
|
-
|
|
100135
|
-
|
|
100136
|
-
|
|
100137
|
-
|
|
100138
|
-
-
|
|
100139
|
-
- **Language for Text:** ${language3}
|
|
100140
|
-
- **Difficulty Level:** ${difficulty}
|
|
100141
|
-
- **Number of Options:** Generate exactly ${numberOfOptions} options.
|
|
100142
|
-
- **Number of Correct Answers:** The 'correctTempOptionIds' array MUST contain between ${minCorrectAnswers} and ${maxCorrectAnswers} valid IDs from the options you generate.
|
|
100143
|
-
|
|
100144
|
-
### Required JSON Output Format
|
|
100145
|
-
Your response must be ONLY the JSON object, matching this exact structure and field names.
|
|
100328
|
+
**FILL IN THE BLANKS (fill_in_the_blanks)**
|
|
100329
|
+
- Best for: Key terminology, formulas, specific facts
|
|
100330
|
+
- Bloom levels: Remembering, Understanding
|
|
100331
|
+
- Use when: Testing precise recall of important terms/concepts
|
|
100332
|
+
- Example: "The process of _____ converts light energy to chemical energy"
|
|
100146
100333
|
|
|
100147
|
-
|
|
100334
|
+
**NUMERIC (numeric)**
|
|
100335
|
+
- Best for: Calculations, quantitative problems, formula application
|
|
100336
|
+
- Bloom levels: Applying, Analyzing
|
|
100337
|
+
- Use when: Mathematical computations are required
|
|
100338
|
+
- Example: "Calculate the molarity of a 2L solution containing 0.5 moles of NaCl"
|
|
100148
100339
|
|
|
100149
|
-
|
|
100150
|
-
|
|
100151
|
-
|
|
100152
|
-
|
|
100153
|
-
|
|
100154
|
-
}
|
|
100155
|
-
if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
|
|
100156
|
-
return { error: `Invalid input: maxCorrectAnswers (${clientInput.maxCorrectAnswers}) must be less than the total numberOfOptions (${clientInput.numberOfOptions}).` };
|
|
100157
|
-
}
|
|
100158
|
-
const ai = new GoogleGenAI({ apiKey });
|
|
100159
|
-
const model = "gemini-2.5-flash";
|
|
100160
|
-
const config3 = {
|
|
100161
|
-
temperature: 0.8,
|
|
100162
|
-
responseMimeType: "application/json",
|
|
100163
|
-
thinkingConfig: {
|
|
100164
|
-
thinkingBudget: 4e3
|
|
100165
|
-
}
|
|
100166
|
-
};
|
|
100167
|
-
const attemptResults = [];
|
|
100168
|
-
let lastError = null;
|
|
100169
|
-
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS3; attempt++) {
|
|
100170
|
-
const startTime = Date.now();
|
|
100171
|
-
const promptText = buildEnhancedPrompt3(clientInput, attempt);
|
|
100172
|
-
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
100173
|
-
try {
|
|
100174
|
-
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
100175
|
-
const parts = [{ text: promptText }];
|
|
100176
|
-
if (clientInput.imageUrl) {
|
|
100177
|
-
const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
100178
|
-
const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
|
|
100179
|
-
parts.unshift(imagePart);
|
|
100180
|
-
}
|
|
100181
|
-
const contents = [{ role: "user", parts }];
|
|
100182
|
-
const aiResult = await ai.models.generateContent({
|
|
100183
|
-
model,
|
|
100184
|
-
config: config3,
|
|
100185
|
-
contents
|
|
100186
|
-
});
|
|
100187
|
-
const response = aiResult;
|
|
100188
|
-
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
100189
|
-
const duration = Date.now() - startTime;
|
|
100190
|
-
DebugLogger.logResponse(attempt, rawText);
|
|
100191
|
-
if (!rawText) {
|
|
100192
|
-
throw new Error("AI returned an empty response.");
|
|
100193
|
-
}
|
|
100194
|
-
const parsedJson = JSON.parse(rawText);
|
|
100195
|
-
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
100196
|
-
const aiGeneratedContent = AIMRQOutputFieldsSchema.parse(parsedJson);
|
|
100197
|
-
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
100198
|
-
if (aiGeneratedContent.options.length !== clientInput.numberOfOptions) {
|
|
100199
|
-
throw new Error(`AI generated ${aiGeneratedContent.options.length} options, but ${clientInput.numberOfOptions} were required.`);
|
|
100200
|
-
}
|
|
100201
|
-
const correctCount = aiGeneratedContent.correctTempOptionIds.length;
|
|
100202
|
-
if (correctCount < clientInput.minCorrectAnswers || correctCount > clientInput.maxCorrectAnswers) {
|
|
100203
|
-
throw new Error(`AI provided ${correctCount} correct answers, which is outside the required range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
|
|
100204
|
-
}
|
|
100205
|
-
if (clientInput.quizContext?.originalCategory) {
|
|
100206
|
-
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
100207
|
-
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
100208
|
-
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
100209
|
-
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
100210
|
-
}
|
|
100211
|
-
}
|
|
100212
|
-
const finalOptions = [];
|
|
100213
|
-
const tempIdToFinalIdMap = {};
|
|
100214
|
-
const allTempIds = /* @__PURE__ */ new Set();
|
|
100215
|
-
aiGeneratedContent.options.forEach((aiOption) => {
|
|
100216
|
-
const finalId = generateUniqueId("opt_mr_");
|
|
100217
|
-
finalOptions.push({ id: finalId, text: aiOption.text });
|
|
100218
|
-
tempIdToFinalIdMap[aiOption.tempId] = finalId;
|
|
100219
|
-
allTempIds.add(aiOption.tempId);
|
|
100220
|
-
});
|
|
100221
|
-
const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
|
|
100222
|
-
if (!allTempIds.has(tempId)) {
|
|
100223
|
-
throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') which does not exist in the generated options.`);
|
|
100224
|
-
}
|
|
100225
|
-
return tempIdToFinalIdMap[tempId];
|
|
100226
|
-
});
|
|
100227
|
-
const completeQuestion = {
|
|
100228
|
-
id: generateUniqueId("mrq_ai_"),
|
|
100229
|
-
questionType: "multiple_response",
|
|
100230
|
-
prompt: aiGeneratedContent.prompt,
|
|
100231
|
-
options: finalOptions,
|
|
100232
|
-
correctAnswerIds: finalCorrectAnswerIds,
|
|
100233
|
-
explanation: aiGeneratedContent.explanation,
|
|
100234
|
-
points: aiGeneratedContent.points,
|
|
100235
|
-
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
100236
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
100237
|
-
contextCode: clientInput.quizContext?.plannedContextId,
|
|
100238
|
-
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
100239
|
-
learningObjective: clientInput.quizContext?.originalLoId,
|
|
100240
|
-
subject: clientInput.quizContext?.originalSubject,
|
|
100241
|
-
category: clientInput.quizContext?.originalCategory,
|
|
100242
|
-
imageUrl: clientInput.imageUrl
|
|
100243
|
-
};
|
|
100244
|
-
const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
|
|
100245
|
-
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
100246
|
-
console.log(`
|
|
100247
|
-
\u2705 MRQ generation successful on attempt ${attempt} (${duration}ms)`);
|
|
100248
|
-
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
100249
|
-
return { question: validatedQuestion };
|
|
100250
|
-
} catch (error) {
|
|
100251
|
-
lastError = error;
|
|
100252
|
-
const duration = Date.now() - startTime;
|
|
100253
|
-
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
100254
|
-
const willRetry = attempt < MAX_RETRY_ATTEMPTS3;
|
|
100255
|
-
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
100256
|
-
if (willRetry) {
|
|
100257
|
-
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS3}ms...`);
|
|
100258
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS3));
|
|
100259
|
-
}
|
|
100260
|
-
}
|
|
100261
|
-
}
|
|
100262
|
-
DebugLogger.logAttemptSummary(attemptResults);
|
|
100263
|
-
const errorMessage = `Failed to generate MRQ question after ${MAX_RETRY_ATTEMPTS3} attempts. Last error: ${lastError?.message}`;
|
|
100264
|
-
console.error("\n\u274C Final Result: FAILED");
|
|
100265
|
-
console.error(errorMessage);
|
|
100266
|
-
return { error: errorMessage };
|
|
100267
|
-
}
|
|
100340
|
+
**MATCHING (matching)**
|
|
100341
|
+
- Best for: Connecting related concepts, terminology pairs
|
|
100342
|
+
- Bloom levels: Remembering, Understanding
|
|
100343
|
+
- Use when: Testing relationships between terms and definitions
|
|
100344
|
+
- Example: Match organelles with their functions
|
|
100268
100345
|
|
|
100269
|
-
|
|
100270
|
-
|
|
100346
|
+
**SEQUENCE (sequence)**
|
|
100347
|
+
- Best for: Process steps, chronological order, procedural knowledge
|
|
100348
|
+
- Bloom levels: Understanding, Applying, Analyzing
|
|
100349
|
+
- Use when: Order or sequence is critical to understanding
|
|
100350
|
+
- Example: "Arrange the steps of mitosis in correct order"
|
|
100271
100351
|
|
|
100272
|
-
|
|
100273
|
-
|
|
100274
|
-
|
|
100275
|
-
|
|
100276
|
-
|
|
100277
|
-
var AIShortAnswerOutputFieldsSchema = z.object({
|
|
100278
|
-
prompt: z.string().describe("The question text that prompts the user for a short answer."),
|
|
100279
|
-
acceptedAnswers: z.array(z.string().min(1)).min(1).describe("An array of one or more acceptable short answers. Include common variations if applicable."),
|
|
100280
|
-
// isCaseSensitive không cần thiết ở đây, chúng ta sẽ quản lý nó ở phía client
|
|
100281
|
-
explanation: z.string().optional(),
|
|
100282
|
-
points: z.number().optional().default(10),
|
|
100283
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
100284
|
-
topic: z.string().optional(),
|
|
100285
|
-
verifiedCategory: z.string().optional()
|
|
100286
|
-
// Thêm để xác thực
|
|
100287
|
-
});
|
|
100352
|
+
**DRAG AND DROP (drag_and_drop)**
|
|
100353
|
+
- Best for: Categorization, classification, spatial relationships
|
|
100354
|
+
- Bloom levels: Understanding, Applying, Analyzing
|
|
100355
|
+
- Use when: Grouping or organizing information is key
|
|
100356
|
+
- Example: "Classify these compounds as acids, bases, or neutral"
|
|
100288
100357
|
|
|
100289
|
-
|
|
100290
|
-
|
|
100291
|
-
|
|
100292
|
-
|
|
100293
|
-
|
|
100294
|
-
const category = quizContext?.originalCategory || "the specified technical category";
|
|
100295
|
-
const attemptInfo = attemptNumber > 1 ? `
|
|
100296
|
-
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
100297
|
-
Previous attempts failed. Ensure 'acceptedAnswers' is a non-empty array of strings and the JSON is valid.
|
|
100358
|
+
**SHORT ANSWER (short_answer)**
|
|
100359
|
+
- Best for: Explanations, definitions, problem-solving steps
|
|
100360
|
+
- Bloom levels: Understanding, Applying, Analyzing, Evaluating, Creating
|
|
100361
|
+
- Use when: Requiring explanatory responses, open-ended thinking
|
|
100362
|
+
- Example: "Explain why enzymes are specific to certain substrates"
|
|
100298
100363
|
|
|
100299
|
-
|
|
100300
|
-
|
|
100301
|
-
|
|
100302
|
-
|
|
100303
|
-
|
|
100304
|
-
|
|
100305
|
-
|
|
100306
|
-
|
|
100307
|
-
|
|
100308
|
-
|
|
100309
|
-
prompt: "In Swift, what keyword is used to declare a constant?",
|
|
100310
|
-
acceptedAnswers: ["let"],
|
|
100311
|
-
explanation: "The 'let' keyword is used to declare constants, which are values that cannot be changed after they are set. 'var' is used for variables.",
|
|
100312
|
-
points: 10,
|
|
100313
|
-
difficulty: "easy",
|
|
100314
|
-
topic: "Swift Constants",
|
|
100315
|
-
verifiedCategory: category
|
|
100316
|
-
}, null, 2);
|
|
100317
|
-
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
100318
|
-
Your mission is to create a high-quality, technically accurate Short Answer Question.
|
|
100364
|
+
**CODING (coding)**
|
|
100365
|
+
- Best for: Programming problems, algorithm implementation
|
|
100366
|
+
- Bloom levels: Applying, Analyzing, Evaluating, Creating
|
|
100367
|
+
- Use when: Technical implementation or code analysis is required
|
|
100368
|
+
- Example: "Write a function to calculate factorial recursively"
|
|
100369
|
+
`;
|
|
100370
|
+
}
|
|
100371
|
+
function generateAdvancedBloomGuidance() {
|
|
100372
|
+
return `
|
|
100373
|
+
ADVANCED BLOOM'S TAXONOMY & QUESTION TYPE OPTIMIZATION:
|
|
100319
100374
|
|
|
100320
|
-
|
|
100321
|
-
|
|
100322
|
-
|
|
100323
|
-
|
|
100375
|
+
**REMEMBERING (Knowledge Recall)**
|
|
100376
|
+
- Primary types: true_false, fill_in_the_blanks, matching
|
|
100377
|
+
- Secondary types: multiple_choice (simple recall)
|
|
100378
|
+
- Focus: Facts, terms, basic concepts, definitions
|
|
100379
|
+
- Misconception addressing: Use true_false to clarify common confusions
|
|
100324
100380
|
|
|
100325
|
-
|
|
100326
|
-
|
|
100381
|
+
**UNDERSTANDING (Comprehension)**
|
|
100382
|
+
- Primary types: multiple_choice, short_answer, matching
|
|
100383
|
+
- Secondary types: multiple_response, sequence
|
|
100384
|
+
- Focus: Explanations, interpretations, examples, classifications
|
|
100385
|
+
- Best for: "Explain why...", "What does this mean...", "Give an example..."
|
|
100327
100386
|
|
|
100328
|
-
|
|
100329
|
-
|
|
100387
|
+
**APPLYING (Using Knowledge)**
|
|
100388
|
+
- Primary types: numeric, short_answer, sequence, coding
|
|
100389
|
+
- Secondary types: multiple_choice, drag_and_drop
|
|
100390
|
+
- Focus: Problem-solving, implementing procedures, using methods
|
|
100391
|
+
- Best for: Calculations, step-by-step processes, practical applications
|
|
100330
100392
|
|
|
100331
|
-
|
|
100332
|
-
-
|
|
100333
|
-
-
|
|
100334
|
-
-
|
|
100393
|
+
**ANALYZING (Breaking Down Information)**
|
|
100394
|
+
- Primary types: multiple_response, short_answer, sequence, coding
|
|
100395
|
+
- Secondary types: drag_and_drop, multiple_choice
|
|
100396
|
+
- Focus: Identifying components, relationships, cause-effect
|
|
100397
|
+
- Best for: "What are the factors...", "How do these relate...", "Break down..."
|
|
100335
100398
|
|
|
100336
|
-
|
|
100337
|
-
|
|
100399
|
+
**EVALUATING (Making Judgments)**
|
|
100400
|
+
- Primary types: short_answer, multiple_response, coding
|
|
100401
|
+
- Secondary types: multiple_choice (with justification)
|
|
100402
|
+
- Focus: Critiquing, judging, comparing alternatives, decision-making
|
|
100403
|
+
- Best for: "Which is better and why...", "Evaluate the approach...", "Critique..."
|
|
100338
100404
|
|
|
100339
|
-
|
|
100405
|
+
**CREATING (Producing New Work)**
|
|
100406
|
+
- Primary types: short_answer, coding, sequence
|
|
100407
|
+
- Secondary types: drag_and_drop (design tasks)
|
|
100408
|
+
- Focus: Designing, planning, producing, constructing
|
|
100409
|
+
- Best for: "Design a solution...", "Create a plan...", "Develop a strategy..."
|
|
100410
|
+
`;
|
|
100411
|
+
}
|
|
100412
|
+
function generateDiversityRules() {
|
|
100413
|
+
return `
|
|
100414
|
+
ENHANCED DIVERSITY & QUALITY ASSURANCE RULES:
|
|
100340
100415
|
|
|
100341
|
-
|
|
100416
|
+
**Question Type Distribution Strategy:**
|
|
100417
|
+
1. Never place more than 3 consecutive questions of the same type
|
|
100418
|
+
2. Distribute question types based on their cognitive complexity
|
|
100419
|
+
3. For quizzes with 10+ questions, use at least 4 different question types
|
|
100420
|
+
4. For quizzes with 20+ questions, use at least 6 different question types
|
|
100421
|
+
5. Balance quick-answer types (true_false, multiple_choice) with deeper types (short_answer, coding)
|
|
100422
|
+
|
|
100423
|
+
**Intelligent Difficulty Progression:**
|
|
100424
|
+
1. Opening (20%): Start with confidence-building questions (Remembering/Understanding)
|
|
100425
|
+
2. Building (40%): Gradually increase complexity (Understanding/Applying)
|
|
100426
|
+
3. Peak (30%): Most challenging questions (Analyzing/Evaluating/Creating)
|
|
100427
|
+
4. Closing (10%): Moderate challenge to end positively
|
|
100428
|
+
|
|
100429
|
+
**Misconception-Driven Planning:**
|
|
100430
|
+
- When common misconceptions are provided, prioritize question types that can effectively address them
|
|
100431
|
+
- Use true_false for binary misconceptions
|
|
100432
|
+
- Use multiple_choice for concept selection misconceptions
|
|
100433
|
+
- Use short_answer for complex misconceptions requiring explanation
|
|
100434
|
+
|
|
100435
|
+
**Contextual Intelligence:**
|
|
100436
|
+
- Programming/Technical topics: Favor coding, numeric, short_answer
|
|
100437
|
+
- Process-oriented topics: Favor sequence, short_answer, drag_and_drop
|
|
100438
|
+
- Conceptual topics: Favor multiple_choice, multiple_response, true_false
|
|
100439
|
+
- Factual topics: Favor fill_in_the_blanks, matching, true_false
|
|
100440
|
+
`;
|
|
100342
100441
|
}
|
|
100343
|
-
async function
|
|
100344
|
-
const
|
|
100345
|
-
|
|
100346
|
-
|
|
100347
|
-
|
|
100348
|
-
|
|
100349
|
-
|
|
100350
|
-
|
|
100442
|
+
async function generateQuizPlan(clientInput, apiKey, imageContexts = []) {
|
|
100443
|
+
const logger = new QuizPlanLogger();
|
|
100444
|
+
try {
|
|
100445
|
+
logger.log("VALIDATION_START", {
|
|
100446
|
+
totalQuestions: clientInput.totalQuestions,
|
|
100447
|
+
availableTypes: clientInput.selectedQuestionTypes,
|
|
100448
|
+
topicCount: clientInput.topics.length,
|
|
100449
|
+
bloomLevelCount: clientInput.bloomLevels.length
|
|
100450
|
+
});
|
|
100451
|
+
const totalTopicRatio = clientInput.topics.reduce((sum, t4) => sum + t4.ratio, 0);
|
|
100452
|
+
if (Math.abs(totalTopicRatio - 100) > 1) {
|
|
100453
|
+
throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
|
|
100351
100454
|
}
|
|
100352
|
-
|
|
100353
|
-
|
|
100354
|
-
|
|
100355
|
-
|
|
100356
|
-
|
|
100357
|
-
|
|
100358
|
-
|
|
100359
|
-
|
|
100360
|
-
|
|
100361
|
-
|
|
100362
|
-
|
|
100363
|
-
|
|
100364
|
-
|
|
100365
|
-
|
|
100366
|
-
|
|
100367
|
-
|
|
100368
|
-
|
|
100369
|
-
|
|
100370
|
-
|
|
100371
|
-
|
|
100372
|
-
|
|
100373
|
-
|
|
100374
|
-
|
|
100375
|
-
|
|
100376
|
-
|
|
100377
|
-
|
|
100378
|
-
|
|
100379
|
-
|
|
100380
|
-
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
100381
|
-
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
100382
|
-
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
100383
|
-
}
|
|
100384
|
-
}
|
|
100385
|
-
const completeQuestion = {
|
|
100386
|
-
id: generateUniqueId("saq_ai_"),
|
|
100387
|
-
questionType: "short_answer",
|
|
100388
|
-
prompt: aiGeneratedContent.prompt,
|
|
100389
|
-
acceptedAnswers: aiGeneratedContent.acceptedAnswers,
|
|
100390
|
-
isCaseSensitive: clientInput.isCaseSensitive,
|
|
100391
|
-
explanation: aiGeneratedContent.explanation,
|
|
100392
|
-
points: aiGeneratedContent.points,
|
|
100393
|
-
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
100394
|
-
difficulty: clientInput.difficulty,
|
|
100395
|
-
contextCode: clientInput.quizContext?.plannedContextId,
|
|
100396
|
-
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
100397
|
-
learningObjective: clientInput.quizContext?.originalLoId,
|
|
100398
|
-
subject: clientInput.quizContext?.originalSubject,
|
|
100399
|
-
category: clientInput.quizContext?.originalCategory,
|
|
100400
|
-
imageUrl: clientInput.imageUrl
|
|
100401
|
-
};
|
|
100402
|
-
const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
|
|
100403
|
-
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
100404
|
-
console.log(`
|
|
100405
|
-
\u2705 Short Answer generation successful on attempt ${attempt} (${duration}ms)`);
|
|
100406
|
-
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
100407
|
-
return { question: validatedQuestion };
|
|
100408
|
-
} catch (error) {
|
|
100409
|
-
lastError = error;
|
|
100410
|
-
const duration = Date.now() - startTime;
|
|
100411
|
-
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
100412
|
-
const willRetry = attempt < MAX_RETRY_ATTEMPTS4;
|
|
100413
|
-
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
100414
|
-
if (willRetry) {
|
|
100415
|
-
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS4}ms...`);
|
|
100416
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS4));
|
|
100455
|
+
const totalBloomRatio = clientInput.bloomLevels.reduce((sum, b2) => sum + b2.ratio, 0);
|
|
100456
|
+
if (Math.abs(totalBloomRatio - 100) > 1) {
|
|
100457
|
+
throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
|
|
100458
|
+
}
|
|
100459
|
+
logger.log("VALIDATION_SUCCESS", {
|
|
100460
|
+
topicRatioSum: totalTopicRatio,
|
|
100461
|
+
bloomRatioSum: totalBloomRatio
|
|
100462
|
+
});
|
|
100463
|
+
const aiStartTime = Date.now();
|
|
100464
|
+
const ai = new GoogleGenAI({
|
|
100465
|
+
apiKey
|
|
100466
|
+
});
|
|
100467
|
+
const model = "gemini-2.5-pro";
|
|
100468
|
+
const config3 = {
|
|
100469
|
+
temperature: 0.8,
|
|
100470
|
+
thinkingConfig: {
|
|
100471
|
+
thinkingBudget: 4096
|
|
100472
|
+
},
|
|
100473
|
+
responseMimeType: "application/json"
|
|
100474
|
+
};
|
|
100475
|
+
logger.log("AI_INITIALIZATION", { model }, Date.now() - aiStartTime);
|
|
100476
|
+
const { language: language3, totalQuestions, numCodingQuestions = 0 } = clientInput;
|
|
100477
|
+
const promptStartTime = Date.now();
|
|
100478
|
+
const topicsDistribution = clientInput.topics.map((t4) => {
|
|
100479
|
+
let topicString = `- Topic Context: "${t4.topic}", LoId: "${t4.originalLoId || "nil"}", Subject: "${t4.originalSubject || "nil"}", Category: "${t4.originalCategory || "nil"}", Topic: "${t4.originalTopic || "nil"}", Ratio: ${t4.ratio}%`;
|
|
100480
|
+
if (t4.commonMisconceptions && t4.commonMisconceptions.length > 0) {
|
|
100481
|
+
topicString += `
|
|
100482
|
+
- Common Misconceptions: [${t4.commonMisconceptions.join(", ")}]`;
|
|
100417
100483
|
}
|
|
100484
|
+
return topicString;
|
|
100485
|
+
}).join("\n ");
|
|
100486
|
+
const bloomDistribution = clientInput.bloomLevels.map(
|
|
100487
|
+
(b2) => `- Level: "${b2.level}", Ratio: ${b2.ratio}%`
|
|
100488
|
+
).join("\n ");
|
|
100489
|
+
let questionTypesForPrompt = [...clientInput.selectedQuestionTypes];
|
|
100490
|
+
if (numCodingQuestions === 0) {
|
|
100491
|
+
questionTypesForPrompt = questionTypesForPrompt.filter(
|
|
100492
|
+
(type) => type !== "short_answer" && type !== "coding"
|
|
100493
|
+
);
|
|
100418
100494
|
}
|
|
100419
|
-
|
|
100420
|
-
|
|
100421
|
-
|
|
100422
|
-
|
|
100423
|
-
|
|
100424
|
-
|
|
100425
|
-
}
|
|
100495
|
+
const allowedQuestionTypes = questionTypesForPrompt.map((t4) => `'${t4}'`).join(", ");
|
|
100496
|
+
const codingRequirement = numCodingQuestions > 0 ? `
|
|
100497
|
+
**CRITICAL CODING REQUIREMENT**: Exactly ${numCodingQuestions} questions in the plan MUST be of type 'coding'. These should typically be at 'applying' or higher Bloom levels and focus on implementation, debugging, or algorithm design.` : "";
|
|
100498
|
+
const imageContextSection = imageContexts && imageContexts.length > 0 ? `
|
|
100499
|
+
## AVAILABLE IMAGE CONTEXT LIBRARY
|
|
100500
|
+
You have access to a library of pre-described images. When planning a question, if its subject, category, and topic match an image in this library AND the image's description is relevant to the planned question's topic, you MUST include the corresponding \`imageId\` in your output. Otherwise, leave the \`imageId\` field null or omit it.
|
|
100426
100501
|
|
|
100427
|
-
|
|
100428
|
-
|
|
100502
|
+
\`\`\`json
|
|
100503
|
+
${JSON.stringify(imageContexts.map((img) => ({ imageId: img.id, subject: img.subject, category: img.category, topic: img.topic, description: img.detailedDescription })), null, 2)}
|
|
100504
|
+
\`\`\`
|
|
100505
|
+
` : "";
|
|
100506
|
+
const enhancedPromptText = `You are an elite educational assessment architect with expertise in cognitive science and adaptive learning. Your mission is to create a strategically optimized quiz plan that maximizes learning effectiveness.
|
|
100429
100507
|
|
|
100430
|
-
|
|
100431
|
-
init_react_shim();
|
|
100432
|
-
BaseQuestionGenerationClientInputSchema.extend({
|
|
100433
|
-
allowDecimals: z.boolean().optional().default(true),
|
|
100434
|
-
minRange: z.number().optional(),
|
|
100435
|
-
maxRange: z.number().optional(),
|
|
100436
|
-
// Thêm trường tolerance để client có thể tùy chỉnh
|
|
100437
|
-
tolerance: z.number().min(0).optional().default(0)
|
|
100438
|
-
});
|
|
100439
|
-
var AINumericOutputFieldsSchema = z.object({
|
|
100440
|
-
prompt: z.string(),
|
|
100441
|
-
answer: z.number(),
|
|
100442
|
-
// AI có thể đề xuất một tolerance, nhưng chúng ta sẽ ưu tiên của client
|
|
100443
|
-
tolerance: z.number().min(0).optional(),
|
|
100444
|
-
explanation: z.string().optional(),
|
|
100445
|
-
points: z.number().optional().default(10),
|
|
100446
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
100447
|
-
topic: z.string().optional(),
|
|
100448
|
-
verifiedCategory: z.string().optional()
|
|
100449
|
-
// Thêm để xác thực
|
|
100450
|
-
});
|
|
100508
|
+
${generateQuestionTypeSelectionGuidance()}
|
|
100451
100509
|
|
|
100452
|
-
|
|
100453
|
-
var MAX_RETRY_ATTEMPTS5 = 3;
|
|
100454
|
-
var RETRY_DELAY_MS5 = 3e3;
|
|
100455
|
-
function buildEnhancedPrompt5(clientInput, attemptNumber) {
|
|
100456
|
-
const { quizContext, language: language3, difficulty, minRange, maxRange, allowDecimals, imageUrl } = clientInput;
|
|
100457
|
-
const category = quizContext?.originalCategory || "the specified technical category";
|
|
100458
|
-
const attemptInfo = attemptNumber > 1 ? `
|
|
100459
|
-
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
100460
|
-
Previous attempts failed. Ensure the 'answer' is a valid number and fits within the specified constraints.
|
|
100510
|
+
${generateAdvancedBloomGuidance()}
|
|
100461
100511
|
|
|
100462
|
-
|
|
100463
|
-
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The question and its numerical answer must be directly related to the content of this image.` : "";
|
|
100464
|
-
const contextStrings = [
|
|
100465
|
-
`**Required Category:** ${category}`,
|
|
100466
|
-
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
100467
|
-
imageContextInstruction,
|
|
100468
|
-
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
100469
|
-
quizContext?.targetMisconception && `**Target Misconception:** The question should clarify this numerical error: "${quizContext.targetMisconception}"`
|
|
100470
|
-
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
100471
|
-
const constraintStrings = [
|
|
100472
|
-
minRange !== void 0 && `The final 'answer' MUST be greater than or equal to ${minRange}.`,
|
|
100473
|
-
maxRange !== void 0 && `The final 'answer' MUST be less than or equal to ${maxRange}.`,
|
|
100474
|
-
!allowDecimals && `The final 'answer' MUST be an integer (whole number).`
|
|
100475
|
-
].filter(Boolean).join("\n");
|
|
100476
|
-
const exampleJson = JSON.stringify({
|
|
100477
|
-
prompt: "A Swift `Int` on a 64-bit platform can store a maximum value of 2^63 - 1. What is the maximum value for an `Int8`?",
|
|
100478
|
-
answer: 127,
|
|
100479
|
-
tolerance: 0,
|
|
100480
|
-
explanation: "An Int8 uses 8 bits. One bit is for the sign, leaving 7 bits for the value. The range is from -128 to 127 (2^7 - 1).",
|
|
100481
|
-
points: 10,
|
|
100482
|
-
difficulty: "medium",
|
|
100483
|
-
topic: "Swift Data Types",
|
|
100484
|
-
verifiedCategory: category
|
|
100485
|
-
}, null, 2);
|
|
100486
|
-
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
100487
|
-
Your mission is to create a high-quality, technically accurate Numeric Question.
|
|
100512
|
+
${generateDiversityRules()}
|
|
100488
100513
|
|
|
100489
|
-
##
|
|
100490
|
-
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
100491
|
-
2. **Quantitative Answer:** The question MUST ask for a specific, objective numerical answer.
|
|
100492
|
-
3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
|
|
100514
|
+
## COMPREHENSIVE QUIZ REQUIREMENTS:
|
|
100493
100515
|
|
|
100494
|
-
|
|
100495
|
-
${
|
|
100516
|
+
**Target Language**: ${language3}
|
|
100517
|
+
**Total Questions**: ${totalQuestions}${codingRequirement}
|
|
100496
100518
|
|
|
100497
|
-
|
|
100498
|
-
|
|
100519
|
+
**Topic Distribution & Learning Context** (follow precisely):
|
|
100520
|
+
${topicsDistribution}
|
|
100499
100521
|
|
|
100500
|
-
|
|
100501
|
-
|
|
100502
|
-
- **Language for Text:** ${language3}
|
|
100503
|
-
- **Difficulty Level:** ${difficulty}
|
|
100504
|
-
${constraintStrings ? `
|
|
100505
|
-
### CRITICAL CONSTRAINTS ON THE ANSWER
|
|
100506
|
-
${constraintStrings}` : ""}
|
|
100522
|
+
**Cognitive Complexity Distribution** (follow precisely):
|
|
100523
|
+
${bloomDistribution}
|
|
100507
100524
|
|
|
100508
|
-
|
|
100509
|
-
Your response must be ONLY the JSON object, matching this exact structure:
|
|
100525
|
+
**Available Question Arsenal**: ${allowedQuestionTypes}
|
|
100510
100526
|
|
|
100511
|
-
${
|
|
100527
|
+
**Image Resources**: ${imageContextSection}
|
|
100512
100528
|
|
|
100513
|
-
|
|
100514
|
-
|
|
100515
|
-
|
|
100516
|
-
|
|
100517
|
-
|
|
100518
|
-
|
|
100519
|
-
|
|
100520
|
-
|
|
100521
|
-
|
|
100522
|
-
|
|
100523
|
-
|
|
100524
|
-
|
|
100525
|
-
|
|
100526
|
-
|
|
100529
|
+
## STRATEGIC PLANNING METHODOLOGY:
|
|
100530
|
+
|
|
100531
|
+
1. **Misconception Analysis**: If common misconceptions are provided, design questions specifically to address and correct them
|
|
100532
|
+
2. **Question Type Intelligence**: Select question types based on the cognitive demands and content nature
|
|
100533
|
+
3. **Difficulty Orchestration**: Create a learning journey that builds confidence while challenging appropriately
|
|
100534
|
+
4. **Diversity Optimization**: Ensure variety prevents monotony and maintains engagement
|
|
100535
|
+
5. **Context Sensitivity**: Match question types to topic characteristics (technical, conceptual, procedural)
|
|
100536
|
+
6. **Image Context Integration**: Intelligently associate questions with relevant images from the provided library to enhance contextual understanding.
|
|
100537
|
+
|
|
100538
|
+
## ENHANCED OUTPUT FORMAT:
|
|
100539
|
+
|
|
100540
|
+
Return ONLY a JSON object with this EXACT structure:
|
|
100541
|
+
|
|
100542
|
+
\`\`\`json
|
|
100543
|
+
{
|
|
100544
|
+
"quizPlan": [
|
|
100545
|
+
{
|
|
100546
|
+
"plannedTopic": "Specific, assessable topic derived from provided context",
|
|
100547
|
+
"plannedQuestionType": "question_type_from_allowed_list",
|
|
100548
|
+
"plannedBloomLevel": "bloom_level_from_requirements",
|
|
100549
|
+
"plannedContextId": "THEO_ABS",
|
|
100550
|
+
"imageId": "imgctx_12345abcde", // or null
|
|
100551
|
+
"targetMisconception": "Specific misconception this question addresses (or 'none' if not applicable)",
|
|
100552
|
+
"difficultyReason": "Strategic explanation of difficulty choice and placement",
|
|
100553
|
+
"topicSpecificity": "broad|focused|specific",
|
|
100554
|
+
"originalLoId": "corresponding_LoId_from_input",
|
|
100555
|
+
"originalSubject": "corresponding_Subject_from_input",
|
|
100556
|
+
"originalCategory": "corresponding_Category_from_input",
|
|
100557
|
+
"originalTopic": "corresponding_Topic_from_input"
|
|
100527
100558
|
}
|
|
100528
|
-
|
|
100529
|
-
|
|
100530
|
-
|
|
100531
|
-
|
|
100532
|
-
|
|
100533
|
-
|
|
100534
|
-
|
|
100535
|
-
|
|
100536
|
-
|
|
100537
|
-
|
|
100538
|
-
|
|
100539
|
-
|
|
100540
|
-
|
|
100541
|
-
|
|
100542
|
-
|
|
100543
|
-
|
|
100544
|
-
|
|
100545
|
-
|
|
100546
|
-
|
|
100547
|
-
|
|
100548
|
-
|
|
100549
|
-
|
|
100550
|
-
|
|
100551
|
-
|
|
100552
|
-
|
|
100553
|
-
|
|
100554
|
-
|
|
100555
|
-
|
|
100556
|
-
|
|
100557
|
-
|
|
100558
|
-
if (clientInput.maxRange !== void 0 && answer > clientInput.maxRange) {
|
|
100559
|
-
throw new Error(`AI answer ${answer} is greater than the required maxRange of ${clientInput.maxRange}.`);
|
|
100560
|
-
}
|
|
100561
|
-
if (!clientInput.allowDecimals && !Number.isInteger(answer)) {
|
|
100562
|
-
throw new Error(`AI answer ${answer} is not an integer, but decimals are not allowed.`);
|
|
100563
|
-
}
|
|
100564
|
-
if (clientInput.quizContext?.originalCategory) {
|
|
100565
|
-
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
100566
|
-
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
100567
|
-
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
100568
|
-
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
100569
|
-
}
|
|
100559
|
+
],
|
|
100560
|
+
"diversityMetrics": {
|
|
100561
|
+
"questionTypeDistribution": {"type1": count1, "type2": count2},
|
|
100562
|
+
"bloomLevelDistribution": {"level1": count1, "level2": count2},
|
|
100563
|
+
"maxConsecutiveSameType": number,
|
|
100564
|
+
"difficultyProgression": "description of how difficulty progresses",
|
|
100565
|
+
"misconceptionCoverage": number_of_misconceptions_addressed
|
|
100566
|
+
},
|
|
100567
|
+
"planningStrategy": {
|
|
100568
|
+
"overallApproach": "Brief description of the strategic approach taken",
|
|
100569
|
+
"keyDecisions": ["Major decision 1", "Major decision 2", "Major decision 3"]
|
|
100570
|
+
}
|
|
100571
|
+
}
|
|
100572
|
+
\`\`\`
|
|
100573
|
+
|
|
100574
|
+
Execute this plan with pedagogical precision. The quiz should feel like a carefully crafted learning journey that challenges and educates simultaneously.`;
|
|
100575
|
+
logger.log("PROMPT_PREPARATION", {
|
|
100576
|
+
promptLength: enhancedPromptText.length,
|
|
100577
|
+
topicCount: clientInput.topics.length,
|
|
100578
|
+
misconceptionCount: clientInput.topics.reduce((sum, t4) => sum + (t4.commonMisconceptions?.length || 0), 0)
|
|
100579
|
+
}, Date.now() - promptStartTime);
|
|
100580
|
+
const generationStartTime = Date.now();
|
|
100581
|
+
const contents = [
|
|
100582
|
+
{
|
|
100583
|
+
role: "user",
|
|
100584
|
+
parts: [
|
|
100585
|
+
{
|
|
100586
|
+
text: enhancedPromptText
|
|
100587
|
+
}
|
|
100588
|
+
]
|
|
100570
100589
|
}
|
|
100571
|
-
|
|
100572
|
-
|
|
100573
|
-
|
|
100574
|
-
|
|
100575
|
-
|
|
100576
|
-
|
|
100577
|
-
|
|
100578
|
-
|
|
100579
|
-
|
|
100580
|
-
|
|
100581
|
-
|
|
100582
|
-
|
|
100583
|
-
|
|
100584
|
-
|
|
100585
|
-
|
|
100586
|
-
|
|
100587
|
-
|
|
100588
|
-
|
|
100589
|
-
|
|
100590
|
-
|
|
100591
|
-
|
|
100592
|
-
|
|
100593
|
-
|
|
100594
|
-
}
|
|
100595
|
-
|
|
100596
|
-
|
|
100597
|
-
|
|
100598
|
-
|
|
100599
|
-
|
|
100600
|
-
|
|
100601
|
-
|
|
100602
|
-
|
|
100590
|
+
];
|
|
100591
|
+
const aiResult = await ai.models.generateContent({
|
|
100592
|
+
model,
|
|
100593
|
+
config: config3,
|
|
100594
|
+
contents
|
|
100595
|
+
});
|
|
100596
|
+
const response = aiResult;
|
|
100597
|
+
const generationDuration = Date.now() - generationStartTime;
|
|
100598
|
+
logger.log("AI_GENERATION", {
|
|
100599
|
+
responseLength: response.candidates?.[0]?.content?.parts?.[0]?.text?.length || 0,
|
|
100600
|
+
duration: generationDuration
|
|
100601
|
+
}, generationDuration);
|
|
100602
|
+
const processingStartTime = Date.now();
|
|
100603
|
+
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
100604
|
+
let jsonText = rawText;
|
|
100605
|
+
if (!rawText.trim().startsWith("{") && !rawText.trim().startsWith("[")) {
|
|
100606
|
+
jsonText = extractJsonFromMarkdown(rawText);
|
|
100607
|
+
}
|
|
100608
|
+
logger.log("JSON_EXTRACTION", {
|
|
100609
|
+
rawTextLength: rawText.length,
|
|
100610
|
+
extractedJsonLength: jsonText.length
|
|
100611
|
+
});
|
|
100612
|
+
const aiGeneratedContent = GenerateQuizPlanOutputSchema.parse(JSON.parse(jsonText));
|
|
100613
|
+
logger.log("SCHEMA_VALIDATION", { success: true }, Date.now() - processingStartTime);
|
|
100614
|
+
const validationStartTime = Date.now();
|
|
100615
|
+
if (aiGeneratedContent.quizPlan.length !== clientInput.totalQuestions) {
|
|
100616
|
+
throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested.`);
|
|
100617
|
+
}
|
|
100618
|
+
const invalidTypes = [];
|
|
100619
|
+
aiGeneratedContent.quizPlan.forEach((item, index3) => {
|
|
100620
|
+
if (!clientInput.selectedQuestionTypes.includes(item.plannedQuestionType)) {
|
|
100621
|
+
invalidTypes.push(`Question ${index3 + 1}: '${item.plannedQuestionType}'`);
|
|
100603
100622
|
}
|
|
100623
|
+
});
|
|
100624
|
+
if (invalidTypes.length > 0) {
|
|
100625
|
+
throw new Error(`Invalid question types found: ${invalidTypes.join(", ")}`);
|
|
100604
100626
|
}
|
|
100627
|
+
const codingQuestions = aiGeneratedContent.quizPlan.filter((q2) => q2.plannedQuestionType === "coding");
|
|
100628
|
+
if (numCodingQuestions > 0 && codingQuestions.length !== numCodingQuestions) {
|
|
100629
|
+
throw new Error(`Expected ${numCodingQuestions} coding questions, but got ${codingQuestions.length}`);
|
|
100630
|
+
}
|
|
100631
|
+
const diversityAnalysis = validateConsecutiveTypes(aiGeneratedContent.quizPlan);
|
|
100632
|
+
logger.log("VALIDATION_COMPLETE", {
|
|
100633
|
+
questionCount: aiGeneratedContent.quizPlan.length,
|
|
100634
|
+
codingQuestionCount: codingQuestions.length,
|
|
100635
|
+
maxConsecutiveType: diversityAnalysis.maxConsecutive,
|
|
100636
|
+
questionTypeDistribution: aiGeneratedContent.diversityMetrics?.questionTypeDistribution || {}
|
|
100637
|
+
}, Date.now() - validationStartTime);
|
|
100638
|
+
const finalResult = {
|
|
100639
|
+
...aiGeneratedContent,
|
|
100640
|
+
logs: logger.getLogs()
|
|
100641
|
+
};
|
|
100642
|
+
logger.log("GENERATION_COMPLETE", {
|
|
100643
|
+
totalDuration: logger.getTotalDuration(),
|
|
100644
|
+
success: true,
|
|
100645
|
+
finalQuestionCount: finalResult.quizPlan.length
|
|
100646
|
+
}, logger.getTotalDuration());
|
|
100647
|
+
console.log("\n=== QUIZ PLAN GENERATION SUMMARY ===");
|
|
100648
|
+
console.log(`\u2705 Successfully generated ${finalResult.quizPlan.length} questions`);
|
|
100649
|
+
console.log(`\u23F1\uFE0F Total generation time: ${logger.getTotalDuration()}ms`);
|
|
100650
|
+
console.log(`\u{1F3AF} Question types: ${Object.keys(finalResult.diversityMetrics?.questionTypeDistribution || {}).join(", ")}`);
|
|
100651
|
+
console.log(`\u{1F9E0} Bloom levels: ${Object.keys(finalResult.diversityMetrics?.bloomLevelDistribution || {}).join(", ")}`);
|
|
100652
|
+
if (numCodingQuestions > 0) {
|
|
100653
|
+
console.log(`\u{1F4BB} Coding questions: ${codingQuestions.length}/${numCodingQuestions} required`);
|
|
100654
|
+
}
|
|
100655
|
+
console.log(JSON.stringify(finalResult));
|
|
100656
|
+
console.log("=====================================\n");
|
|
100657
|
+
return finalResult;
|
|
100658
|
+
} catch (error) {
|
|
100659
|
+
logger.log("ERROR", {
|
|
100660
|
+
message: error.message,
|
|
100661
|
+
stack: error.stack,
|
|
100662
|
+
totalDuration: logger.getTotalDuration()
|
|
100663
|
+
});
|
|
100664
|
+
console.error("\u274C Quiz Plan Generation Failed:", error.message);
|
|
100665
|
+
console.error("\u{1F4CB} Full logs available in returned object");
|
|
100666
|
+
throw new Error(`Failed to generate Quiz Plan: ${error.message}`);
|
|
100605
100667
|
}
|
|
100606
|
-
DebugLogger.logAttemptSummary(attemptResults);
|
|
100607
|
-
const errorMessage = `Failed to generate Numeric question after ${MAX_RETRY_ATTEMPTS5} attempts. Last error: ${lastError?.message}`;
|
|
100608
|
-
console.error("\n\u274C Final Result: FAILED");
|
|
100609
|
-
console.error(errorMessage);
|
|
100610
|
-
return { error: errorMessage };
|
|
100611
100668
|
}
|
|
100612
|
-
|
|
100613
|
-
|
|
100614
|
-
|
|
100615
|
-
|
|
100616
|
-
|
|
100617
|
-
|
|
100618
|
-
|
|
100619
|
-
|
|
100620
|
-
|
|
100621
|
-
|
|
100622
|
-
|
|
100623
|
-
|
|
100624
|
-
|
|
100625
|
-
|
|
100626
|
-
|
|
100627
|
-
content: z.string().optional().describe("The text content for a 'text' segment."),
|
|
100628
|
-
acceptedAnswers: z.array(z.string().min(1)).min(1).optional().describe("An array of correct answers for a 'blank' segment.")
|
|
100629
|
-
})).min(1).describe("An array of text and blank segments representing the question."),
|
|
100630
|
-
explanation: z.string().optional(),
|
|
100631
|
-
points: z.number().optional().default(10),
|
|
100632
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
100633
|
-
topic: z.string().optional(),
|
|
100634
|
-
verifiedCategory: z.string().optional()
|
|
100635
|
-
// Thêm để xác thực
|
|
100636
|
-
});
|
|
100637
|
-
|
|
100638
|
-
// src/ai/flows/question-gen/generate-fitb-question.ts
|
|
100639
|
-
var MAX_RETRY_ATTEMPTS6 = 3;
|
|
100640
|
-
var RETRY_DELAY_MS6 = 3e3;
|
|
100641
|
-
function buildEnhancedPrompt6(clientInput, attemptNumber) {
|
|
100642
|
-
const { quizContext, language: language3, difficulty, numberOfBlanks, imageUrl } = clientInput;
|
|
100643
|
-
const category = quizContext?.originalCategory || "the specified technical category";
|
|
100644
|
-
const attemptInfo = attemptNumber > 1 ? `
|
|
100645
|
-
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
100646
|
-
Previous attempts failed. Pay strict attention to the JSON schema, especially the 'segments' array structure. Ensure 'blank' segments have 'acceptedAnswers' and 'text' segments have 'content'.
|
|
100647
|
-
|
|
100648
|
-
` : "";
|
|
100649
|
-
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The question and blanks must be directly related to the content of this image.` : "";
|
|
100650
|
-
const contextStrings = [
|
|
100651
|
-
`**Required Category:** ${category}`,
|
|
100652
|
-
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
100653
|
-
imageContextInstruction,
|
|
100654
|
-
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
100655
|
-
quizContext?.targetMisconception && `**Target Misconception:** Design the blank to test this specific point: "${quizContext.targetMisconception}"`
|
|
100656
|
-
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
100657
|
-
const exampleJson = JSON.stringify({
|
|
100658
|
-
prompt: "Complete the following Swift code snippet.",
|
|
100659
|
-
segments: [
|
|
100660
|
-
{ "type": "text", "content": "To declare a new function in Swift, you use the `" },
|
|
100661
|
-
{ "type": "blank", "acceptedAnswers": ["func"] },
|
|
100662
|
-
{ "type": "text", "content": "` keyword." }
|
|
100663
|
-
],
|
|
100664
|
-
explanation: "The 'func' keyword is used to declare a function in the Swift programming language.",
|
|
100665
|
-
points: 10,
|
|
100666
|
-
difficulty: "easy",
|
|
100667
|
-
topic: "Swift Function Declaration",
|
|
100668
|
-
verifiedCategory: category
|
|
100669
|
-
}, null, 2);
|
|
100670
|
-
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
100671
|
-
Your mission is to create a high-quality, technically accurate Fill-in-the-Blanks Question.
|
|
100672
|
-
|
|
100673
|
-
## Core Rules (Non-negotiable)
|
|
100674
|
-
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
100675
|
-
2. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
|
|
100676
|
-
3. **Logical Segments:** For 'blank' segments, you MUST provide 'acceptedAnswers'. For 'text' segments, you MUST provide 'content'. Do not mix them.
|
|
100677
|
-
|
|
100678
|
-
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
100679
|
-
${contextStrings}
|
|
100680
|
-
|
|
100681
|
-
## Task: Generate the Question
|
|
100682
|
-
Based on all the rules and context above, generate a single Fill-in-the-Blanks Question.
|
|
100683
|
-
|
|
100684
|
-
### Input Parameters
|
|
100685
|
-
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
100686
|
-
- **Language for Text:** ${language3}
|
|
100687
|
-
- **Difficulty Level:** ${difficulty}
|
|
100688
|
-
- **Number of Blanks:** Generate exactly ${numberOfBlanks} segment(s) with type 'blank'.
|
|
100689
|
-
|
|
100690
|
-
### Required JSON Output Format
|
|
100691
|
-
Your response must be ONLY the JSON object, matching this exact structure:
|
|
100692
|
-
|
|
100693
|
-
${exampleJson}
|
|
100694
|
-
|
|
100695
|
-
Now, generate the JSON for the requested question.`;
|
|
100696
|
-
}
|
|
100697
|
-
async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
|
|
100698
|
-
const ai = new GoogleGenAI({ apiKey });
|
|
100699
|
-
const model = "gemini-2.5-flash";
|
|
100700
|
-
const config3 = {
|
|
100701
|
-
temperature: 0.8,
|
|
100702
|
-
responseMimeType: "application/json",
|
|
100703
|
-
thinkingConfig: {
|
|
100704
|
-
thinkingBudget: 4e3
|
|
100705
|
-
}
|
|
100706
|
-
};
|
|
100707
|
-
const attemptResults = [];
|
|
100708
|
-
let lastError = null;
|
|
100709
|
-
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS6; attempt++) {
|
|
100710
|
-
const startTime = Date.now();
|
|
100711
|
-
const promptText = buildEnhancedPrompt6(clientInput, attempt);
|
|
100712
|
-
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
100713
|
-
try {
|
|
100714
|
-
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
100715
|
-
const parts = [{ text: promptText }];
|
|
100716
|
-
if (clientInput.imageUrl) {
|
|
100717
|
-
const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
100718
|
-
const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
|
|
100719
|
-
parts.unshift(imagePart);
|
|
100720
|
-
}
|
|
100721
|
-
const contents = [{ role: "user", parts }];
|
|
100722
|
-
const aiResult = await ai.models.generateContent({ model, config: config3, contents });
|
|
100723
|
-
const response = aiResult;
|
|
100724
|
-
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
100725
|
-
const duration = Date.now() - startTime;
|
|
100726
|
-
DebugLogger.logResponse(attempt, rawText);
|
|
100727
|
-
if (!rawText) throw new Error("AI returned an empty response.");
|
|
100728
|
-
const parsedJson = JSON.parse(rawText);
|
|
100729
|
-
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
100730
|
-
const aiGeneratedContent = AIFillInTheBlanksOutputFieldsSchema.parse(parsedJson);
|
|
100731
|
-
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
100732
|
-
const blankCount = aiGeneratedContent.segments.filter((s4) => s4.type === "blank").length;
|
|
100733
|
-
if (blankCount !== clientInput.numberOfBlanks) {
|
|
100734
|
-
throw new Error(`AI generated ${blankCount} blanks, but ${clientInput.numberOfBlanks} were required.`);
|
|
100735
|
-
}
|
|
100736
|
-
aiGeneratedContent.segments.forEach((segment, index3) => {
|
|
100737
|
-
if (segment.type === "blank" && (!segment.acceptedAnswers || segment.acceptedAnswers.length === 0)) {
|
|
100738
|
-
throw new Error(`Segment ${index3} is a 'blank' but is missing 'acceptedAnswers'.`);
|
|
100739
|
-
}
|
|
100740
|
-
if (segment.type === "text" && typeof segment.content !== "string") {
|
|
100741
|
-
throw new Error(`Segment ${index3} is 'text' but is missing 'content'.`);
|
|
100742
|
-
}
|
|
100743
|
-
});
|
|
100744
|
-
if (clientInput.quizContext?.originalCategory) {
|
|
100745
|
-
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
100746
|
-
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
100747
|
-
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
100748
|
-
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
100749
|
-
}
|
|
100750
|
-
}
|
|
100751
|
-
const finalSegments = [];
|
|
100752
|
-
const finalAnswers = [];
|
|
100753
|
-
aiGeneratedContent.segments.forEach((segment) => {
|
|
100754
|
-
if (segment.type === "text") {
|
|
100755
|
-
finalSegments.push({ type: "text", content: segment.content });
|
|
100756
|
-
} else if (segment.type === "blank" && segment.acceptedAnswers) {
|
|
100757
|
-
const blankId = generateUniqueId("blank_");
|
|
100758
|
-
finalSegments.push({ type: "blank", id: blankId });
|
|
100759
|
-
finalAnswers.push({ blankId, acceptedValues: segment.acceptedAnswers });
|
|
100760
|
-
}
|
|
100761
|
-
});
|
|
100762
|
-
const completeQuestion = {
|
|
100763
|
-
id: generateUniqueId("fitb_ai_"),
|
|
100764
|
-
questionType: "fill_in_the_blanks",
|
|
100765
|
-
prompt: aiGeneratedContent.prompt,
|
|
100766
|
-
segments: finalSegments,
|
|
100767
|
-
answers: finalAnswers,
|
|
100768
|
-
isCaseSensitive: clientInput.isCaseSensitive,
|
|
100769
|
-
explanation: aiGeneratedContent.explanation,
|
|
100770
|
-
points: aiGeneratedContent.points,
|
|
100771
|
-
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
100772
|
-
difficulty: clientInput.difficulty,
|
|
100773
|
-
contextCode: clientInput.quizContext?.plannedContextId,
|
|
100774
|
-
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
100775
|
-
learningObjective: clientInput.quizContext?.originalLoId,
|
|
100776
|
-
subject: clientInput.quizContext?.originalSubject,
|
|
100777
|
-
category: clientInput.quizContext?.originalCategory,
|
|
100778
|
-
imageUrl: clientInput.imageUrl
|
|
100779
|
-
};
|
|
100780
|
-
const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
|
|
100781
|
-
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
100782
|
-
console.log(`
|
|
100783
|
-
\u2705 FITB generation successful on attempt ${attempt} (${duration}ms)`);
|
|
100784
|
-
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
100785
|
-
return { question: validatedQuestion };
|
|
100786
|
-
} catch (error) {
|
|
100787
|
-
lastError = error;
|
|
100788
|
-
const duration = Date.now() - startTime;
|
|
100789
|
-
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
100790
|
-
const willRetry = attempt < MAX_RETRY_ATTEMPTS6;
|
|
100791
|
-
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
100792
|
-
if (willRetry) {
|
|
100793
|
-
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS6}ms...`);
|
|
100794
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS6));
|
|
100669
|
+
function validateConsecutiveTypes(quizPlan) {
|
|
100670
|
+
let maxConsecutive = 1;
|
|
100671
|
+
let maxType = quizPlan[0]?.plannedQuestionType || "";
|
|
100672
|
+
let maxStartIndex = 0;
|
|
100673
|
+
let currentConsecutive = 1;
|
|
100674
|
+
let currentType = quizPlan[0]?.plannedQuestionType || "";
|
|
100675
|
+
let currentStartIndex = 0;
|
|
100676
|
+
for (let i2 = 1; i2 < quizPlan.length; i2++) {
|
|
100677
|
+
if (quizPlan[i2].plannedQuestionType === currentType) {
|
|
100678
|
+
currentConsecutive++;
|
|
100679
|
+
} else {
|
|
100680
|
+
if (currentConsecutive > maxConsecutive) {
|
|
100681
|
+
maxConsecutive = currentConsecutive;
|
|
100682
|
+
maxType = currentType;
|
|
100683
|
+
maxStartIndex = currentStartIndex;
|
|
100795
100684
|
}
|
|
100685
|
+
currentConsecutive = 1;
|
|
100686
|
+
currentType = quizPlan[i2].plannedQuestionType;
|
|
100687
|
+
currentStartIndex = i2;
|
|
100796
100688
|
}
|
|
100797
100689
|
}
|
|
100798
|
-
|
|
100799
|
-
|
|
100800
|
-
|
|
100801
|
-
|
|
100802
|
-
|
|
100803
|
-
|
|
100804
|
-
|
|
100805
|
-
|
|
100806
|
-
|
|
100807
|
-
|
|
100808
|
-
|
|
100809
|
-
init_react_shim();
|
|
100810
|
-
BaseQuestionGenerationClientInputSchema.extend({
|
|
100811
|
-
numberOfItems: z.number().int().min(2).max(10).optional().default(4)
|
|
100812
|
-
});
|
|
100813
|
-
var AISequenceOutputFieldsSchema = z.object({
|
|
100814
|
-
prompt: z.string().describe("The instructional text for the user, e.g., 'Arrange the steps in the correct order.'"),
|
|
100815
|
-
// Yêu cầu AI cung cấp các item dưới dạng một mảng duy nhất đã được sắp xếp đúng thứ tự
|
|
100816
|
-
// Điều này đơn giản hóa logic và giảm rủi ro
|
|
100817
|
-
itemsInCorrectOrder: z.array(z.string().min(1)).min(2).describe("An array of strings, with each string representing an item to be sequenced. The array itself MUST be in the correct final order."),
|
|
100818
|
-
explanation: z.string().optional(),
|
|
100819
|
-
points: z.number().optional().default(10),
|
|
100820
|
-
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
100821
|
-
topic: z.string().optional(),
|
|
100822
|
-
verifiedCategory: z.string().optional()
|
|
100823
|
-
// Thêm để xác thực
|
|
100824
|
-
});
|
|
100825
|
-
|
|
100826
|
-
// src/ai/flows/question-gen/generate-sequence-question.ts
|
|
100827
|
-
var MAX_RETRY_ATTEMPTS7 = 3;
|
|
100828
|
-
var RETRY_DELAY_MS7 = 3e3;
|
|
100829
|
-
function buildEnhancedPrompt7(clientInput, attemptNumber) {
|
|
100830
|
-
const { quizContext, language: language3, difficulty, numberOfItems, imageUrl } = clientInput;
|
|
100831
|
-
const category = quizContext?.originalCategory || "the specified technical category";
|
|
100832
|
-
const attemptInfo = attemptNumber > 1 ? `
|
|
100833
|
-
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
100834
|
-
Previous attempts failed. Ensure the 'itemsInCorrectOrder' array has exactly the required number of items and the JSON is valid.
|
|
100835
|
-
|
|
100836
|
-
` : "";
|
|
100837
|
-
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The sequence of items must be directly related to the process or content shown in this image.` : "";
|
|
100838
|
-
const contextStrings = [
|
|
100839
|
-
`**Required Category:** ${category}`,
|
|
100840
|
-
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
100841
|
-
imageContextInstruction,
|
|
100842
|
-
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
100843
|
-
quizContext?.targetMisconception && `**Target Misconception:** The sequence should clarify this specific process error: "${quizContext.targetMisconception}"`
|
|
100844
|
-
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
100845
|
-
const exampleJson = JSON.stringify({
|
|
100846
|
-
prompt: "Arrange the steps to make a network request in Swift using URLSession.",
|
|
100847
|
-
itemsInCorrectOrder: [
|
|
100848
|
-
"Create a URL object.",
|
|
100849
|
-
"Create a URLSessionDataTask with the URL.",
|
|
100850
|
-
"Start the task by calling its resume() method.",
|
|
100851
|
-
"Handle the completion handler with data, response, and error."
|
|
100852
|
-
],
|
|
100853
|
-
explanation: "This is the fundamental sequence for a basic data task in URLSession.",
|
|
100854
|
-
points: 10,
|
|
100855
|
-
difficulty: "medium",
|
|
100856
|
-
topic: "Swift Networking",
|
|
100857
|
-
verifiedCategory: category
|
|
100858
|
-
}, null, 2);
|
|
100859
|
-
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
100860
|
-
Your mission is to create a high-quality, technically accurate Sequence Question.
|
|
100861
|
-
|
|
100862
|
-
## Core Rules (Non-negotiable)
|
|
100863
|
-
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
100864
|
-
2. **Unambiguous Order:** The items MUST represent a clear, objective process, timeline, or order with only one correct sequence.
|
|
100865
|
-
3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
|
|
100866
|
-
|
|
100867
|
-
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
100868
|
-
${contextStrings}
|
|
100869
|
-
|
|
100870
|
-
## Task: Generate the Question
|
|
100871
|
-
Based on all the rules and context above, generate a single Sequence Question.
|
|
100872
|
-
|
|
100873
|
-
### Input Parameters
|
|
100874
|
-
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
100875
|
-
- **Language for Text:** ${language3}
|
|
100876
|
-
- **Difficulty Level:** ${difficulty}
|
|
100877
|
-
- **Number of Items:** Generate an array 'itemsInCorrectOrder' containing exactly ${numberOfItems} items. The array itself MUST be in the correct final sequence.
|
|
100878
|
-
|
|
100879
|
-
### Required JSON Output Format
|
|
100880
|
-
Your response must be ONLY the JSON object, matching this exact structure:
|
|
100690
|
+
if (currentConsecutive > maxConsecutive) {
|
|
100691
|
+
maxConsecutive = currentConsecutive;
|
|
100692
|
+
maxType = currentType;
|
|
100693
|
+
maxStartIndex = currentStartIndex;
|
|
100694
|
+
}
|
|
100695
|
+
return {
|
|
100696
|
+
maxConsecutive,
|
|
100697
|
+
type: maxType,
|
|
100698
|
+
startIndex: maxStartIndex
|
|
100699
|
+
};
|
|
100700
|
+
}
|
|
100881
100701
|
|
|
100882
|
-
|
|
100702
|
+
// src/ai/flows/generate-questions-from-quiz-plan.ts
|
|
100703
|
+
init_react_shim();
|
|
100883
100704
|
|
|
100884
|
-
|
|
100885
|
-
|
|
100886
|
-
|
|
100887
|
-
|
|
100888
|
-
|
|
100889
|
-
|
|
100890
|
-
|
|
100891
|
-
|
|
100892
|
-
|
|
100893
|
-
|
|
100705
|
+
// src/services/TopicDataService.ts
|
|
100706
|
+
init_react_shim();
|
|
100707
|
+
var TopicDataService = class {
|
|
100708
|
+
/**
|
|
100709
|
+
* Saves an array of LearningObjective objects to Local Storage, overwriting existing data.
|
|
100710
|
+
* @param data The array of learning objectives to save.
|
|
100711
|
+
*/
|
|
100712
|
+
static saveData(data) {
|
|
100713
|
+
try {
|
|
100714
|
+
if (typeof window === "undefined") return;
|
|
100715
|
+
const serializedData = JSON.stringify(data);
|
|
100716
|
+
localStorage.setItem(this.STORAGE_KEY, serializedData);
|
|
100717
|
+
} catch (error) {
|
|
100718
|
+
console.error("Error saving learning objectives to Local Storage:", error);
|
|
100894
100719
|
}
|
|
100895
|
-
}
|
|
100896
|
-
|
|
100897
|
-
|
|
100898
|
-
|
|
100899
|
-
|
|
100900
|
-
|
|
100901
|
-
|
|
100720
|
+
}
|
|
100721
|
+
/**
|
|
100722
|
+
* Merges a new set of learning objectives with the existing data in Local Storage.
|
|
100723
|
+
* If an LO ID from newData already exists, it will be updated. Otherwise, it will be added.
|
|
100724
|
+
* @param newData The array of new or updated learning objectives.
|
|
100725
|
+
*/
|
|
100726
|
+
static mergeData(newData) {
|
|
100727
|
+
const existingData = this.getData();
|
|
100728
|
+
const loMap = new Map(existingData.map((lo) => [lo.loId, lo]));
|
|
100729
|
+
newData.forEach((newLo) => {
|
|
100730
|
+
loMap.set(newLo.loId, newLo);
|
|
100731
|
+
});
|
|
100732
|
+
const mergedData = Array.from(loMap.values());
|
|
100733
|
+
this.saveData(mergedData);
|
|
100734
|
+
}
|
|
100735
|
+
/**
|
|
100736
|
+
* Retrieves the array of LearningObjective objects from Local Storage.
|
|
100737
|
+
* @returns An array of learning objectives, or an empty array if none are found or an error occurs.
|
|
100738
|
+
*/
|
|
100739
|
+
static getData() {
|
|
100902
100740
|
try {
|
|
100903
|
-
|
|
100904
|
-
const
|
|
100905
|
-
|
|
100906
|
-
|
|
100907
|
-
|
|
100908
|
-
|
|
100909
|
-
|
|
100910
|
-
|
|
100911
|
-
|
|
100912
|
-
|
|
100913
|
-
|
|
100914
|
-
|
|
100915
|
-
|
|
100916
|
-
|
|
100917
|
-
|
|
100918
|
-
|
|
100919
|
-
|
|
100920
|
-
|
|
100921
|
-
|
|
100922
|
-
|
|
100741
|
+
if (typeof window === "undefined") return [];
|
|
100742
|
+
const storedData = localStorage.getItem(this.STORAGE_KEY);
|
|
100743
|
+
return storedData ? JSON.parse(storedData) : [];
|
|
100744
|
+
} catch (error) {
|
|
100745
|
+
console.error("Error retrieving learning objectives from Local Storage:", error);
|
|
100746
|
+
this.clearData();
|
|
100747
|
+
return [];
|
|
100748
|
+
}
|
|
100749
|
+
}
|
|
100750
|
+
/**
|
|
100751
|
+
* Removes all learning objective data from Local Storage.
|
|
100752
|
+
*/
|
|
100753
|
+
static clearData() {
|
|
100754
|
+
try {
|
|
100755
|
+
if (typeof window === "undefined") return;
|
|
100756
|
+
localStorage.removeItem(this.STORAGE_KEY);
|
|
100757
|
+
} catch (error) {
|
|
100758
|
+
console.error("Error clearing learning objectives from Local Storage:", error);
|
|
100759
|
+
}
|
|
100760
|
+
}
|
|
100761
|
+
/**
|
|
100762
|
+
* Parses TSV content into an array of LearningObjective objects.
|
|
100763
|
+
* @param tsvContent The raw string content from a .tsv file.
|
|
100764
|
+
* @returns An object containing the successfully parsed data and any errors encountered.
|
|
100765
|
+
*/
|
|
100766
|
+
static parseTSV(tsvContent) {
|
|
100767
|
+
const lines = tsvContent.split("\n").filter((line) => line.trim() !== "");
|
|
100768
|
+
if (lines.length < 2) {
|
|
100769
|
+
return { data: [], errors: ["File is empty or contains only a header."] };
|
|
100770
|
+
}
|
|
100771
|
+
const headerLine = lines.shift();
|
|
100772
|
+
const headers = headerLine.split(" ").map((h3) => h3.trim());
|
|
100773
|
+
if (headers.length !== this.EXPECTED_HEADERS.length || !this.EXPECTED_HEADERS.every((h3, i2) => h3 === headers[i2])) {
|
|
100774
|
+
const errorMsg = `Invalid TSV header. Expected: "${this.EXPECTED_HEADERS.join(" ")}". Received: "${headers.join(" ")}"`;
|
|
100775
|
+
return { data: [], errors: [errorMsg] };
|
|
100776
|
+
}
|
|
100777
|
+
const data = [];
|
|
100778
|
+
const errors2 = [];
|
|
100779
|
+
lines.forEach((line, index3) => {
|
|
100780
|
+
const values = line.split(" ").map((v) => v.trim());
|
|
100781
|
+
if (values.length !== this.EXPECTED_HEADERS.length) {
|
|
100782
|
+
errors2.push(`Line ${index3 + 2}: Incorrect number of columns. Expected ${this.EXPECTED_HEADERS.length}, but got ${values.length}.`);
|
|
100783
|
+
return;
|
|
100923
100784
|
}
|
|
100924
|
-
|
|
100925
|
-
|
|
100926
|
-
|
|
100927
|
-
|
|
100928
|
-
|
|
100929
|
-
|
|
100785
|
+
const [
|
|
100786
|
+
loId,
|
|
100787
|
+
loDescription,
|
|
100788
|
+
subject,
|
|
100789
|
+
category,
|
|
100790
|
+
topic,
|
|
100791
|
+
keywordsStr,
|
|
100792
|
+
grade,
|
|
100793
|
+
stemElementsStr,
|
|
100794
|
+
bloomLevelsStr
|
|
100795
|
+
] = values;
|
|
100796
|
+
if (!loId || !subject || !category || !topic) {
|
|
100797
|
+
errors2.push(`Line ${index3 + 2}: Missing required fields (LO ID, Subject, Category, or Topic).`);
|
|
100798
|
+
return;
|
|
100930
100799
|
}
|
|
100931
|
-
const
|
|
100932
|
-
|
|
100933
|
-
|
|
100934
|
-
|
|
100935
|
-
|
|
100936
|
-
|
|
100937
|
-
|
|
100938
|
-
|
|
100939
|
-
|
|
100940
|
-
|
|
100941
|
-
prompt: aiGeneratedContent.prompt,
|
|
100942
|
-
items: finalItems,
|
|
100943
|
-
correctOrder: finalCorrectOrder,
|
|
100944
|
-
explanation: aiGeneratedContent.explanation,
|
|
100945
|
-
points: aiGeneratedContent.points,
|
|
100946
|
-
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
100947
|
-
difficulty: clientInput.difficulty,
|
|
100948
|
-
contextCode: clientInput.quizContext?.plannedContextId,
|
|
100949
|
-
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
100950
|
-
learningObjective: clientInput.quizContext?.originalLoId,
|
|
100951
|
-
subject: clientInput.quizContext?.originalSubject,
|
|
100952
|
-
category: clientInput.quizContext?.originalCategory,
|
|
100953
|
-
imageUrl: clientInput.imageUrl
|
|
100800
|
+
const learningObjective = {
|
|
100801
|
+
loId,
|
|
100802
|
+
loDescription,
|
|
100803
|
+
subject,
|
|
100804
|
+
category,
|
|
100805
|
+
topic,
|
|
100806
|
+
keywords: keywordsStr.split(",").map((k3) => k3.trim()).filter(Boolean),
|
|
100807
|
+
grade,
|
|
100808
|
+
stemElements: stemElementsStr.split(",").map((s4) => s4.trim()).filter(Boolean),
|
|
100809
|
+
bloomLevelsGuideline: bloomLevelsStr.split(",").map((b2) => b2.trim()).filter(Boolean)
|
|
100954
100810
|
};
|
|
100955
|
-
|
|
100956
|
-
|
|
100957
|
-
|
|
100958
|
-
\u2705 Sequence generation successful on attempt ${attempt} (${duration}ms)`);
|
|
100959
|
-
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
100960
|
-
return { question: validatedQuestion };
|
|
100961
|
-
} catch (error) {
|
|
100962
|
-
lastError = error;
|
|
100963
|
-
const duration = Date.now() - startTime;
|
|
100964
|
-
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
100965
|
-
const willRetry = attempt < MAX_RETRY_ATTEMPTS7;
|
|
100966
|
-
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
100967
|
-
if (willRetry) {
|
|
100968
|
-
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS7}ms...`);
|
|
100969
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS7));
|
|
100970
|
-
}
|
|
100971
|
-
}
|
|
100811
|
+
data.push(learningObjective);
|
|
100812
|
+
});
|
|
100813
|
+
return { data, errors: errors2 };
|
|
100972
100814
|
}
|
|
100973
|
-
|
|
100974
|
-
|
|
100975
|
-
|
|
100976
|
-
|
|
100977
|
-
|
|
100978
|
-
|
|
100815
|
+
/**
|
|
100816
|
+
* Gets a unique list of all subjects from the stored data.
|
|
100817
|
+
* @returns An array of subject strings.
|
|
100818
|
+
*/
|
|
100819
|
+
static getSubjects() {
|
|
100820
|
+
const data = this.getData();
|
|
100821
|
+
const subjects = data.map((item) => item.subject);
|
|
100822
|
+
return [...new Set(subjects)].sort();
|
|
100823
|
+
}
|
|
100824
|
+
/**
|
|
100825
|
+
* Gets a unique list of categories for a given subject.
|
|
100826
|
+
* @param subject The subject to filter by.
|
|
100827
|
+
* @returns An array of category strings.
|
|
100828
|
+
*/
|
|
100829
|
+
static getCategoriesBySubject(subject) {
|
|
100830
|
+
const data = this.getData();
|
|
100831
|
+
const categories = data.filter((item) => item.subject === subject).map((item) => item.category);
|
|
100832
|
+
return [...new Set(categories)].sort();
|
|
100833
|
+
}
|
|
100834
|
+
/**
|
|
100835
|
+
* Gets a unique list of topics for a given category.
|
|
100836
|
+
* @param category The category to filter by.
|
|
100837
|
+
* @returns An array of topic strings.
|
|
100838
|
+
*/
|
|
100839
|
+
static getTopicsByCategory(category) {
|
|
100840
|
+
const data = this.getData();
|
|
100841
|
+
const topics = data.filter((item) => item.category === category).map((item) => item.topic);
|
|
100842
|
+
return [...new Set(topics)].sort();
|
|
100843
|
+
}
|
|
100844
|
+
/**
|
|
100845
|
+
* Retrieves all LearningObjective details for a given list of topics.
|
|
100846
|
+
* @param topics An array of topic strings to search for.
|
|
100847
|
+
* @returns An array of matching LearningObjective objects.
|
|
100848
|
+
*/
|
|
100849
|
+
static getLearningObjectivesByTopics(topics) {
|
|
100850
|
+
const data = this.getData();
|
|
100851
|
+
const topicSet = new Set(topics);
|
|
100852
|
+
return data.filter((item) => topicSet.has(item.topic));
|
|
100853
|
+
}
|
|
100854
|
+
};
|
|
100855
|
+
TopicDataService.STORAGE_KEY = "interactive_quiz_kit_learning_objectives";
|
|
100856
|
+
TopicDataService.EXPECTED_HEADERS = [
|
|
100857
|
+
"LO ID",
|
|
100858
|
+
"LO Description",
|
|
100859
|
+
"Subject",
|
|
100860
|
+
"Category",
|
|
100861
|
+
"Topic",
|
|
100862
|
+
"Keywords",
|
|
100863
|
+
"Grade",
|
|
100864
|
+
"STEM Element(s)",
|
|
100865
|
+
"Bloom\u2019s Level(s) Guideline"
|
|
100866
|
+
];
|
|
100979
100867
|
|
|
100980
|
-
// src/ai/flows/question-gen/generate-
|
|
100868
|
+
// src/ai/flows/question-gen/generate-mrq-question.ts
|
|
100981
100869
|
init_react_shim();
|
|
100982
100870
|
|
|
100983
|
-
// src/ai/flows/question-gen/generate-
|
|
100871
|
+
// src/ai/flows/question-gen/generate-mrq-question-types.ts
|
|
100984
100872
|
init_react_shim();
|
|
100985
100873
|
BaseQuestionGenerationClientInputSchema.extend({
|
|
100986
|
-
|
|
100987
|
-
|
|
100874
|
+
numberOfOptions: z.number().int().min(2).max(8).optional().default(5),
|
|
100875
|
+
minCorrectAnswers: z.number().int().min(1).optional().default(2),
|
|
100876
|
+
maxCorrectAnswers: z.number().int().min(1).optional().default(3)
|
|
100988
100877
|
});
|
|
100989
|
-
var
|
|
100990
|
-
prompt: z.string()
|
|
100991
|
-
|
|
100992
|
-
|
|
100993
|
-
optionText: z.string().min(1).describe("The text for the right-hand side item (the matching option).")
|
|
100994
|
-
})).min(2),
|
|
100878
|
+
var AIMRQOutputFieldsSchema = z.object({
|
|
100879
|
+
prompt: z.string(),
|
|
100880
|
+
options: z.array(z.object({ tempId: z.string(), text: z.string() })).min(2).max(8),
|
|
100881
|
+
correctTempOptionIds: z.array(z.string()).min(1),
|
|
100995
100882
|
explanation: z.string().optional(),
|
|
100996
100883
|
points: z.number().optional().default(10),
|
|
100997
100884
|
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
100998
100885
|
topic: z.string().optional(),
|
|
100999
|
-
verifiedCategory: z.string().optional()
|
|
101000
|
-
// Thêm để xác thực
|
|
100886
|
+
verifiedCategory: z.string().optional().describe("The category this question actually addresses.")
|
|
101001
100887
|
});
|
|
101002
100888
|
|
|
101003
|
-
// src/ai/flows/question-gen/generate-
|
|
101004
|
-
var
|
|
101005
|
-
var
|
|
101006
|
-
function
|
|
101007
|
-
const { quizContext, language: language3, difficulty,
|
|
100889
|
+
// src/ai/flows/question-gen/generate-mrq-question.ts
|
|
100890
|
+
var MAX_RETRY_ATTEMPTS3 = 3;
|
|
100891
|
+
var RETRY_DELAY_MS3 = 3e3;
|
|
100892
|
+
function buildEnhancedPrompt3(clientInput, attemptNumber) {
|
|
100893
|
+
const { quizContext, language: language3, difficulty, numberOfOptions, minCorrectAnswers, maxCorrectAnswers, imageUrl } = clientInput;
|
|
101008
100894
|
const category = quizContext?.originalCategory || "the specified technical category";
|
|
101009
100895
|
const attemptInfo = attemptNumber > 1 ? `
|
|
101010
100896
|
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
101011
|
-
Previous attempts failed
|
|
100897
|
+
Previous attempts failed due to validation errors. Pay close attention to the number of correct answers and the JSON schema.
|
|
101012
100898
|
|
|
101013
100899
|
` : "";
|
|
101014
|
-
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The
|
|
100900
|
+
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The question and options must be directly related to the content of this image.` : "";
|
|
101015
100901
|
const contextStrings = [
|
|
101016
|
-
`**Required Category:** ${category}`,
|
|
100902
|
+
`**Required Category:** ${category} (This is the ONLY language to be used)`,
|
|
101017
100903
|
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
101018
100904
|
imageContextInstruction,
|
|
101019
100905
|
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
101020
|
-
quizContext?.targetMisconception && `**Target Misconception:**
|
|
100906
|
+
quizContext?.targetMisconception && `**Target Misconception:** Use this to create plausible incorrect answers (distractors). The misconception is: "${quizContext.targetMisconception}"`,
|
|
100907
|
+
quizContext?.difficultyReason && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
|
|
101021
100908
|
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
101022
100909
|
const exampleJson = JSON.stringify({
|
|
101023
|
-
prompt: "
|
|
101024
|
-
|
|
101025
|
-
{ "
|
|
101026
|
-
{ "
|
|
101027
|
-
{ "
|
|
100910
|
+
prompt: "Which of the following are considered programming paradigms?",
|
|
100911
|
+
options: [
|
|
100912
|
+
{ "tempId": "A", "text": "Object-Oriented" },
|
|
100913
|
+
{ "tempId": "B", "text": "Assembly" },
|
|
100914
|
+
{ "tempId": "C", "text": "Functional" },
|
|
100915
|
+
{ "tempId": "D", "text": "Procedural" },
|
|
100916
|
+
{ "tempId": "E", "text": "Middleware" }
|
|
101028
100917
|
],
|
|
101029
|
-
|
|
100918
|
+
correctTempOptionIds: ["A", "C", "D"],
|
|
100919
|
+
explanation: "Object-Oriented, Functional, and Procedural are all major programming paradigms. Assembly is a low-level language, and Middleware is a type of software, not a paradigm.",
|
|
101030
100920
|
points: 10,
|
|
101031
|
-
difficulty: "
|
|
101032
|
-
topic: "
|
|
100921
|
+
difficulty: "medium",
|
|
100922
|
+
topic: "Programming Paradigms",
|
|
101033
100923
|
verifiedCategory: category
|
|
101034
100924
|
}, null, 2);
|
|
101035
|
-
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
101036
|
-
Your mission is to create a high-quality, technically accurate
|
|
100925
|
+
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in the programming language: ${category}.
|
|
100926
|
+
Your sole mission is to create a high-quality, technically accurate Multiple Response Question. You must adhere to the following rules at all times.
|
|
101037
100927
|
|
|
101038
100928
|
## Core Rules (Non-negotiable)
|
|
101039
|
-
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
101040
|
-
2. **
|
|
101041
|
-
3. **
|
|
100929
|
+
1. **Category Purity:** The question, options, and explanation MUST be exclusively about **${category}**.
|
|
100930
|
+
2. **Context Adherence:** The question's content must directly align with all provided context.
|
|
100931
|
+
3. **Format Integrity:** You MUST return ONLY a single, valid JSON object that strictly follows the provided schema. Do not include any extra text or comments.
|
|
101042
100932
|
|
|
101043
100933
|
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
101044
100934
|
${contextStrings}
|
|
101045
100935
|
|
|
101046
100936
|
## Task: Generate the Question
|
|
101047
|
-
Based on all the rules and context above, generate a single
|
|
100937
|
+
Based on all the rules and context above, generate a single Multiple Response Question.
|
|
101048
100938
|
|
|
101049
100939
|
### Input Parameters
|
|
101050
100940
|
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
101051
100941
|
- **Language for Text:** ${language3}
|
|
101052
100942
|
- **Difficulty Level:** ${difficulty}
|
|
101053
|
-
- **Number of
|
|
100943
|
+
- **Number of Options:** Generate exactly ${numberOfOptions} options.
|
|
100944
|
+
- **Number of Correct Answers:** The 'correctTempOptionIds' array MUST contain between ${minCorrectAnswers} and ${maxCorrectAnswers} valid IDs from the options you generate.
|
|
101054
100945
|
|
|
101055
100946
|
### Required JSON Output Format
|
|
101056
|
-
Your response must be ONLY the JSON object, matching this exact structure
|
|
100947
|
+
Your response must be ONLY the JSON object, matching this exact structure and field names.
|
|
101057
100948
|
|
|
101058
100949
|
${exampleJson}
|
|
101059
100950
|
|
|
101060
100951
|
Now, generate the JSON for the requested question.`;
|
|
101061
100952
|
}
|
|
101062
|
-
async function
|
|
100953
|
+
async function generateMRQQuestion(clientInput, apiKey) {
|
|
100954
|
+
if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
|
|
100955
|
+
return { error: `Invalid input: minCorrectAnswers (${clientInput.minCorrectAnswers}) cannot be greater than maxCorrectAnswers (${clientInput.maxCorrectAnswers}).` };
|
|
100956
|
+
}
|
|
100957
|
+
if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
|
|
100958
|
+
return { error: `Invalid input: maxCorrectAnswers (${clientInput.maxCorrectAnswers}) must be less than the total numberOfOptions (${clientInput.numberOfOptions}).` };
|
|
100959
|
+
}
|
|
101063
100960
|
const ai = new GoogleGenAI({ apiKey });
|
|
101064
100961
|
const model = "gemini-2.5-flash";
|
|
101065
100962
|
const config3 = {
|
|
@@ -101071,9 +100968,9 @@ async function generateMatchingQuestion(clientInput, apiKey) {
|
|
|
101071
100968
|
};
|
|
101072
100969
|
const attemptResults = [];
|
|
101073
100970
|
let lastError = null;
|
|
101074
|
-
for (let attempt = 1; attempt <=
|
|
100971
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS3; attempt++) {
|
|
101075
100972
|
const startTime = Date.now();
|
|
101076
|
-
const promptText =
|
|
100973
|
+
const promptText = buildEnhancedPrompt3(clientInput, attempt);
|
|
101077
100974
|
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
101078
100975
|
try {
|
|
101079
100976
|
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
@@ -101084,18 +100981,28 @@ async function generateMatchingQuestion(clientInput, apiKey) {
|
|
|
101084
100981
|
parts.unshift(imagePart);
|
|
101085
100982
|
}
|
|
101086
100983
|
const contents = [{ role: "user", parts }];
|
|
101087
|
-
const aiResult = await ai.models.generateContent({
|
|
100984
|
+
const aiResult = await ai.models.generateContent({
|
|
100985
|
+
model,
|
|
100986
|
+
config: config3,
|
|
100987
|
+
contents
|
|
100988
|
+
});
|
|
101088
100989
|
const response = aiResult;
|
|
101089
100990
|
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
101090
100991
|
const duration = Date.now() - startTime;
|
|
101091
100992
|
DebugLogger.logResponse(attempt, rawText);
|
|
101092
|
-
if (!rawText)
|
|
100993
|
+
if (!rawText) {
|
|
100994
|
+
throw new Error("AI returned an empty response.");
|
|
100995
|
+
}
|
|
101093
100996
|
const parsedJson = JSON.parse(rawText);
|
|
101094
100997
|
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
101095
|
-
const aiGeneratedContent =
|
|
100998
|
+
const aiGeneratedContent = AIMRQOutputFieldsSchema.parse(parsedJson);
|
|
101096
100999
|
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
101097
|
-
if (aiGeneratedContent.
|
|
101098
|
-
throw new Error(`AI generated ${aiGeneratedContent.
|
|
101000
|
+
if (aiGeneratedContent.options.length !== clientInput.numberOfOptions) {
|
|
101001
|
+
throw new Error(`AI generated ${aiGeneratedContent.options.length} options, but ${clientInput.numberOfOptions} were required.`);
|
|
101002
|
+
}
|
|
101003
|
+
const correctCount = aiGeneratedContent.correctTempOptionIds.length;
|
|
101004
|
+
if (correctCount < clientInput.minCorrectAnswers || correctCount > clientInput.maxCorrectAnswers) {
|
|
101005
|
+
throw new Error(`AI provided ${correctCount} correct answers, which is outside the required range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
|
|
101099
101006
|
}
|
|
101100
101007
|
if (clientInput.quizContext?.originalCategory) {
|
|
101101
101008
|
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
@@ -101104,1015 +101011,954 @@ async function generateMatchingQuestion(clientInput, apiKey) {
|
|
|
101104
101011
|
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
101105
101012
|
}
|
|
101106
101013
|
}
|
|
101107
|
-
const finalPrompts = [];
|
|
101108
101014
|
const finalOptions = [];
|
|
101109
|
-
const
|
|
101110
|
-
|
|
101111
|
-
|
|
101112
|
-
const
|
|
101113
|
-
|
|
101114
|
-
|
|
101115
|
-
|
|
101015
|
+
const tempIdToFinalIdMap = {};
|
|
101016
|
+
const allTempIds = /* @__PURE__ */ new Set();
|
|
101017
|
+
aiGeneratedContent.options.forEach((aiOption) => {
|
|
101018
|
+
const finalId = generateUniqueId("opt_mr_");
|
|
101019
|
+
finalOptions.push({ id: finalId, text: aiOption.text });
|
|
101020
|
+
tempIdToFinalIdMap[aiOption.tempId] = finalId;
|
|
101021
|
+
allTempIds.add(aiOption.tempId);
|
|
101022
|
+
});
|
|
101023
|
+
const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
|
|
101024
|
+
if (!allTempIds.has(tempId)) {
|
|
101025
|
+
throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') which does not exist in the generated options.`);
|
|
101026
|
+
}
|
|
101027
|
+
return tempIdToFinalIdMap[tempId];
|
|
101116
101028
|
});
|
|
101117
101029
|
const completeQuestion = {
|
|
101118
|
-
id: generateUniqueId("
|
|
101119
|
-
questionType: "
|
|
101030
|
+
id: generateUniqueId("mrq_ai_"),
|
|
101031
|
+
questionType: "multiple_response",
|
|
101120
101032
|
prompt: aiGeneratedContent.prompt,
|
|
101121
|
-
prompts: finalPrompts,
|
|
101122
101033
|
options: finalOptions,
|
|
101123
|
-
|
|
101124
|
-
shuffleOptions: clientInput.shuffleOptions,
|
|
101034
|
+
correctAnswerIds: finalCorrectAnswerIds,
|
|
101125
101035
|
explanation: aiGeneratedContent.explanation,
|
|
101126
101036
|
points: aiGeneratedContent.points,
|
|
101127
101037
|
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
101128
|
-
difficulty: clientInput.difficulty,
|
|
101038
|
+
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
101129
101039
|
contextCode: clientInput.quizContext?.plannedContextId,
|
|
101130
101040
|
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
101131
|
-
learningObjective: clientInput.quizContext?.originalLoId,
|
|
101132
|
-
subject: clientInput.quizContext?.originalSubject,
|
|
101133
|
-
category: clientInput.quizContext?.originalCategory,
|
|
101134
|
-
imageUrl: clientInput.imageUrl
|
|
101135
|
-
};
|
|
101136
|
-
const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
|
|
101137
|
-
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
101138
|
-
console.log(`
|
|
101139
|
-
\u2705 Matching generation successful on attempt ${attempt} (${duration}ms)`);
|
|
101140
|
-
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
101141
|
-
return { question: validatedQuestion };
|
|
101142
|
-
} catch (error) {
|
|
101143
|
-
lastError = error;
|
|
101144
|
-
const duration = Date.now() - startTime;
|
|
101145
|
-
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
101146
|
-
const willRetry = attempt < MAX_RETRY_ATTEMPTS8;
|
|
101147
|
-
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
101148
|
-
if (willRetry) {
|
|
101149
|
-
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS8}ms...`);
|
|
101150
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS8));
|
|
101151
|
-
}
|
|
101152
|
-
}
|
|
101153
|
-
}
|
|
101154
|
-
DebugLogger.logAttemptSummary(attemptResults);
|
|
101155
|
-
const errorMessage = `Failed to generate Matching question after ${MAX_RETRY_ATTEMPTS8} attempts. Last error: ${lastError?.message}`;
|
|
101156
|
-
console.error("\n\u274C Final Result: FAILED");
|
|
101157
|
-
console.error(errorMessage);
|
|
101158
|
-
return { error: errorMessage };
|
|
101159
|
-
}
|
|
101160
|
-
|
|
101161
|
-
// src/react-ui/components/authoring/AIQuestionGeneratorModal.tsx
|
|
101162
|
-
var supportedQuestionTypesForAI = [
|
|
101163
|
-
{ value: "true_false", label: "True/False" },
|
|
101164
|
-
{ value: "multiple_choice", label: "Multiple Choice" },
|
|
101165
|
-
{ value: "multiple_response", label: "Multiple Response" },
|
|
101166
|
-
{ value: "short_answer", label: "Short Answer" },
|
|
101167
|
-
{ value: "numeric", label: "Numeric" },
|
|
101168
|
-
{ value: "fill_in_the_blanks", label: "Fill In The Blanks" },
|
|
101169
|
-
{ value: "sequence", label: "Sequence" },
|
|
101170
|
-
{ value: "matching", label: "Matching" }
|
|
101171
|
-
];
|
|
101172
|
-
var contextOptions = [
|
|
101173
|
-
{ shortContextId: "A", contextId: "THEO_ABS", contextDescription: "L\xFD thuy\u1EBFt/Tr\u1EEBu t\u01B0\u1EE3ng", contextExample: '"Ph\xE1t bi\u1EC3u \u0111\u1ECBnh lu\u1EADt III Newton." ho\u1EB7c "Gi\u1EA3i th\xEDch kh\xE1i ni\u1EC7m \u0111\u1EA1o h\xE0m."', keywords: "l\xFD thuy\u1EBFt, tr\u1EEBu t\u01B0\u1EE3ng, \u0111\u1ECBnh ngh\u0129a, kh\xE1i ni\u1EC7m, nguy\xEAn l\xFD, c\xF4ng th\u1EE9c, \u0111\u1ECBnh lu\u1EADt" },
|
|
101174
|
-
{ shortContextId: "B", contextId: "SPEC_CASE", contextDescription: "V\xED d\u1EE5 C\u1EE5 th\u1EC3/Tr\u01B0\u1EDDng h\u1EE3p Ri\xEAng", contextExample: '"T\xEDnh l\u1EF1c t\xE1c d\u1EE5ng l\xEAn v\u1EADt n\u1EB7ng 2kg r\u01A1i t\u1EF1 do." ho\u1EB7c "Cho h\xE0m s\u1ED1 y = x^2, t\xECm \u0111\u1EA1o h\xE0m t\u1EA1i x = 3."', keywords: "v\xED d\u1EE5, c\u1EE5 th\u1EC3, tr\u01B0\u1EDDng h\u1EE3p ri\xEAng, \xE1p d\u1EE5ng, t\xEDnh to\xE1n, minh h\u1ECDa, s\u1ED1 li\u1EC7u c\u1EE5 th\u1EC3" },
|
|
101175
|
-
{ shortContextId: "C", contextId: "NAT_OBS", contextDescription: "Hi\u1EC7n t\u01B0\u1EE3ng T\u1EF1 nhi\xEAn/Quan s\xE1t", contextExample: '"Gi\u1EA3i th\xEDch t\u1EA1i sao c\u1EA7u v\u1ED3ng xu\u1EA5t hi\u1EC7n sau c\u01A1n m\u01B0a." ho\u1EB7c "M\xF4 t\u1EA3 qu\xE1 tr\xECnh quang h\u1EE3p \u1EDF l\xE1 c\xE2y."', keywords: "hi\u1EC7n t\u01B0\u1EE3ng, t\u1EF1 nhi\xEAn, quan s\xE1t, gi\u1EA3i th\xEDch, m\xF4 t\u1EA3, th\u1EBF gi\u1EDBi th\u1EF1c, sinh h\u1ECDc, v\u1EADt l\xFD" },
|
|
101176
|
-
{ shortContextId: "D", contextId: "TECH_ENG", contextDescription: "\u1EE8ng d\u1EE5ng C\xF4ng ngh\u1EC7/K\u1EF9 thu\u1EADt", contextExample: '"Nguy\xEAn l\xFD ho\u1EA1t \u0111\u1ED9ng c\u1EE7a \u0111\u1ED9ng c\u01A1 \u0111\u1ED1t trong l\xE0 g\xEC?" ho\u1EB7c "Pin m\u1EB7t tr\u1EDDi chuy\u1EC3n \u0111\u1ED5i n\u0103ng l\u01B0\u1EE3ng \xE1nh s\xE1ng th\xE0nh \u0111i\u1EC7n n\u0103ng nh\u01B0 th\u1EBF n\xE0o?"', keywords: "\u1EE9ng d\u1EE5ng, c\xF4ng ngh\u1EC7, k\u1EF9 thu\u1EADt, thi\u1EBFt b\u1ECB, m\xE1y m\xF3c, ho\u1EA1t \u0111\u1ED9ng, nguy\xEAn l\xFD, ch\u1EBF t\u1EA1o" },
|
|
101177
|
-
{ shortContextId: "E", contextId: "EXP_INV", contextDescription: "Th\xED nghi\u1EC7m/\u0110i\u1EC1u tra Khoa h\u1ECDc", contextExample: '"Thi\u1EBFt k\u1EBF th\xED nghi\u1EC7m ch\u1EE9ng minh \u0111\u1ECBnh lu\u1EADt b\u1EA3o to\xE0n n\u0103ng l\u01B0\u1EE3ng." ho\u1EB7c "Ph\xE2n t\xEDch k\u1EBFt qu\u1EA3 th\xED nghi\u1EC7m v\u1EC1 t\u1ED1c \u0111\u1ED9 ph\u1EA3n \u1EE9ng."', keywords: "th\xED nghi\u1EC7m, \u0111i\u1EC1u tra, khoa h\u1ECDc, thi\u1EBFt k\u1EBF, quy tr\xECnh, k\u1EBFt qu\u1EA3, ph\xE2n t\xEDch, d\u1EEF li\u1EC7u" },
|
|
101178
|
-
{ shortContextId: "F", contextId: "REAL_PROB", contextDescription: "V\u1EA5n \u0111\u1EC1 Th\u1EF1c t\u1EBF/X\xE3 h\u1ED9i/M\xF4i tr\u01B0\u1EDDng", contextExample: '"\u0110\u1EC1 xu\u1EA5t gi\u1EA3i ph\xE1p gi\u1EA3m thi\u1EC3u \xF4 nhi\u1EC5m kh\xF4ng kh\xED t\u1EA1i \u0111\xF4 th\u1ECB." ho\u1EB7c "Ph\xE2n t\xEDch \u1EA3nh h\u01B0\u1EDFng c\u1EE7a bi\u1EBFn \u0111\u1ED5i kh\xED h\u1EADu \u0111\u1EBFn n\xF4ng nghi\u1EC7p."', keywords: "v\u1EA5n \u0111\u1EC1, th\u1EF1c t\u1EBF, x\xE3 h\u1ED9i, m\xF4i tr\u01B0\u1EDDng, gi\u1EA3i ph\xE1p, ph\xE2n t\xEDch, \u1EA3nh h\u01B0\u1EDFng, b\u1EC1n v\u1EEFng" },
|
|
101179
|
-
{ shortContextId: "G", contextId: "DATA_MOD", contextDescription: "Di\u1EC5n gi\u1EA3i D\u1EEF li\u1EC7u/M\xF4 h\xECnh h\xF3a", contextExample: '"D\u1EF1a v\xE0o bi\u1EC3u \u0111\u1ED3, nh\u1EADn x\xE9t xu h\u01B0\u1EDBng nhi\u1EC7t \u0111\u1ED9 to\xE0n c\u1EA7u." ho\u1EB7c "X\xE2y d\u1EF1ng m\xF4 h\xECnh to\xE1n h\u1ECDc m\xF4 t\u1EA3 s\u1EF1 t\u0103ng tr\u01B0\u1EDFng d\xE2n s\u1ED1."', keywords: "d\u1EEF li\u1EC7u, bi\u1EC3u \u0111\u1ED3, b\u1EA3ng s\u1ED1 li\u1EC7u, m\xF4 h\xECnh, di\u1EC5n gi\u1EA3i, ph\xE2n t\xEDch, xu h\u01B0\u1EDBng, d\u1EF1 \u0111o\xE1n" },
|
|
101180
|
-
{ shortContextId: "H", contextId: "HIST_SCI", contextDescription: "L\u1ECBch s\u1EED/Ph\xE1t tri\u1EC3n Khoa h\u1ECDc", contextExample: '"Tr\xECnh b\xE0y b\u1ED1i c\u1EA3nh ra \u0111\u1EDDi thuy\u1EBFt t\u01B0\u01A1ng \u0111\u1ED1i c\u1EE7a Einstein." ho\u1EB7c "Ai l\xE0 ng\u01B0\u1EDDi \u0111\u1EA7u ti\xEAn ph\xE1t hi\u1EC7n ra c\u1EA5u tr\xFAc DNA?"', keywords: "l\u1ECBch s\u1EED, ph\xE1t tri\u1EC3n, khoa h\u1ECDc, nh\xE0 khoa h\u1ECDc, ph\xE1t minh, kh\xE1m ph\xE1, b\u1ED1i c\u1EA3nh" },
|
|
101181
|
-
{ shortContextId: "I", contextId: "INTERDISC", contextDescription: "Li\xEAn ng\xE0nh (Interdisciplinary)", contextExample: '"S\u1EED d\u1EE5ng ki\u1EBFn th\u1EE9c To\xE1n v\xE0 V\u1EADt l\xFD \u0111\u1EC3 t\xEDnh qu\u1EF9 \u0111\u1EA1o c\u1EE7a v\u1EADt n\xE9m xi\xEAn." ho\u1EB7c "Ph\xE2n t\xEDch c\u01A1 ch\u1EBF h\xF3a sinh c\u1EE7a qu\xE1 tr\xECnh ti\xEAu h\xF3a."', keywords: "li\xEAn ng\xE0nh, t\xEDch h\u1EE3p, To\xE1n-L\xFD, Sinh-H\xF3a, Khoa h\u1ECDc-C\xF4ng ngh\u1EC7, k\u1EBFt n\u1ED1i ki\u1EBFn th\u1EE9c" },
|
|
101182
|
-
{ shortContextId: "J", contextId: "HYPO_COMP", contextDescription: "Gi\u1EA3 \u0111\u1ECBnh/So s\xE1nh T\xECnh hu\u1ED1ng", contextExample: '"N\u1EBFu Tr\xE1i \u0110\u1EA5t kh\xF4ng c\xF3 t\u1EEB tr\u01B0\u1EDDng th\xEC \u0111i\u1EC1u g\xEC s\u1EBD x\u1EA3y ra?" ho\u1EB7c "So s\xE1nh \u01B0u nh\u01B0\u1EE3c \u0111i\u1EC3m c\u1EE7a n\u0103ng l\u01B0\u1EE3ng h\u1EA1t nh\xE2n v\xE0 n\u0103ng l\u01B0\u1EE3ng m\u1EB7t tr\u1EDDi."', keywords: "gi\u1EA3 \u0111\u1ECBnh, so s\xE1nh, t\xECnh hu\u1ED1ng, n\u1EBFu...th\xEC, \u01B0u \u0111i\u1EC3m, nh\u01B0\u1EE3c \u0111i\u1EC3m, ph\xE2n t\xEDch, \u0111\xE1nh gi\xE1" },
|
|
101183
|
-
{ shortContextId: "__custom__", contextId: "__custom__", contextDescription: "Other (Custom Input)", contextExample: "", keywords: "custom, other, specific" }
|
|
101184
|
-
];
|
|
101185
|
-
var bloomLevelOptions = [
|
|
101186
|
-
{ value: "remembering", label: "Nh\u1EDB - D\u1EC5 (Remembering)" },
|
|
101187
|
-
{ value: "understanding", label: "Hi\u1EC3u - Trung B\xECnh (Understanding)" },
|
|
101188
|
-
{ value: "applying", label: "V\u1EADn D\u1EE5ng - Kh\xF3 (Applying)" }
|
|
101189
|
-
];
|
|
101190
|
-
var calculateCombinedDifficulty = (contextId, bloomLevel, qType, customContextInput) => {
|
|
101191
|
-
let contextScore = 1;
|
|
101192
|
-
const selectedContext = contextOptions.find((c4) => c4.contextId === contextId);
|
|
101193
|
-
if (selectedContext) {
|
|
101194
|
-
if (["THEO_ABS", "HIST_SCI", "__none__"].includes(selectedContext.contextId) || selectedContext.contextId === "__custom__" && !customContextInput.trim()) {
|
|
101195
|
-
contextScore = 1;
|
|
101196
|
-
} else if (["SPEC_CASE", "NAT_OBS", "DATA_MOD", "INTERDISC", "HYPO_COMP"].includes(selectedContext.contextId) || selectedContext.contextId === "__custom__" && customContextInput.trim()) {
|
|
101197
|
-
contextScore = 2;
|
|
101198
|
-
} else if (["TECH_ENG", "EXP_INV", "REAL_PROB"].includes(selectedContext.contextId)) {
|
|
101199
|
-
contextScore = 3;
|
|
101200
|
-
}
|
|
101201
|
-
} else if (contextId === "__custom__" && customContextInput.trim()) {
|
|
101202
|
-
contextScore = 2;
|
|
101203
|
-
}
|
|
101204
|
-
let bloomScore = 1;
|
|
101205
|
-
if (bloomLevel === "understanding") bloomScore = 2;
|
|
101206
|
-
else if (bloomLevel === "applying") bloomScore = 3;
|
|
101207
|
-
let questionTypeScore = 1;
|
|
101208
|
-
switch (qType) {
|
|
101209
|
-
case "true_false":
|
|
101210
|
-
case "multiple_choice":
|
|
101211
|
-
case "short_answer":
|
|
101212
|
-
questionTypeScore = 1;
|
|
101213
|
-
break;
|
|
101214
|
-
case "matching":
|
|
101215
|
-
case "fill_in_the_blanks":
|
|
101216
|
-
case "numeric":
|
|
101217
|
-
questionTypeScore = 2;
|
|
101218
|
-
break;
|
|
101219
|
-
case "sequence":
|
|
101220
|
-
case "multiple_response":
|
|
101221
|
-
questionTypeScore = 3;
|
|
101222
|
-
break;
|
|
101223
|
-
default:
|
|
101224
|
-
questionTypeScore = 1;
|
|
101225
|
-
}
|
|
101226
|
-
const totalScore = bloomScore + contextScore + questionTypeScore;
|
|
101227
|
-
if (totalScore <= 4) return "easy";
|
|
101228
|
-
if (totalScore <= 6) return "medium";
|
|
101229
|
-
return "hard";
|
|
101230
|
-
};
|
|
101231
|
-
var AIQuestionGeneratorModal = ({
|
|
101232
|
-
isOpen,
|
|
101233
|
-
onClose,
|
|
101234
|
-
onQuestionGenerated,
|
|
101235
|
-
language: language3
|
|
101236
|
-
// <-- NHẬN PROP
|
|
101237
|
-
}) => {
|
|
101238
|
-
const [topic, setTopic] = useState("");
|
|
101239
|
-
const [selectedContextId, setSelectedContextId] = useState("__none__");
|
|
101240
|
-
const [customContextInput, setCustomContextInput] = useState("");
|
|
101241
|
-
const [questionType, setQuestionType] = useState("multiple_choice");
|
|
101242
|
-
const [selectedBloomLevel, setSelectedBloomLevel] = useState("remembering");
|
|
101243
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
101244
|
-
const [error, setError] = useState(null);
|
|
101245
|
-
const [computedDifficultyForDisplay, setComputedDifficultyForDisplay] = useState(null);
|
|
101246
|
-
const { toast: toast2 } = useToast();
|
|
101247
|
-
const [numberOfOptions, setNumberOfOptions] = useState(4);
|
|
101248
|
-
const [minCorrectAnswers, setMinCorrectAnswers] = useState(1);
|
|
101249
|
-
const [maxCorrectAnswers, setMaxCorrectAnswers] = useState(2);
|
|
101250
|
-
const [isCaseSensitive, setIsCaseSensitive] = useState(false);
|
|
101251
|
-
const [numberOfBlanks, setNumberOfBlanks] = useState(2);
|
|
101252
|
-
const [numberOfSequenceItems, setNumberOfSequenceItems] = useState(4);
|
|
101253
|
-
const [numberOfMatchingPairs, setNumberOfMatchingPairs] = useState(4);
|
|
101254
|
-
const [isApiKeyManagerModalOpen, setIsApiKeyManagerModalOpen] = useState(false);
|
|
101255
|
-
const [geminiApiKeyExists, setGeminiApiKeyExists] = useState(false);
|
|
101256
|
-
useEffect(() => {
|
|
101257
|
-
if (isOpen) {
|
|
101258
|
-
setTopic("");
|
|
101259
|
-
setSelectedContextId("__none__");
|
|
101260
|
-
setCustomContextInput("");
|
|
101261
|
-
setQuestionType("multiple_choice");
|
|
101262
|
-
setSelectedBloomLevel("remembering");
|
|
101263
|
-
setError(null);
|
|
101264
|
-
setIsLoading(false);
|
|
101265
|
-
setNumberOfOptions(4);
|
|
101266
|
-
setMinCorrectAnswers(1);
|
|
101267
|
-
setMaxCorrectAnswers(2);
|
|
101268
|
-
setIsCaseSensitive(false);
|
|
101269
|
-
setNumberOfBlanks(2);
|
|
101270
|
-
setNumberOfSequenceItems(4);
|
|
101271
|
-
setNumberOfMatchingPairs(4);
|
|
101272
|
-
setGeminiApiKeyExists(APIKeyService.hasAPIKey(GEMINI_API_KEY_SERVICE_NAME));
|
|
101273
|
-
}
|
|
101274
|
-
}, [isOpen]);
|
|
101275
|
-
useEffect(() => {
|
|
101276
|
-
if (isOpen) {
|
|
101277
|
-
const difficulty = calculateCombinedDifficulty(selectedContextId, selectedBloomLevel, questionType, customContextInput);
|
|
101278
|
-
setComputedDifficultyForDisplay(difficulty);
|
|
101279
|
-
setGeminiApiKeyExists(APIKeyService.hasAPIKey(GEMINI_API_KEY_SERVICE_NAME));
|
|
101280
|
-
}
|
|
101281
|
-
}, [selectedContextId, selectedBloomLevel, questionType, customContextInput, isOpen]);
|
|
101282
|
-
const handleApiKeyModalClose = () => {
|
|
101283
|
-
setIsApiKeyManagerModalOpen(false);
|
|
101284
|
-
setGeminiApiKeyExists(APIKeyService.hasAPIKey(GEMINI_API_KEY_SERVICE_NAME));
|
|
101285
|
-
};
|
|
101286
|
-
const handleSubmit = async () => {
|
|
101287
|
-
if (!topic.trim()) {
|
|
101288
|
-
setError("Please provide a topic for the question.");
|
|
101289
|
-
return;
|
|
101290
|
-
}
|
|
101291
|
-
const geminiKey = APIKeyService.getAPIKey(GEMINI_API_KEY_SERVICE_NAME);
|
|
101292
|
-
if (!geminiKey) {
|
|
101293
|
-
setError("Gemini API Key is not set. Please set it via the settings icon in the header or the button below.");
|
|
101294
|
-
setGeminiApiKeyExists(false);
|
|
101295
|
-
return;
|
|
101296
|
-
}
|
|
101297
|
-
setGeminiApiKeyExists(true);
|
|
101298
|
-
setError(null);
|
|
101299
|
-
setIsLoading(true);
|
|
101300
|
-
let contextDescriptionForAI = void 0;
|
|
101301
|
-
if (selectedContextId === "__custom__") {
|
|
101302
|
-
contextDescriptionForAI = customContextInput.trim() || void 0;
|
|
101303
|
-
} else if (selectedContextId && selectedContextId !== "__none__") {
|
|
101304
|
-
const selectedContextObj = contextOptions.find((c4) => c4.contextId === selectedContextId);
|
|
101305
|
-
contextDescriptionForAI = selectedContextObj?.contextDescription;
|
|
101306
|
-
}
|
|
101307
|
-
const finalDifficultyForAI = calculateCombinedDifficulty(selectedContextId, selectedBloomLevel, questionType, customContextInput);
|
|
101308
|
-
try {
|
|
101309
|
-
let generatedQuestionData = null;
|
|
101310
|
-
const baseClientInput = {
|
|
101311
|
-
topic,
|
|
101312
|
-
language: language3,
|
|
101313
|
-
// <-- SỬ DỤNG PROP
|
|
101314
|
-
difficulty: finalDifficultyForAI,
|
|
101315
|
-
contextDescription: contextDescriptionForAI,
|
|
101316
|
-
selectedContextId: selectedContextId !== "__none__" && selectedContextId !== "__custom__" ? selectedContextId : selectedContextId === "__custom__" && customContextInput.trim() ? "__custom__" : void 0
|
|
101041
|
+
learningObjective: clientInput.quizContext?.originalLoId,
|
|
101042
|
+
subject: clientInput.quizContext?.originalSubject,
|
|
101043
|
+
category: clientInput.quizContext?.originalCategory,
|
|
101044
|
+
imageUrl: clientInput.imageUrl
|
|
101317
101045
|
};
|
|
101318
|
-
|
|
101319
|
-
|
|
101320
|
-
|
|
101321
|
-
|
|
101322
|
-
|
|
101323
|
-
|
|
101324
|
-
|
|
101325
|
-
|
|
101326
|
-
|
|
101327
|
-
|
|
101328
|
-
|
|
101329
|
-
|
|
101330
|
-
|
|
101331
|
-
|
|
101332
|
-
|
|
101333
|
-
break;
|
|
101334
|
-
case "numeric":
|
|
101335
|
-
generatedQuestionData = await generateNumericQuestion({ ...baseClientInput, allowDecimals: true, tolerance: 0 }, geminiKey);
|
|
101336
|
-
break;
|
|
101337
|
-
case "fill_in_the_blanks":
|
|
101338
|
-
generatedQuestionData = await generateFillInTheBlanksQuestion({ ...baseClientInput, numberOfBlanks, isCaseSensitive }, geminiKey);
|
|
101339
|
-
break;
|
|
101340
|
-
case "sequence":
|
|
101341
|
-
generatedQuestionData = await generateSequenceQuestion({ ...baseClientInput, numberOfItems: numberOfSequenceItems }, geminiKey);
|
|
101342
|
-
break;
|
|
101343
|
-
case "matching":
|
|
101344
|
-
generatedQuestionData = await generateMatchingQuestion({ ...baseClientInput, numberOfPairs: numberOfMatchingPairs, shuffleOptions: true }, geminiKey);
|
|
101345
|
-
break;
|
|
101346
|
-
default:
|
|
101347
|
-
const _exhaustiveCheck = questionType;
|
|
101348
|
-
throw new Error(`AI generation for question type "${_exhaustiveCheck}" is not implemented yet.`);
|
|
101349
|
-
}
|
|
101350
|
-
if (generatedQuestionData && generatedQuestionData.question) {
|
|
101351
|
-
const completeQuestion = generatedQuestionData.question;
|
|
101352
|
-
completeQuestion.difficulty = finalDifficultyForAI;
|
|
101353
|
-
completeQuestion.bloomLevel = selectedBloomLevel;
|
|
101354
|
-
completeQuestion.topic = completeQuestion.topic || topic;
|
|
101355
|
-
if (completeQuestion.points === void 0) completeQuestion.points = 10;
|
|
101356
|
-
onQuestionGenerated(completeQuestion);
|
|
101357
|
-
toast2({ title: "AI Question Generated", description: "Review and edit the generated question." });
|
|
101358
|
-
onClose();
|
|
101359
|
-
} else {
|
|
101360
|
-
throw new Error("AI did not return a valid question object.");
|
|
101361
|
-
}
|
|
101362
|
-
} catch (e3) {
|
|
101363
|
-
console.error("AI Question Generation Error:", e3);
|
|
101364
|
-
let errorMessage = e3.message || "An unknown error occurred during AI question generation.";
|
|
101365
|
-
if (typeof errorMessage === "string" && (errorMessage.includes("API key not valid") || errorMessage.startsWith("FAILED_PRECONDITION"))) {
|
|
101366
|
-
errorMessage = "The provided Google Gemini API Key is invalid or not configured correctly. Please check your API key in the settings.";
|
|
101367
|
-
setGeminiApiKeyExists(false);
|
|
101368
|
-
}
|
|
101369
|
-
setError(errorMessage);
|
|
101370
|
-
toast2({ title: "AI Generation Failed", description: errorMessage, variant: "destructive" });
|
|
101371
|
-
} finally {
|
|
101372
|
-
setIsLoading(false);
|
|
101373
|
-
}
|
|
101374
|
-
};
|
|
101375
|
-
const renderSpecificParams = () => {
|
|
101376
|
-
switch (questionType) {
|
|
101377
|
-
case "multiple_choice":
|
|
101378
|
-
case "multiple_response":
|
|
101379
|
-
return /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-num-options" }, "Number of Options (2-6 for MCQ, 2-8 for MRQ)"), /* @__PURE__ */ React169__default.createElement(
|
|
101380
|
-
Input,
|
|
101381
|
-
{
|
|
101382
|
-
id: "ai-num-options",
|
|
101383
|
-
type: "number",
|
|
101384
|
-
value: numberOfOptions,
|
|
101385
|
-
onChange: (e3) => setNumberOfOptions(parseInt(e3.target.value, 10)),
|
|
101386
|
-
min: 2,
|
|
101387
|
-
max: questionType === "multiple_choice" ? 6 : 8
|
|
101388
|
-
}
|
|
101389
|
-
), questionType === "multiple_response" && /* @__PURE__ */ React169__default.createElement(React169__default.Fragment, null, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-min-correct" }, "Min Correct Answers (MRQ)"), /* @__PURE__ */ React169__default.createElement(Input, { id: "ai-min-correct", type: "number", value: minCorrectAnswers, onChange: (e3) => setMinCorrectAnswers(parseInt(e3.target.value, 10)), min: 1, max: numberOfOptions }), /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-max-correct" }, "Max Correct Answers (MRQ)"), /* @__PURE__ */ React169__default.createElement(Input, { id: "ai-max-correct", type: "number", value: maxCorrectAnswers, onChange: (e3) => setMaxCorrectAnswers(parseInt(e3.target.value, 10)), min: minCorrectAnswers, max: numberOfOptions })));
|
|
101390
|
-
case "short_answer":
|
|
101391
|
-
case "fill_in_the_blanks":
|
|
101392
|
-
return /* @__PURE__ */ React169__default.createElement("div", { className: "flex items-center space-x-2" }, /* @__PURE__ */ React169__default.createElement("input", { type: "checkbox", id: "ai-case-sensitive", checked: isCaseSensitive, onChange: (e3) => setIsCaseSensitive(e3.target.checked), className: "h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" }), /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-case-sensitive" }, "Case Sensitive Answers"), questionType === "fill_in_the_blanks" && /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-1 ml-4" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-num-blanks" }, "Approx. Number of Blanks (1-5)"), /* @__PURE__ */ React169__default.createElement(Input, { id: "ai-num-blanks", type: "number", value: numberOfBlanks, onChange: (e3) => setNumberOfBlanks(parseInt(e3.target.value, 10)), min: 1, max: 5 })));
|
|
101393
|
-
case "sequence":
|
|
101394
|
-
return /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-num-seq-items" }, "Number of Items to Sequence (2-10)"), /* @__PURE__ */ React169__default.createElement(Input, { id: "ai-num-seq-items", type: "number", value: numberOfSequenceItems, onChange: (e3) => setNumberOfSequenceItems(parseInt(e3.target.value, 10)), min: 2, max: 10 }));
|
|
101395
|
-
case "matching":
|
|
101396
|
-
return /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-num-match-pairs" }, "Number of Pairs to Match (2-8)"), /* @__PURE__ */ React169__default.createElement(Input, { id: "ai-num-match-pairs", type: "number", value: numberOfMatchingPairs, onChange: (e3) => setNumberOfMatchingPairs(parseInt(e3.target.value, 10)), min: 2, max: 8 }));
|
|
101397
|
-
default:
|
|
101398
|
-
return null;
|
|
101399
|
-
}
|
|
101400
|
-
};
|
|
101401
|
-
return /* @__PURE__ */ React169__default.createElement(React169__default.Fragment, null, /* @__PURE__ */ React169__default.createElement(Dialog2, { open: isOpen, onOpenChange: (open) => {
|
|
101402
|
-
if (!open) onClose();
|
|
101403
|
-
} }, /* @__PURE__ */ React169__default.createElement(DialogContent2, { className: "sm:max-w-[550px]" }, /* @__PURE__ */ React169__default.createElement(DialogHeader, null, /* @__PURE__ */ React169__default.createElement(DialogTitle2, { className: "flex items-center font-headline text-2xl" }, /* @__PURE__ */ React169__default.createElement(WandSparkles, { className: "mr-2 h-6 w-6 text-primary" }), " AI Question Generator"), /* @__PURE__ */ React169__default.createElement(DialogDescription2, null, "Provide inputs and let AI generate a question for you.")), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-4 py-4 max-h-[60vh] overflow-y-auto pl-2 pr-2" }, !geminiApiKeyExists && /* @__PURE__ */ React169__default.createElement("div", { className: "p-3 mb-4 border border-amber-500 bg-amber-50 rounded-md text-amber-700" }, /* @__PURE__ */ React169__default.createElement("div", { className: "flex items-start" }, /* @__PURE__ */ React169__default.createElement(TriangleAlert, { className: "h-5 w-5 mr-2 mt-0.5 flex-shrink-0" }), /* @__PURE__ */ React169__default.createElement("div", null, /* @__PURE__ */ React169__default.createElement("p", { className: "font-semibold" }, "Gemini API Key Required"), /* @__PURE__ */ React169__default.createElement("p", { className: "text-xs" }, "To use AI question generation, please set your Google Gemini API Key."), /* @__PURE__ */ React169__default.createElement(
|
|
101404
|
-
Button,
|
|
101405
|
-
{
|
|
101406
|
-
variant: "link",
|
|
101407
|
-
className: "p-0 h-auto text-xs text-amber-700 hover:text-amber-800 mt-1",
|
|
101408
|
-
onClick: () => {
|
|
101409
|
-
onClose();
|
|
101410
|
-
setIsApiKeyManagerModalOpen(true);
|
|
101046
|
+
const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
|
|
101047
|
+
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
101048
|
+
console.log(`
|
|
101049
|
+
\u2705 MRQ generation successful on attempt ${attempt} (${duration}ms)`);
|
|
101050
|
+
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
101051
|
+
return { question: validatedQuestion };
|
|
101052
|
+
} catch (error) {
|
|
101053
|
+
lastError = error;
|
|
101054
|
+
const duration = Date.now() - startTime;
|
|
101055
|
+
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
101056
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS3;
|
|
101057
|
+
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
101058
|
+
if (willRetry) {
|
|
101059
|
+
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS3}ms...`);
|
|
101060
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS3));
|
|
101411
101061
|
}
|
|
101412
|
-
},
|
|
101413
|
-
"Set API Key Now"
|
|
101414
|
-
)))), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-topic" }, "Topic"), /* @__PURE__ */ React169__default.createElement(
|
|
101415
|
-
Textarea,
|
|
101416
|
-
{
|
|
101417
|
-
id: "ai-topic",
|
|
101418
|
-
value: topic,
|
|
101419
|
-
onChange: (e3) => setTopic(e3.target.value),
|
|
101420
|
-
placeholder: "e.g., Photosynthesis, World War II History, Basic Algebra Equations",
|
|
101421
|
-
className: "min-h-[80px]"
|
|
101422
|
-
}
|
|
101423
|
-
)), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-context-select" }, "Context (Optional)"), /* @__PURE__ */ React169__default.createElement(
|
|
101424
|
-
Select2,
|
|
101425
|
-
{
|
|
101426
|
-
value: selectedContextId,
|
|
101427
|
-
onValueChange: (value) => setSelectedContextId(value)
|
|
101428
|
-
},
|
|
101429
|
-
/* @__PURE__ */ React169__default.createElement(SelectTrigger2, { id: "ai-context-select" }, /* @__PURE__ */ React169__default.createElement(SelectValue2, { placeholder: "Select a context or choose 'Other'..." })),
|
|
101430
|
-
/* @__PURE__ */ React169__default.createElement(SelectContent2, null, /* @__PURE__ */ React169__default.createElement(SelectItem2, { value: "__none__" }, "None (General Topic)"), contextOptions.map((opt) => /* @__PURE__ */ React169__default.createElement(SelectItem2, { key: opt.contextId, value: opt.contextId }, opt.contextDescription, " ", opt.shortContextId !== "__custom__" ? `(${opt.shortContextId})` : "")))
|
|
101431
|
-
)), selectedContextId === "__custom__" && /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-custom-context" }, "Custom Context"), /* @__PURE__ */ React169__default.createElement(
|
|
101432
|
-
Textarea,
|
|
101433
|
-
{
|
|
101434
|
-
id: "ai-custom-context",
|
|
101435
|
-
value: customContextInput,
|
|
101436
|
-
onChange: (e3) => setCustomContextInput(e3.target.value),
|
|
101437
|
-
placeholder: "Enter your specific context here...",
|
|
101438
|
-
className: "min-h-[60px]"
|
|
101439
101062
|
}
|
|
101440
|
-
|
|
101441
|
-
|
|
101442
|
-
|
|
101443
|
-
|
|
101444
|
-
|
|
101445
|
-
|
|
101446
|
-
|
|
101447
|
-
/* @__PURE__ */ React169__default.createElement(SelectContent2, null, supportedQuestionTypesForAI.map((qType) => /* @__PURE__ */ React169__default.createElement(SelectItem2, { key: qType.value, value: qType.value }, qType.label)))
|
|
101448
|
-
)), /* @__PURE__ */ React169__default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React169__default.createElement(Label2, { htmlFor: "ai-bloom-level" }, "Bloom Level"), /* @__PURE__ */ React169__default.createElement(
|
|
101449
|
-
Select2,
|
|
101450
|
-
{
|
|
101451
|
-
value: selectedBloomLevel,
|
|
101452
|
-
onValueChange: (value) => setSelectedBloomLevel(value)
|
|
101453
|
-
},
|
|
101454
|
-
/* @__PURE__ */ React169__default.createElement(SelectTrigger2, { id: "ai-bloom-level" }, /* @__PURE__ */ React169__default.createElement(SelectValue2, { placeholder: "Select Bloom Level" })),
|
|
101455
|
-
/* @__PURE__ */ React169__default.createElement(SelectContent2, null, bloomLevelOptions.map((level) => /* @__PURE__ */ React169__default.createElement(SelectItem2, { key: level.value, value: level.value }, level.label)))
|
|
101456
|
-
))), renderSpecificParams(), computedDifficultyForDisplay && /* @__PURE__ */ React169__default.createElement("div", { className: "mt-3 p-3 border rounded-md bg-secondary/50" }, /* @__PURE__ */ React169__default.createElement("p", { className: "text-sm font-medium text-foreground" }, "Calculated AI Difficulty: ", /* @__PURE__ */ React169__default.createElement("span", { className: "font-bold text-primary capitalize" }, computedDifficultyForDisplay))), error && /* @__PURE__ */ React169__default.createElement("p", { className: "text-sm text-destructive flex items-center mt-2" }, /* @__PURE__ */ React169__default.createElement(TriangleAlert, { className: "mr-1 h-4 w-4" }), " ", error)), /* @__PURE__ */ React169__default.createElement(DialogFooter, null, /* @__PURE__ */ React169__default.createElement(DialogClose2, { asChild: true }, /* @__PURE__ */ React169__default.createElement(Button, { type: "button", variant: "outline" }, "Cancel")), /* @__PURE__ */ React169__default.createElement(Button, { type: "button", onClick: handleSubmit, disabled: isLoading || !topic.trim() || !geminiApiKeyExists }, isLoading ? /* @__PURE__ */ React169__default.createElement(LoaderCircle, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ React169__default.createElement(WandSparkles, { className: "mr-2 h-4 w-4" }), "Generate Question")))), /* @__PURE__ */ React169__default.createElement(APIKeyManagerModal, { isOpen: isApiKeyManagerModalOpen, onClose: handleApiKeyModalClose }));
|
|
101457
|
-
};
|
|
101458
|
-
|
|
101459
|
-
// src/react-ui/components/authoring/AIFullQuizGeneratorModal.tsx
|
|
101460
|
-
init_react_shim();
|
|
101063
|
+
}
|
|
101064
|
+
DebugLogger.logAttemptSummary(attemptResults);
|
|
101065
|
+
const errorMessage = `Failed to generate MRQ question after ${MAX_RETRY_ATTEMPTS3} attempts. Last error: ${lastError?.message}`;
|
|
101066
|
+
console.error("\n\u274C Final Result: FAILED");
|
|
101067
|
+
console.error(errorMessage);
|
|
101068
|
+
return { error: errorMessage };
|
|
101069
|
+
}
|
|
101461
101070
|
|
|
101462
|
-
// src/ai/flows/generate-
|
|
101071
|
+
// src/ai/flows/question-gen/generate-short-answer-question.ts
|
|
101463
101072
|
init_react_shim();
|
|
101464
101073
|
|
|
101465
|
-
// src/ai/flows/generate-
|
|
101074
|
+
// src/ai/flows/question-gen/generate-short-answer-question-types.ts
|
|
101466
101075
|
init_react_shim();
|
|
101467
|
-
|
|
101468
|
-
|
|
101469
|
-
ratio: z.number().min(0).max(100),
|
|
101470
|
-
originalLoId: z.string().optional(),
|
|
101471
|
-
originalSubject: z.string().optional(),
|
|
101472
|
-
originalCategory: z.string().optional(),
|
|
101473
|
-
originalTopic: z.string().optional(),
|
|
101474
|
-
commonMisconceptions: z.array(z.string()).optional()
|
|
101475
|
-
});
|
|
101476
|
-
var BloomLevelStringsEnum = z.enum(["remembering", "understanding", "applying", "analyzing", "evaluating", "creating"]);
|
|
101477
|
-
var fullQuizSupportedQuestionTypesArray = [
|
|
101478
|
-
"true_false",
|
|
101479
|
-
"multiple_choice",
|
|
101480
|
-
"multiple_response",
|
|
101481
|
-
"short_answer",
|
|
101482
|
-
"numeric",
|
|
101483
|
-
"fill_in_the_blanks",
|
|
101484
|
-
"sequence",
|
|
101485
|
-
"matching",
|
|
101486
|
-
"drag_and_drop",
|
|
101487
|
-
"coding"
|
|
101488
|
-
];
|
|
101489
|
-
z.object({
|
|
101490
|
-
language: z.string().optional().default("English"),
|
|
101491
|
-
totalQuestions: z.number().int().min(1).max(50),
|
|
101492
|
-
numCodingQuestions: z.number().optional().default(0),
|
|
101493
|
-
topics: z.array(TopicWithMetadataSchema).min(1),
|
|
101494
|
-
bloomLevels: z.array(z.object({
|
|
101495
|
-
level: BloomLevelStringsEnum,
|
|
101496
|
-
ratio: z.number().min(0).max(100)
|
|
101497
|
-
})).min(1),
|
|
101498
|
-
selectedContextIds: z.array(z.string()).optional(),
|
|
101499
|
-
selectedQuestionTypes: z.array(z.enum(fullQuizSupportedQuestionTypesArray)).min(1),
|
|
101500
|
-
imageContexts: z.array(z.custom()).optional().describe("Library of available image contexts for the AI to use.")
|
|
101501
|
-
});
|
|
101502
|
-
var PlannedQuestionSchema = z.object({
|
|
101503
|
-
plannedTopic: z.string().min(1).describe("The specific, assessable topic for this question."),
|
|
101504
|
-
plannedQuestionType: z.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
|
|
101505
|
-
plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned."),
|
|
101506
|
-
plannedContextId: z.string().optional().describe("The specific context ID chosen for this question."),
|
|
101507
|
-
imageId: z.string().nullable().optional().describe("The ID of the image from the context library to be used for this question."),
|
|
101508
|
-
targetMisconception: z.string().optional().describe("A specific common misconception this question should target."),
|
|
101509
|
-
difficultyReason: z.string().optional().describe("Strategic explanation of difficulty choice and placement."),
|
|
101510
|
-
topicSpecificity: z.enum(["broad", "focused", "specific"]).optional().describe("How specific the topic coverage should be."),
|
|
101511
|
-
originalLoId: z.string().optional(),
|
|
101512
|
-
originalSubject: z.string().optional(),
|
|
101513
|
-
originalCategory: z.string().optional(),
|
|
101514
|
-
originalTopic: z.string().optional()
|
|
101076
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
101077
|
+
isCaseSensitive: z.boolean().optional().default(false)
|
|
101515
101078
|
});
|
|
101516
|
-
var
|
|
101517
|
-
|
|
101518
|
-
|
|
101519
|
-
|
|
101520
|
-
|
|
101521
|
-
|
|
101522
|
-
|
|
101523
|
-
|
|
101524
|
-
|
|
101525
|
-
|
|
101526
|
-
}).optional()
|
|
101079
|
+
var AIShortAnswerOutputFieldsSchema = z.object({
|
|
101080
|
+
prompt: z.string().describe("The question text that prompts the user for a short answer."),
|
|
101081
|
+
acceptedAnswers: z.array(z.string().min(1)).min(1).describe("An array of one or more acceptable short answers. Include common variations if applicable."),
|
|
101082
|
+
// isCaseSensitive không cần thiết ở đây, chúng ta sẽ quản lý nó ở phía client
|
|
101083
|
+
explanation: z.string().optional(),
|
|
101084
|
+
points: z.number().optional().default(10),
|
|
101085
|
+
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
101086
|
+
topic: z.string().optional(),
|
|
101087
|
+
verifiedCategory: z.string().optional()
|
|
101088
|
+
// Thêm để xác thực
|
|
101527
101089
|
});
|
|
101528
101090
|
|
|
101529
|
-
// src/ai/flows/generate-
|
|
101530
|
-
var
|
|
101531
|
-
|
|
101532
|
-
|
|
101533
|
-
|
|
101534
|
-
|
|
101535
|
-
|
|
101536
|
-
|
|
101537
|
-
|
|
101538
|
-
|
|
101539
|
-
|
|
101540
|
-
|
|
101541
|
-
|
|
101542
|
-
|
|
101543
|
-
|
|
101544
|
-
|
|
101545
|
-
|
|
101091
|
+
// src/ai/flows/question-gen/generate-short-answer-question.ts
|
|
101092
|
+
var MAX_RETRY_ATTEMPTS4 = 3;
|
|
101093
|
+
var RETRY_DELAY_MS4 = 3e3;
|
|
101094
|
+
function buildEnhancedPrompt4(clientInput, attemptNumber) {
|
|
101095
|
+
const { quizContext, language: language3, difficulty, imageUrl } = clientInput;
|
|
101096
|
+
const category = quizContext?.originalCategory || "the specified technical category";
|
|
101097
|
+
const attemptInfo = attemptNumber > 1 ? `
|
|
101098
|
+
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
101099
|
+
Previous attempts failed. Ensure 'acceptedAnswers' is a non-empty array of strings and the JSON is valid.
|
|
101100
|
+
|
|
101101
|
+
` : "";
|
|
101102
|
+
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The question and its short answer must be directly related to the content of this image.` : "";
|
|
101103
|
+
const contextStrings = [
|
|
101104
|
+
`**Required Category:** ${category}`,
|
|
101105
|
+
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
101106
|
+
imageContextInstruction,
|
|
101107
|
+
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
101108
|
+
quizContext?.targetMisconception && `**Target Misconception:** The question should require an answer that corrects this specific misconception: "${quizContext.targetMisconception}"`
|
|
101109
|
+
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
101110
|
+
const exampleJson = JSON.stringify({
|
|
101111
|
+
prompt: "In Swift, what keyword is used to declare a constant?",
|
|
101112
|
+
acceptedAnswers: ["let"],
|
|
101113
|
+
explanation: "The 'let' keyword is used to declare constants, which are values that cannot be changed after they are set. 'var' is used for variables.",
|
|
101114
|
+
points: 10,
|
|
101115
|
+
difficulty: "easy",
|
|
101116
|
+
topic: "Swift Constants",
|
|
101117
|
+
verifiedCategory: category
|
|
101118
|
+
}, null, 2);
|
|
101119
|
+
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
101120
|
+
Your mission is to create a high-quality, technically accurate Short Answer Question.
|
|
101121
|
+
|
|
101122
|
+
## Core Rules (Non-negotiable)
|
|
101123
|
+
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
101124
|
+
2. **Objective Answer:** The question must have a short, factual, and objective answer. Avoid questions that are subjective or require long explanations.
|
|
101125
|
+
3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
|
|
101126
|
+
|
|
101127
|
+
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
101128
|
+
${contextStrings}
|
|
101129
|
+
|
|
101130
|
+
## Task: Generate the Question
|
|
101131
|
+
Based on all the rules and context above, generate a single Short Answer Question.
|
|
101132
|
+
|
|
101133
|
+
### Input Parameters
|
|
101134
|
+
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
101135
|
+
- **Language for Text:** ${language3}
|
|
101136
|
+
- **Difficulty Level:** ${difficulty}
|
|
101137
|
+
|
|
101138
|
+
### Required JSON Output Format
|
|
101139
|
+
Your response must be ONLY the JSON object, matching this exact structure:
|
|
101140
|
+
|
|
101141
|
+
${exampleJson}
|
|
101142
|
+
|
|
101143
|
+
Now, generate the JSON for the requested question.`;
|
|
101144
|
+
}
|
|
101145
|
+
async function generateShortAnswerQuestion(clientInput, apiKey) {
|
|
101146
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
101147
|
+
const model = "gemini-2.5-flash";
|
|
101148
|
+
const config3 = {
|
|
101149
|
+
temperature: 0.5,
|
|
101150
|
+
responseMimeType: "application/json",
|
|
101151
|
+
thinkingConfig: {
|
|
101152
|
+
thinkingBudget: 4e3
|
|
101153
|
+
}
|
|
101154
|
+
};
|
|
101155
|
+
const attemptResults = [];
|
|
101156
|
+
let lastError = null;
|
|
101157
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS4; attempt++) {
|
|
101158
|
+
const startTime = Date.now();
|
|
101159
|
+
const promptText = buildEnhancedPrompt4(clientInput, attempt);
|
|
101160
|
+
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
101161
|
+
try {
|
|
101162
|
+
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
101163
|
+
const parts = [{ text: promptText }];
|
|
101164
|
+
if (clientInput.imageUrl) {
|
|
101165
|
+
const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
101166
|
+
const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
|
|
101167
|
+
parts.unshift(imagePart);
|
|
101168
|
+
}
|
|
101169
|
+
const contents = [{ role: "user", parts }];
|
|
101170
|
+
const aiResult = await ai.models.generateContent({ model, config: config3, contents });
|
|
101171
|
+
const response = aiResult;
|
|
101172
|
+
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
101173
|
+
const duration = Date.now() - startTime;
|
|
101174
|
+
DebugLogger.logResponse(attempt, rawText);
|
|
101175
|
+
if (!rawText) throw new Error("AI returned an empty response.");
|
|
101176
|
+
const parsedJson = JSON.parse(rawText);
|
|
101177
|
+
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
101178
|
+
const aiGeneratedContent = AIShortAnswerOutputFieldsSchema.parse(parsedJson);
|
|
101179
|
+
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
101180
|
+
if (clientInput.quizContext?.originalCategory) {
|
|
101181
|
+
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
101182
|
+
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
101183
|
+
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
101184
|
+
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
101185
|
+
}
|
|
101186
|
+
}
|
|
101187
|
+
const completeQuestion = {
|
|
101188
|
+
id: generateUniqueId("saq_ai_"),
|
|
101189
|
+
questionType: "short_answer",
|
|
101190
|
+
prompt: aiGeneratedContent.prompt,
|
|
101191
|
+
acceptedAnswers: aiGeneratedContent.acceptedAnswers,
|
|
101192
|
+
isCaseSensitive: clientInput.isCaseSensitive,
|
|
101193
|
+
explanation: aiGeneratedContent.explanation,
|
|
101194
|
+
points: aiGeneratedContent.points,
|
|
101195
|
+
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
101196
|
+
difficulty: clientInput.difficulty,
|
|
101197
|
+
contextCode: clientInput.quizContext?.plannedContextId,
|
|
101198
|
+
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
101199
|
+
learningObjective: clientInput.quizContext?.originalLoId,
|
|
101200
|
+
subject: clientInput.quizContext?.originalSubject,
|
|
101201
|
+
category: clientInput.quizContext?.originalCategory,
|
|
101202
|
+
imageUrl: clientInput.imageUrl
|
|
101203
|
+
};
|
|
101204
|
+
const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
|
|
101205
|
+
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
101206
|
+
console.log(`
|
|
101207
|
+
\u2705 Short Answer generation successful on attempt ${attempt} (${duration}ms)`);
|
|
101208
|
+
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
101209
|
+
return { question: validatedQuestion };
|
|
101210
|
+
} catch (error) {
|
|
101211
|
+
lastError = error;
|
|
101212
|
+
const duration = Date.now() - startTime;
|
|
101213
|
+
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
101214
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS4;
|
|
101215
|
+
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
101216
|
+
if (willRetry) {
|
|
101217
|
+
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS4}ms...`);
|
|
101218
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS4));
|
|
101219
|
+
}
|
|
101546
101220
|
}
|
|
101547
101221
|
}
|
|
101548
|
-
|
|
101549
|
-
|
|
101550
|
-
|
|
101551
|
-
|
|
101552
|
-
|
|
101553
|
-
|
|
101554
|
-
};
|
|
101555
|
-
function generateQuestionTypeSelectionGuidance() {
|
|
101556
|
-
return `
|
|
101557
|
-
QUESTION TYPE SELECTION BEST PRACTICES:
|
|
101222
|
+
DebugLogger.logAttemptSummary(attemptResults);
|
|
101223
|
+
const errorMessage = `Failed to generate Short Answer question after ${MAX_RETRY_ATTEMPTS4} attempts. Last error: ${lastError?.message}`;
|
|
101224
|
+
console.error("\n\u274C Final Result: FAILED");
|
|
101225
|
+
console.error(errorMessage);
|
|
101226
|
+
return { error: errorMessage };
|
|
101227
|
+
}
|
|
101558
101228
|
|
|
101559
|
-
|
|
101560
|
-
|
|
101561
|
-
- Bloom levels: Primarily Remembering, Understanding
|
|
101562
|
-
- Use when: Testing definitive statements, clarifying misconceptions
|
|
101563
|
-
- Example: "Photosynthesis only occurs during daytime" (targets timing misconception)
|
|
101229
|
+
// src/ai/flows/question-gen/generate-numeric-question.ts
|
|
101230
|
+
init_react_shim();
|
|
101564
101231
|
|
|
101565
|
-
|
|
101566
|
-
|
|
101567
|
-
|
|
101568
|
-
|
|
101569
|
-
|
|
101232
|
+
// src/ai/flows/question-gen/generate-numeric-question-types.ts
|
|
101233
|
+
init_react_shim();
|
|
101234
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
101235
|
+
allowDecimals: z.boolean().optional().default(true),
|
|
101236
|
+
minRange: z.number().optional(),
|
|
101237
|
+
maxRange: z.number().optional(),
|
|
101238
|
+
// Thêm trường tolerance để client có thể tùy chỉnh
|
|
101239
|
+
tolerance: z.number().min(0).optional().default(0)
|
|
101240
|
+
});
|
|
101241
|
+
var AINumericOutputFieldsSchema = z.object({
|
|
101242
|
+
prompt: z.string(),
|
|
101243
|
+
answer: z.number(),
|
|
101244
|
+
// AI có thể đề xuất một tolerance, nhưng chúng ta sẽ ưu tiên của client
|
|
101245
|
+
tolerance: z.number().min(0).optional(),
|
|
101246
|
+
explanation: z.string().optional(),
|
|
101247
|
+
points: z.number().optional().default(10),
|
|
101248
|
+
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
101249
|
+
topic: z.string().optional(),
|
|
101250
|
+
verifiedCategory: z.string().optional()
|
|
101251
|
+
// Thêm để xác thực
|
|
101252
|
+
});
|
|
101570
101253
|
|
|
101571
|
-
|
|
101572
|
-
|
|
101573
|
-
|
|
101574
|
-
|
|
101575
|
-
|
|
101254
|
+
// src/ai/flows/question-gen/generate-numeric-question.ts
|
|
101255
|
+
var MAX_RETRY_ATTEMPTS5 = 3;
|
|
101256
|
+
var RETRY_DELAY_MS5 = 3e3;
|
|
101257
|
+
function buildEnhancedPrompt5(clientInput, attemptNumber) {
|
|
101258
|
+
const { quizContext, language: language3, difficulty, minRange, maxRange, allowDecimals, imageUrl } = clientInput;
|
|
101259
|
+
const category = quizContext?.originalCategory || "the specified technical category";
|
|
101260
|
+
const attemptInfo = attemptNumber > 1 ? `
|
|
101261
|
+
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
101262
|
+
Previous attempts failed. Ensure the 'answer' is a valid number and fits within the specified constraints.
|
|
101576
101263
|
|
|
101577
|
-
|
|
101578
|
-
|
|
101579
|
-
|
|
101580
|
-
|
|
101581
|
-
|
|
101264
|
+
` : "";
|
|
101265
|
+
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The question and its numerical answer must be directly related to the content of this image.` : "";
|
|
101266
|
+
const contextStrings = [
|
|
101267
|
+
`**Required Category:** ${category}`,
|
|
101268
|
+
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
101269
|
+
imageContextInstruction,
|
|
101270
|
+
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
101271
|
+
quizContext?.targetMisconception && `**Target Misconception:** The question should clarify this numerical error: "${quizContext.targetMisconception}"`
|
|
101272
|
+
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
101273
|
+
const constraintStrings = [
|
|
101274
|
+
minRange !== void 0 && `The final 'answer' MUST be greater than or equal to ${minRange}.`,
|
|
101275
|
+
maxRange !== void 0 && `The final 'answer' MUST be less than or equal to ${maxRange}.`,
|
|
101276
|
+
!allowDecimals && `The final 'answer' MUST be an integer (whole number).`
|
|
101277
|
+
].filter(Boolean).join("\n");
|
|
101278
|
+
const exampleJson = JSON.stringify({
|
|
101279
|
+
prompt: "A Swift `Int` on a 64-bit platform can store a maximum value of 2^63 - 1. What is the maximum value for an `Int8`?",
|
|
101280
|
+
answer: 127,
|
|
101281
|
+
tolerance: 0,
|
|
101282
|
+
explanation: "An Int8 uses 8 bits. One bit is for the sign, leaving 7 bits for the value. The range is from -128 to 127 (2^7 - 1).",
|
|
101283
|
+
points: 10,
|
|
101284
|
+
difficulty: "medium",
|
|
101285
|
+
topic: "Swift Data Types",
|
|
101286
|
+
verifiedCategory: category
|
|
101287
|
+
}, null, 2);
|
|
101288
|
+
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
101289
|
+
Your mission is to create a high-quality, technically accurate Numeric Question.
|
|
101582
101290
|
|
|
101583
|
-
|
|
101584
|
-
|
|
101585
|
-
|
|
101586
|
-
|
|
101587
|
-
- Example: "Calculate the molarity of a 2L solution containing 0.5 moles of NaCl"
|
|
101291
|
+
## Core Rules (Non-negotiable)
|
|
101292
|
+
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
101293
|
+
2. **Quantitative Answer:** The question MUST ask for a specific, objective numerical answer.
|
|
101294
|
+
3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
|
|
101588
101295
|
|
|
101589
|
-
|
|
101590
|
-
|
|
101591
|
-
- Bloom levels: Remembering, Understanding
|
|
101592
|
-
- Use when: Testing relationships between terms and definitions
|
|
101593
|
-
- Example: Match organelles with their functions
|
|
101296
|
+
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
101297
|
+
${contextStrings}
|
|
101594
101298
|
|
|
101595
|
-
|
|
101596
|
-
|
|
101597
|
-
- Bloom levels: Understanding, Applying, Analyzing
|
|
101598
|
-
- Use when: Order or sequence is critical to understanding
|
|
101599
|
-
- Example: "Arrange the steps of mitosis in correct order"
|
|
101299
|
+
## Task: Generate the Question
|
|
101300
|
+
Based on all the rules and context above, generate a single Numeric Question.
|
|
101600
101301
|
|
|
101601
|
-
|
|
101602
|
-
-
|
|
101603
|
-
-
|
|
101604
|
-
-
|
|
101605
|
-
|
|
101302
|
+
### Input Parameters & Constraints
|
|
101303
|
+
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
101304
|
+
- **Language for Text:** ${language3}
|
|
101305
|
+
- **Difficulty Level:** ${difficulty}
|
|
101306
|
+
${constraintStrings ? `
|
|
101307
|
+
### CRITICAL CONSTRAINTS ON THE ANSWER
|
|
101308
|
+
${constraintStrings}` : ""}
|
|
101606
101309
|
|
|
101607
|
-
|
|
101608
|
-
|
|
101609
|
-
- Bloom levels: Understanding, Applying, Analyzing, Evaluating, Creating
|
|
101610
|
-
- Use when: Requiring explanatory responses, open-ended thinking
|
|
101611
|
-
- Example: "Explain why enzymes are specific to certain substrates"
|
|
101310
|
+
### Required JSON Output Format
|
|
101311
|
+
Your response must be ONLY the JSON object, matching this exact structure:
|
|
101612
101312
|
|
|
101613
|
-
|
|
101614
|
-
|
|
101615
|
-
|
|
101616
|
-
|
|
101617
|
-
|
|
101618
|
-
|
|
101313
|
+
${exampleJson}
|
|
101314
|
+
|
|
101315
|
+
Now, generate the JSON for the requested question.`;
|
|
101316
|
+
}
|
|
101317
|
+
async function generateNumericQuestion(clientInput, apiKey) {
|
|
101318
|
+
if (clientInput.minRange !== void 0 && clientInput.maxRange !== void 0 && clientInput.minRange > clientInput.maxRange) {
|
|
101319
|
+
return { error: `Invalid input: minRange (${clientInput.minRange}) cannot be greater than maxRange (${clientInput.maxRange}).` };
|
|
101320
|
+
}
|
|
101321
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
101322
|
+
const model = "gemini-2.5-flash";
|
|
101323
|
+
const config3 = {
|
|
101324
|
+
temperature: 0.4,
|
|
101325
|
+
// Giữ nhiệt độ thấp cho các câu trả lời chính xác
|
|
101326
|
+
responseMimeType: "application/json",
|
|
101327
|
+
thinkingConfig: {
|
|
101328
|
+
thinkingBudget: 4e3
|
|
101329
|
+
}
|
|
101330
|
+
};
|
|
101331
|
+
const attemptResults = [];
|
|
101332
|
+
let lastError = null;
|
|
101333
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS5; attempt++) {
|
|
101334
|
+
const startTime = Date.now();
|
|
101335
|
+
const promptText = buildEnhancedPrompt5(clientInput, attempt);
|
|
101336
|
+
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
101337
|
+
try {
|
|
101338
|
+
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
101339
|
+
const parts = [{ text: promptText }];
|
|
101340
|
+
if (clientInput.imageUrl) {
|
|
101341
|
+
const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
101342
|
+
const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
|
|
101343
|
+
parts.unshift(imagePart);
|
|
101344
|
+
}
|
|
101345
|
+
const contents = [{ role: "user", parts }];
|
|
101346
|
+
const aiResult = await ai.models.generateContent({ model, config: config3, contents });
|
|
101347
|
+
const response = aiResult;
|
|
101348
|
+
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
101349
|
+
const duration = Date.now() - startTime;
|
|
101350
|
+
DebugLogger.logResponse(attempt, rawText);
|
|
101351
|
+
if (!rawText) throw new Error("AI returned an empty response.");
|
|
101352
|
+
const parsedJson = JSON.parse(rawText);
|
|
101353
|
+
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
101354
|
+
const aiGeneratedContent = AINumericOutputFieldsSchema.parse(parsedJson);
|
|
101355
|
+
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
101356
|
+
const answer = aiGeneratedContent.answer;
|
|
101357
|
+
if (clientInput.minRange !== void 0 && answer < clientInput.minRange) {
|
|
101358
|
+
throw new Error(`AI answer ${answer} is less than the required minRange of ${clientInput.minRange}.`);
|
|
101359
|
+
}
|
|
101360
|
+
if (clientInput.maxRange !== void 0 && answer > clientInput.maxRange) {
|
|
101361
|
+
throw new Error(`AI answer ${answer} is greater than the required maxRange of ${clientInput.maxRange}.`);
|
|
101362
|
+
}
|
|
101363
|
+
if (!clientInput.allowDecimals && !Number.isInteger(answer)) {
|
|
101364
|
+
throw new Error(`AI answer ${answer} is not an integer, but decimals are not allowed.`);
|
|
101365
|
+
}
|
|
101366
|
+
if (clientInput.quizContext?.originalCategory) {
|
|
101367
|
+
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
101368
|
+
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
101369
|
+
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
101370
|
+
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
101371
|
+
}
|
|
101372
|
+
}
|
|
101373
|
+
const completeQuestion = {
|
|
101374
|
+
id: generateUniqueId("num_ai_"),
|
|
101375
|
+
questionType: "numeric",
|
|
101376
|
+
prompt: aiGeneratedContent.prompt,
|
|
101377
|
+
answer: aiGeneratedContent.answer,
|
|
101378
|
+
tolerance: clientInput.tolerance ?? aiGeneratedContent.tolerance ?? 0,
|
|
101379
|
+
explanation: aiGeneratedContent.explanation,
|
|
101380
|
+
points: aiGeneratedContent.points,
|
|
101381
|
+
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
101382
|
+
difficulty: clientInput.difficulty,
|
|
101383
|
+
contextCode: clientInput.quizContext?.plannedContextId,
|
|
101384
|
+
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
101385
|
+
learningObjective: clientInput.quizContext?.originalLoId,
|
|
101386
|
+
subject: clientInput.quizContext?.originalSubject,
|
|
101387
|
+
category: clientInput.quizContext?.originalCategory,
|
|
101388
|
+
imageUrl: clientInput.imageUrl
|
|
101389
|
+
};
|
|
101390
|
+
const validatedQuestion = NumericQuestionZodSchema.parse(completeQuestion);
|
|
101391
|
+
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
101392
|
+
console.log(`
|
|
101393
|
+
\u2705 Numeric generation successful on attempt ${attempt} (${duration}ms)`);
|
|
101394
|
+
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
101395
|
+
return { question: validatedQuestion };
|
|
101396
|
+
} catch (error) {
|
|
101397
|
+
lastError = error;
|
|
101398
|
+
const duration = Date.now() - startTime;
|
|
101399
|
+
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
101400
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS5;
|
|
101401
|
+
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
101402
|
+
if (willRetry) {
|
|
101403
|
+
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS5}ms...`);
|
|
101404
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS5));
|
|
101405
|
+
}
|
|
101406
|
+
}
|
|
101407
|
+
}
|
|
101408
|
+
DebugLogger.logAttemptSummary(attemptResults);
|
|
101409
|
+
const errorMessage = `Failed to generate Numeric question after ${MAX_RETRY_ATTEMPTS5} attempts. Last error: ${lastError?.message}`;
|
|
101410
|
+
console.error("\n\u274C Final Result: FAILED");
|
|
101411
|
+
console.error(errorMessage);
|
|
101412
|
+
return { error: errorMessage };
|
|
101619
101413
|
}
|
|
101620
|
-
function generateAdvancedBloomGuidance() {
|
|
101621
|
-
return `
|
|
101622
|
-
ADVANCED BLOOM'S TAXONOMY & QUESTION TYPE OPTIMIZATION:
|
|
101623
101414
|
|
|
101624
|
-
|
|
101625
|
-
|
|
101626
|
-
- Secondary types: multiple_choice (simple recall)
|
|
101627
|
-
- Focus: Facts, terms, basic concepts, definitions
|
|
101628
|
-
- Misconception addressing: Use true_false to clarify common confusions
|
|
101415
|
+
// src/ai/flows/question-gen/generate-fitb-question.ts
|
|
101416
|
+
init_react_shim();
|
|
101629
101417
|
|
|
101630
|
-
|
|
101631
|
-
|
|
101632
|
-
|
|
101633
|
-
|
|
101634
|
-
|
|
101418
|
+
// src/ai/flows/question-gen/generate-fitb-question-types.ts
|
|
101419
|
+
init_react_shim();
|
|
101420
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
101421
|
+
numberOfBlanks: z.number().int().min(1).max(5).optional().default(1),
|
|
101422
|
+
isCaseSensitive: z.boolean().optional().default(false)
|
|
101423
|
+
});
|
|
101424
|
+
var AIFillInTheBlanksOutputFieldsSchema = z.object({
|
|
101425
|
+
prompt: z.string().describe("The instructional text for the user, e.g., 'Fill in the blanks to complete the sentence.'"),
|
|
101426
|
+
// Yêu cầu AI trả về cấu trúc segments trực tiếp
|
|
101427
|
+
segments: z.array(z.object({
|
|
101428
|
+
type: z.enum(["text", "blank"]),
|
|
101429
|
+
content: z.string().optional().describe("The text content for a 'text' segment."),
|
|
101430
|
+
acceptedAnswers: z.array(z.string().min(1)).min(1).optional().describe("An array of correct answers for a 'blank' segment.")
|
|
101431
|
+
})).min(1).describe("An array of text and blank segments representing the question."),
|
|
101432
|
+
explanation: z.string().optional(),
|
|
101433
|
+
points: z.number().optional().default(10),
|
|
101434
|
+
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
101435
|
+
topic: z.string().optional(),
|
|
101436
|
+
verifiedCategory: z.string().optional()
|
|
101437
|
+
// Thêm để xác thực
|
|
101438
|
+
});
|
|
101635
101439
|
|
|
101636
|
-
|
|
101637
|
-
|
|
101638
|
-
|
|
101639
|
-
|
|
101640
|
-
|
|
101440
|
+
// src/ai/flows/question-gen/generate-fitb-question.ts
|
|
101441
|
+
var MAX_RETRY_ATTEMPTS6 = 3;
|
|
101442
|
+
var RETRY_DELAY_MS6 = 3e3;
|
|
101443
|
+
function buildEnhancedPrompt6(clientInput, attemptNumber) {
|
|
101444
|
+
const { quizContext, language: language3, difficulty, numberOfBlanks, imageUrl } = clientInput;
|
|
101445
|
+
const category = quizContext?.originalCategory || "the specified technical category";
|
|
101446
|
+
const attemptInfo = attemptNumber > 1 ? `
|
|
101447
|
+
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
101448
|
+
Previous attempts failed. Pay strict attention to the JSON schema, especially the 'segments' array structure. Ensure 'blank' segments have 'acceptedAnswers' and 'text' segments have 'content'.
|
|
101641
101449
|
|
|
101642
|
-
|
|
101643
|
-
|
|
101644
|
-
|
|
101645
|
-
|
|
101646
|
-
|
|
101450
|
+
` : "";
|
|
101451
|
+
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The question and blanks must be directly related to the content of this image.` : "";
|
|
101452
|
+
const contextStrings = [
|
|
101453
|
+
`**Required Category:** ${category}`,
|
|
101454
|
+
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
101455
|
+
imageContextInstruction,
|
|
101456
|
+
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
101457
|
+
quizContext?.targetMisconception && `**Target Misconception:** Design the blank to test this specific point: "${quizContext.targetMisconception}"`
|
|
101458
|
+
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
101459
|
+
const exampleJson = JSON.stringify({
|
|
101460
|
+
prompt: "Complete the following Swift code snippet.",
|
|
101461
|
+
segments: [
|
|
101462
|
+
{ "type": "text", "content": "To declare a new function in Swift, you use the `" },
|
|
101463
|
+
{ "type": "blank", "acceptedAnswers": ["func"] },
|
|
101464
|
+
{ "type": "text", "content": "` keyword." }
|
|
101465
|
+
],
|
|
101466
|
+
explanation: "The 'func' keyword is used to declare a function in the Swift programming language.",
|
|
101467
|
+
points: 10,
|
|
101468
|
+
difficulty: "easy",
|
|
101469
|
+
topic: "Swift Function Declaration",
|
|
101470
|
+
verifiedCategory: category
|
|
101471
|
+
}, null, 2);
|
|
101472
|
+
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
101473
|
+
Your mission is to create a high-quality, technically accurate Fill-in-the-Blanks Question.
|
|
101647
101474
|
|
|
101648
|
-
|
|
101649
|
-
|
|
101650
|
-
|
|
101651
|
-
|
|
101652
|
-
- Best for: "Which is better and why...", "Evaluate the approach...", "Critique..."
|
|
101475
|
+
## Core Rules (Non-negotiable)
|
|
101476
|
+
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
101477
|
+
2. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
|
|
101478
|
+
3. **Logical Segments:** For 'blank' segments, you MUST provide 'acceptedAnswers'. For 'text' segments, you MUST provide 'content'. Do not mix them.
|
|
101653
101479
|
|
|
101654
|
-
|
|
101655
|
-
|
|
101656
|
-
- Secondary types: drag_and_drop (design tasks)
|
|
101657
|
-
- Focus: Designing, planning, producing, constructing
|
|
101658
|
-
- Best for: "Design a solution...", "Create a plan...", "Develop a strategy..."
|
|
101659
|
-
`;
|
|
101660
|
-
}
|
|
101661
|
-
function generateDiversityRules() {
|
|
101662
|
-
return `
|
|
101663
|
-
ENHANCED DIVERSITY & QUALITY ASSURANCE RULES:
|
|
101480
|
+
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
101481
|
+
${contextStrings}
|
|
101664
101482
|
|
|
101665
|
-
|
|
101666
|
-
|
|
101667
|
-
2. Distribute question types based on their cognitive complexity
|
|
101668
|
-
3. For quizzes with 10+ questions, use at least 4 different question types
|
|
101669
|
-
4. For quizzes with 20+ questions, use at least 6 different question types
|
|
101670
|
-
5. Balance quick-answer types (true_false, multiple_choice) with deeper types (short_answer, coding)
|
|
101483
|
+
## Task: Generate the Question
|
|
101484
|
+
Based on all the rules and context above, generate a single Fill-in-the-Blanks Question.
|
|
101671
101485
|
|
|
101672
|
-
|
|
101673
|
-
|
|
101674
|
-
|
|
101675
|
-
|
|
101676
|
-
|
|
101486
|
+
### Input Parameters
|
|
101487
|
+
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
101488
|
+
- **Language for Text:** ${language3}
|
|
101489
|
+
- **Difficulty Level:** ${difficulty}
|
|
101490
|
+
- **Number of Blanks:** Generate exactly ${numberOfBlanks} segment(s) with type 'blank'.
|
|
101677
101491
|
|
|
101678
|
-
|
|
101679
|
-
|
|
101680
|
-
- Use true_false for binary misconceptions
|
|
101681
|
-
- Use multiple_choice for concept selection misconceptions
|
|
101682
|
-
- Use short_answer for complex misconceptions requiring explanation
|
|
101492
|
+
### Required JSON Output Format
|
|
101493
|
+
Your response must be ONLY the JSON object, matching this exact structure:
|
|
101683
101494
|
|
|
101684
|
-
|
|
101685
|
-
|
|
101686
|
-
|
|
101687
|
-
- Conceptual topics: Favor multiple_choice, multiple_response, true_false
|
|
101688
|
-
- Factual topics: Favor fill_in_the_blanks, matching, true_false
|
|
101689
|
-
`;
|
|
101495
|
+
${exampleJson}
|
|
101496
|
+
|
|
101497
|
+
Now, generate the JSON for the requested question.`;
|
|
101690
101498
|
}
|
|
101691
|
-
async function
|
|
101692
|
-
const
|
|
101693
|
-
|
|
101694
|
-
|
|
101695
|
-
|
|
101696
|
-
|
|
101697
|
-
|
|
101698
|
-
|
|
101699
|
-
});
|
|
101700
|
-
const totalTopicRatio = clientInput.topics.reduce((sum, t4) => sum + t4.ratio, 0);
|
|
101701
|
-
if (Math.abs(totalTopicRatio - 100) > 1) {
|
|
101702
|
-
throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
|
|
101703
|
-
}
|
|
101704
|
-
const totalBloomRatio = clientInput.bloomLevels.reduce((sum, b2) => sum + b2.ratio, 0);
|
|
101705
|
-
if (Math.abs(totalBloomRatio - 100) > 1) {
|
|
101706
|
-
throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
|
|
101499
|
+
async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
|
|
101500
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
101501
|
+
const model = "gemini-2.5-flash";
|
|
101502
|
+
const config3 = {
|
|
101503
|
+
temperature: 0.8,
|
|
101504
|
+
responseMimeType: "application/json",
|
|
101505
|
+
thinkingConfig: {
|
|
101506
|
+
thinkingBudget: 4e3
|
|
101707
101507
|
}
|
|
101708
|
-
|
|
101709
|
-
|
|
101710
|
-
|
|
101711
|
-
|
|
101712
|
-
const
|
|
101713
|
-
const
|
|
101714
|
-
|
|
101715
|
-
|
|
101716
|
-
|
|
101717
|
-
|
|
101718
|
-
|
|
101719
|
-
|
|
101720
|
-
|
|
101721
|
-
|
|
101722
|
-
|
|
101723
|
-
|
|
101724
|
-
|
|
101725
|
-
|
|
101726
|
-
|
|
101727
|
-
|
|
101728
|
-
|
|
101729
|
-
if (
|
|
101730
|
-
|
|
101731
|
-
|
|
101508
|
+
};
|
|
101509
|
+
const attemptResults = [];
|
|
101510
|
+
let lastError = null;
|
|
101511
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS6; attempt++) {
|
|
101512
|
+
const startTime = Date.now();
|
|
101513
|
+
const promptText = buildEnhancedPrompt6(clientInput, attempt);
|
|
101514
|
+
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
101515
|
+
try {
|
|
101516
|
+
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
101517
|
+
const parts = [{ text: promptText }];
|
|
101518
|
+
if (clientInput.imageUrl) {
|
|
101519
|
+
const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
101520
|
+
const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
|
|
101521
|
+
parts.unshift(imagePart);
|
|
101522
|
+
}
|
|
101523
|
+
const contents = [{ role: "user", parts }];
|
|
101524
|
+
const aiResult = await ai.models.generateContent({ model, config: config3, contents });
|
|
101525
|
+
const response = aiResult;
|
|
101526
|
+
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
101527
|
+
const duration = Date.now() - startTime;
|
|
101528
|
+
DebugLogger.logResponse(attempt, rawText);
|
|
101529
|
+
if (!rawText) throw new Error("AI returned an empty response.");
|
|
101530
|
+
const parsedJson = JSON.parse(rawText);
|
|
101531
|
+
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
101532
|
+
const aiGeneratedContent = AIFillInTheBlanksOutputFieldsSchema.parse(parsedJson);
|
|
101533
|
+
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
101534
|
+
const blankCount = aiGeneratedContent.segments.filter((s4) => s4.type === "blank").length;
|
|
101535
|
+
if (blankCount !== clientInput.numberOfBlanks) {
|
|
101536
|
+
throw new Error(`AI generated ${blankCount} blanks, but ${clientInput.numberOfBlanks} were required.`);
|
|
101537
|
+
}
|
|
101538
|
+
aiGeneratedContent.segments.forEach((segment, index3) => {
|
|
101539
|
+
if (segment.type === "blank" && (!segment.acceptedAnswers || segment.acceptedAnswers.length === 0)) {
|
|
101540
|
+
throw new Error(`Segment ${index3} is a 'blank' but is missing 'acceptedAnswers'.`);
|
|
101541
|
+
}
|
|
101542
|
+
if (segment.type === "text" && typeof segment.content !== "string") {
|
|
101543
|
+
throw new Error(`Segment ${index3} is 'text' but is missing 'content'.`);
|
|
101544
|
+
}
|
|
101545
|
+
});
|
|
101546
|
+
if (clientInput.quizContext?.originalCategory) {
|
|
101547
|
+
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
101548
|
+
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
101549
|
+
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
101550
|
+
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
101551
|
+
}
|
|
101552
|
+
}
|
|
101553
|
+
const finalSegments = [];
|
|
101554
|
+
const finalAnswers = [];
|
|
101555
|
+
aiGeneratedContent.segments.forEach((segment) => {
|
|
101556
|
+
if (segment.type === "text") {
|
|
101557
|
+
finalSegments.push({ type: "text", content: segment.content });
|
|
101558
|
+
} else if (segment.type === "blank" && segment.acceptedAnswers) {
|
|
101559
|
+
const blankId = generateUniqueId("blank_");
|
|
101560
|
+
finalSegments.push({ type: "blank", id: blankId });
|
|
101561
|
+
finalAnswers.push({ blankId, acceptedValues: segment.acceptedAnswers });
|
|
101562
|
+
}
|
|
101563
|
+
});
|
|
101564
|
+
const completeQuestion = {
|
|
101565
|
+
id: generateUniqueId("fitb_ai_"),
|
|
101566
|
+
questionType: "fill_in_the_blanks",
|
|
101567
|
+
prompt: aiGeneratedContent.prompt,
|
|
101568
|
+
segments: finalSegments,
|
|
101569
|
+
answers: finalAnswers,
|
|
101570
|
+
isCaseSensitive: clientInput.isCaseSensitive,
|
|
101571
|
+
explanation: aiGeneratedContent.explanation,
|
|
101572
|
+
points: aiGeneratedContent.points,
|
|
101573
|
+
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
101574
|
+
difficulty: clientInput.difficulty,
|
|
101575
|
+
contextCode: clientInput.quizContext?.plannedContextId,
|
|
101576
|
+
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
101577
|
+
learningObjective: clientInput.quizContext?.originalLoId,
|
|
101578
|
+
subject: clientInput.quizContext?.originalSubject,
|
|
101579
|
+
category: clientInput.quizContext?.originalCategory,
|
|
101580
|
+
imageUrl: clientInput.imageUrl
|
|
101581
|
+
};
|
|
101582
|
+
const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
|
|
101583
|
+
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
101584
|
+
console.log(`
|
|
101585
|
+
\u2705 FITB generation successful on attempt ${attempt} (${duration}ms)`);
|
|
101586
|
+
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
101587
|
+
return { question: validatedQuestion };
|
|
101588
|
+
} catch (error) {
|
|
101589
|
+
lastError = error;
|
|
101590
|
+
const duration = Date.now() - startTime;
|
|
101591
|
+
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
101592
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS6;
|
|
101593
|
+
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
101594
|
+
if (willRetry) {
|
|
101595
|
+
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS6}ms...`);
|
|
101596
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS6));
|
|
101732
101597
|
}
|
|
101733
|
-
return topicString;
|
|
101734
|
-
}).join("\n ");
|
|
101735
|
-
const bloomDistribution = clientInput.bloomLevels.map(
|
|
101736
|
-
(b2) => `- Level: "${b2.level}", Ratio: ${b2.ratio}%`
|
|
101737
|
-
).join("\n ");
|
|
101738
|
-
let questionTypesForPrompt = [...clientInput.selectedQuestionTypes];
|
|
101739
|
-
if (numCodingQuestions === 0) {
|
|
101740
|
-
questionTypesForPrompt = questionTypesForPrompt.filter(
|
|
101741
|
-
(type) => type !== "short_answer" && type !== "coding"
|
|
101742
|
-
);
|
|
101743
101598
|
}
|
|
101744
|
-
|
|
101745
|
-
|
|
101746
|
-
|
|
101747
|
-
|
|
101748
|
-
|
|
101749
|
-
|
|
101750
|
-
|
|
101751
|
-
\`\`\`json
|
|
101752
|
-
${JSON.stringify(imageContexts.map((img) => ({ imageId: img.id, subject: img.subject, category: img.category, topic: img.topic, description: img.detailedDescription })), null, 2)}
|
|
101753
|
-
\`\`\`
|
|
101754
|
-
` : "";
|
|
101755
|
-
const enhancedPromptText = `You are an elite educational assessment architect with expertise in cognitive science and adaptive learning. Your mission is to create a strategically optimized quiz plan that maximizes learning effectiveness.
|
|
101756
|
-
|
|
101757
|
-
${generateQuestionTypeSelectionGuidance()}
|
|
101758
|
-
|
|
101759
|
-
${generateAdvancedBloomGuidance()}
|
|
101760
|
-
|
|
101761
|
-
${generateDiversityRules()}
|
|
101762
|
-
|
|
101763
|
-
## COMPREHENSIVE QUIZ REQUIREMENTS:
|
|
101764
|
-
|
|
101765
|
-
**Target Language**: ${language3}
|
|
101766
|
-
**Total Questions**: ${totalQuestions}${codingRequirement}
|
|
101767
|
-
|
|
101768
|
-
**Topic Distribution & Learning Context** (follow precisely):
|
|
101769
|
-
${topicsDistribution}
|
|
101599
|
+
}
|
|
101600
|
+
DebugLogger.logAttemptSummary(attemptResults);
|
|
101601
|
+
const errorMessage = `Failed to generate FITB question after ${MAX_RETRY_ATTEMPTS6} attempts. Last error: ${lastError?.message}`;
|
|
101602
|
+
console.error("\n\u274C Final Result: FAILED");
|
|
101603
|
+
console.error(errorMessage);
|
|
101604
|
+
return { error: errorMessage };
|
|
101605
|
+
}
|
|
101770
101606
|
|
|
101771
|
-
|
|
101772
|
-
|
|
101607
|
+
// src/ai/flows/question-gen/generate-sequence-question.ts
|
|
101608
|
+
init_react_shim();
|
|
101773
101609
|
|
|
101774
|
-
|
|
101610
|
+
// src/ai/flows/question-gen/generate-sequence-question-types.ts
|
|
101611
|
+
init_react_shim();
|
|
101612
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
101613
|
+
numberOfItems: z.number().int().min(2).max(10).optional().default(4)
|
|
101614
|
+
});
|
|
101615
|
+
var AISequenceOutputFieldsSchema = z.object({
|
|
101616
|
+
prompt: z.string().describe("The instructional text for the user, e.g., 'Arrange the steps in the correct order.'"),
|
|
101617
|
+
// Yêu cầu AI cung cấp các item dưới dạng một mảng duy nhất đã được sắp xếp đúng thứ tự
|
|
101618
|
+
// Điều này đơn giản hóa logic và giảm rủi ro
|
|
101619
|
+
itemsInCorrectOrder: z.array(z.string().min(1)).min(2).describe("An array of strings, with each string representing an item to be sequenced. The array itself MUST be in the correct final order."),
|
|
101620
|
+
explanation: z.string().optional(),
|
|
101621
|
+
points: z.number().optional().default(10),
|
|
101622
|
+
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
101623
|
+
topic: z.string().optional(),
|
|
101624
|
+
verifiedCategory: z.string().optional()
|
|
101625
|
+
// Thêm để xác thực
|
|
101626
|
+
});
|
|
101775
101627
|
|
|
101776
|
-
|
|
101628
|
+
// src/ai/flows/question-gen/generate-sequence-question.ts
|
|
101629
|
+
var MAX_RETRY_ATTEMPTS7 = 3;
|
|
101630
|
+
var RETRY_DELAY_MS7 = 3e3;
|
|
101631
|
+
function buildEnhancedPrompt7(clientInput, attemptNumber) {
|
|
101632
|
+
const { quizContext, language: language3, difficulty, numberOfItems, imageUrl } = clientInput;
|
|
101633
|
+
const category = quizContext?.originalCategory || "the specified technical category";
|
|
101634
|
+
const attemptInfo = attemptNumber > 1 ? `
|
|
101635
|
+
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
101636
|
+
Previous attempts failed. Ensure the 'itemsInCorrectOrder' array has exactly the required number of items and the JSON is valid.
|
|
101777
101637
|
|
|
101778
|
-
|
|
101638
|
+
` : "";
|
|
101639
|
+
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The sequence of items must be directly related to the process or content shown in this image.` : "";
|
|
101640
|
+
const contextStrings = [
|
|
101641
|
+
`**Required Category:** ${category}`,
|
|
101642
|
+
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
101643
|
+
imageContextInstruction,
|
|
101644
|
+
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
101645
|
+
quizContext?.targetMisconception && `**Target Misconception:** The sequence should clarify this specific process error: "${quizContext.targetMisconception}"`
|
|
101646
|
+
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
101647
|
+
const exampleJson = JSON.stringify({
|
|
101648
|
+
prompt: "Arrange the steps to make a network request in Swift using URLSession.",
|
|
101649
|
+
itemsInCorrectOrder: [
|
|
101650
|
+
"Create a URL object.",
|
|
101651
|
+
"Create a URLSessionDataTask with the URL.",
|
|
101652
|
+
"Start the task by calling its resume() method.",
|
|
101653
|
+
"Handle the completion handler with data, response, and error."
|
|
101654
|
+
],
|
|
101655
|
+
explanation: "This is the fundamental sequence for a basic data task in URLSession.",
|
|
101656
|
+
points: 10,
|
|
101657
|
+
difficulty: "medium",
|
|
101658
|
+
topic: "Swift Networking",
|
|
101659
|
+
verifiedCategory: category
|
|
101660
|
+
}, null, 2);
|
|
101661
|
+
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
101662
|
+
Your mission is to create a high-quality, technically accurate Sequence Question.
|
|
101779
101663
|
|
|
101780
|
-
|
|
101781
|
-
|
|
101782
|
-
|
|
101783
|
-
|
|
101784
|
-
5. **Context Sensitivity**: Match question types to topic characteristics (technical, conceptual, procedural)
|
|
101785
|
-
6. **Image Context Integration**: Intelligently associate questions with relevant images from the provided library to enhance contextual understanding.
|
|
101664
|
+
## Core Rules (Non-negotiable)
|
|
101665
|
+
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
101666
|
+
2. **Unambiguous Order:** The items MUST represent a clear, objective process, timeline, or order with only one correct sequence.
|
|
101667
|
+
3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
|
|
101786
101668
|
|
|
101787
|
-
##
|
|
101669
|
+
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
101670
|
+
${contextStrings}
|
|
101788
101671
|
|
|
101789
|
-
|
|
101672
|
+
## Task: Generate the Question
|
|
101673
|
+
Based on all the rules and context above, generate a single Sequence Question.
|
|
101790
101674
|
|
|
101791
|
-
|
|
101792
|
-
{
|
|
101793
|
-
|
|
101794
|
-
|
|
101795
|
-
|
|
101796
|
-
"plannedQuestionType": "question_type_from_allowed_list",
|
|
101797
|
-
"plannedBloomLevel": "bloom_level_from_requirements",
|
|
101798
|
-
"plannedContextId": "THEO_ABS",
|
|
101799
|
-
"imageId": "imgctx_12345abcde", // or null
|
|
101800
|
-
"targetMisconception": "Specific misconception this question addresses (or 'none' if not applicable)",
|
|
101801
|
-
"difficultyReason": "Strategic explanation of difficulty choice and placement",
|
|
101802
|
-
"topicSpecificity": "broad|focused|specific",
|
|
101803
|
-
"originalLoId": "corresponding_LoId_from_input",
|
|
101804
|
-
"originalSubject": "corresponding_Subject_from_input",
|
|
101805
|
-
"originalCategory": "corresponding_Category_from_input",
|
|
101806
|
-
"originalTopic": "corresponding_Topic_from_input"
|
|
101807
|
-
}
|
|
101808
|
-
],
|
|
101809
|
-
"diversityMetrics": {
|
|
101810
|
-
"questionTypeDistribution": {"type1": count1, "type2": count2},
|
|
101811
|
-
"bloomLevelDistribution": {"level1": count1, "level2": count2},
|
|
101812
|
-
"maxConsecutiveSameType": number,
|
|
101813
|
-
"difficultyProgression": "description of how difficulty progresses",
|
|
101814
|
-
"misconceptionCoverage": number_of_misconceptions_addressed
|
|
101815
|
-
},
|
|
101816
|
-
"planningStrategy": {
|
|
101817
|
-
"overallApproach": "Brief description of the strategic approach taken",
|
|
101818
|
-
"keyDecisions": ["Major decision 1", "Major decision 2", "Major decision 3"]
|
|
101819
|
-
}
|
|
101820
|
-
}
|
|
101821
|
-
\`\`\`
|
|
101675
|
+
### Input Parameters
|
|
101676
|
+
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
101677
|
+
- **Language for Text:** ${language3}
|
|
101678
|
+
- **Difficulty Level:** ${difficulty}
|
|
101679
|
+
- **Number of Items:** Generate an array 'itemsInCorrectOrder' containing exactly ${numberOfItems} items. The array itself MUST be in the correct final sequence.
|
|
101822
101680
|
|
|
101823
|
-
|
|
101824
|
-
|
|
101825
|
-
|
|
101826
|
-
|
|
101827
|
-
|
|
101828
|
-
|
|
101829
|
-
|
|
101830
|
-
|
|
101831
|
-
|
|
101832
|
-
|
|
101833
|
-
|
|
101834
|
-
|
|
101835
|
-
|
|
101836
|
-
|
|
101837
|
-
|
|
101838
|
-
}
|
|
101839
|
-
];
|
|
101840
|
-
const aiResult = await ai.models.generateContent({
|
|
101841
|
-
model,
|
|
101842
|
-
config: config3,
|
|
101843
|
-
contents
|
|
101844
|
-
});
|
|
101845
|
-
const response = aiResult;
|
|
101846
|
-
const generationDuration = Date.now() - generationStartTime;
|
|
101847
|
-
logger.log("AI_GENERATION", {
|
|
101848
|
-
responseLength: response.candidates?.[0]?.content?.parts?.[0]?.text?.length || 0,
|
|
101849
|
-
duration: generationDuration
|
|
101850
|
-
}, generationDuration);
|
|
101851
|
-
const processingStartTime = Date.now();
|
|
101852
|
-
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
101853
|
-
let jsonText = rawText;
|
|
101854
|
-
if (!rawText.trim().startsWith("{") && !rawText.trim().startsWith("[")) {
|
|
101855
|
-
jsonText = extractJsonFromMarkdown(rawText);
|
|
101856
|
-
}
|
|
101857
|
-
logger.log("JSON_EXTRACTION", {
|
|
101858
|
-
rawTextLength: rawText.length,
|
|
101859
|
-
extractedJsonLength: jsonText.length
|
|
101860
|
-
});
|
|
101861
|
-
const aiGeneratedContent = GenerateQuizPlanOutputSchema.parse(JSON.parse(jsonText));
|
|
101862
|
-
logger.log("SCHEMA_VALIDATION", { success: true }, Date.now() - processingStartTime);
|
|
101863
|
-
const validationStartTime = Date.now();
|
|
101864
|
-
if (aiGeneratedContent.quizPlan.length !== clientInput.totalQuestions) {
|
|
101865
|
-
throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested.`);
|
|
101681
|
+
### Required JSON Output Format
|
|
101682
|
+
Your response must be ONLY the JSON object, matching this exact structure:
|
|
101683
|
+
|
|
101684
|
+
${exampleJson}
|
|
101685
|
+
|
|
101686
|
+
Now, generate the JSON for the requested question.`;
|
|
101687
|
+
}
|
|
101688
|
+
async function generateSequenceQuestion(clientInput, apiKey) {
|
|
101689
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
101690
|
+
const model = "gemini-2.5-flash";
|
|
101691
|
+
const config3 = {
|
|
101692
|
+
temperature: 0.7,
|
|
101693
|
+
responseMimeType: "application/json",
|
|
101694
|
+
thinkingConfig: {
|
|
101695
|
+
thinkingBudget: 4e3
|
|
101866
101696
|
}
|
|
101867
|
-
|
|
101868
|
-
|
|
101869
|
-
|
|
101870
|
-
|
|
101697
|
+
};
|
|
101698
|
+
const attemptResults = [];
|
|
101699
|
+
let lastError = null;
|
|
101700
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS7; attempt++) {
|
|
101701
|
+
const startTime = Date.now();
|
|
101702
|
+
const promptText = buildEnhancedPrompt7(clientInput, attempt);
|
|
101703
|
+
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
101704
|
+
try {
|
|
101705
|
+
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
101706
|
+
const parts = [{ text: promptText }];
|
|
101707
|
+
if (clientInput.imageUrl) {
|
|
101708
|
+
const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
101709
|
+
const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
|
|
101710
|
+
parts.unshift(imagePart);
|
|
101871
101711
|
}
|
|
101872
|
-
|
|
101873
|
-
|
|
101874
|
-
|
|
101875
|
-
|
|
101876
|
-
|
|
101877
|
-
|
|
101878
|
-
throw new Error(
|
|
101879
|
-
|
|
101880
|
-
|
|
101881
|
-
|
|
101882
|
-
|
|
101883
|
-
|
|
101884
|
-
|
|
101885
|
-
|
|
101886
|
-
|
|
101887
|
-
|
|
101888
|
-
|
|
101889
|
-
|
|
101890
|
-
|
|
101891
|
-
|
|
101892
|
-
|
|
101893
|
-
|
|
101894
|
-
|
|
101895
|
-
|
|
101896
|
-
|
|
101897
|
-
|
|
101898
|
-
|
|
101899
|
-
|
|
101900
|
-
|
|
101901
|
-
|
|
101902
|
-
|
|
101903
|
-
|
|
101904
|
-
|
|
101905
|
-
|
|
101906
|
-
|
|
101907
|
-
|
|
101908
|
-
|
|
101909
|
-
|
|
101910
|
-
|
|
101911
|
-
|
|
101912
|
-
|
|
101913
|
-
|
|
101914
|
-
|
|
101915
|
-
|
|
101916
|
-
|
|
101917
|
-
|
|
101918
|
-
|
|
101919
|
-
|
|
101920
|
-
|
|
101921
|
-
|
|
101922
|
-
|
|
101923
|
-
|
|
101924
|
-
|
|
101925
|
-
|
|
101926
|
-
|
|
101927
|
-
|
|
101928
|
-
|
|
101929
|
-
if (
|
|
101930
|
-
|
|
101931
|
-
|
|
101932
|
-
maxStartIndex = currentStartIndex;
|
|
101712
|
+
const contents = [{ role: "user", parts }];
|
|
101713
|
+
const aiResult = await ai.models.generateContent({ model, config: config3, contents });
|
|
101714
|
+
const response = aiResult;
|
|
101715
|
+
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
101716
|
+
const duration = Date.now() - startTime;
|
|
101717
|
+
DebugLogger.logResponse(attempt, rawText);
|
|
101718
|
+
if (!rawText) throw new Error("AI returned an empty response.");
|
|
101719
|
+
const parsedJson = JSON.parse(rawText);
|
|
101720
|
+
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
101721
|
+
const aiGeneratedContent = AISequenceOutputFieldsSchema.parse(parsedJson);
|
|
101722
|
+
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
101723
|
+
if (aiGeneratedContent.itemsInCorrectOrder.length !== clientInput.numberOfItems) {
|
|
101724
|
+
throw new Error(`AI generated ${aiGeneratedContent.itemsInCorrectOrder.length} items, but ${clientInput.numberOfItems} were required.`);
|
|
101725
|
+
}
|
|
101726
|
+
if (clientInput.quizContext?.originalCategory) {
|
|
101727
|
+
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
101728
|
+
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
101729
|
+
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
101730
|
+
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
101731
|
+
}
|
|
101732
|
+
}
|
|
101733
|
+
const finalItems = [];
|
|
101734
|
+
const finalCorrectOrder = [];
|
|
101735
|
+
aiGeneratedContent.itemsInCorrectOrder.forEach((content4) => {
|
|
101736
|
+
const id3 = generateUniqueId("seqi_");
|
|
101737
|
+
finalItems.push({ id: id3, content: content4 });
|
|
101738
|
+
finalCorrectOrder.push(id3);
|
|
101739
|
+
});
|
|
101740
|
+
const completeQuestion = {
|
|
101741
|
+
id: generateUniqueId("seq_ai_"),
|
|
101742
|
+
questionType: "sequence",
|
|
101743
|
+
prompt: aiGeneratedContent.prompt,
|
|
101744
|
+
items: finalItems,
|
|
101745
|
+
correctOrder: finalCorrectOrder,
|
|
101746
|
+
explanation: aiGeneratedContent.explanation,
|
|
101747
|
+
points: aiGeneratedContent.points,
|
|
101748
|
+
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
101749
|
+
difficulty: clientInput.difficulty,
|
|
101750
|
+
contextCode: clientInput.quizContext?.plannedContextId,
|
|
101751
|
+
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
101752
|
+
learningObjective: clientInput.quizContext?.originalLoId,
|
|
101753
|
+
subject: clientInput.quizContext?.originalSubject,
|
|
101754
|
+
category: clientInput.quizContext?.originalCategory,
|
|
101755
|
+
imageUrl: clientInput.imageUrl
|
|
101756
|
+
};
|
|
101757
|
+
const validatedQuestion = SequenceQuestionZodSchema.parse(completeQuestion);
|
|
101758
|
+
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
101759
|
+
console.log(`
|
|
101760
|
+
\u2705 Sequence generation successful on attempt ${attempt} (${duration}ms)`);
|
|
101761
|
+
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
101762
|
+
return { question: validatedQuestion };
|
|
101763
|
+
} catch (error) {
|
|
101764
|
+
lastError = error;
|
|
101765
|
+
const duration = Date.now() - startTime;
|
|
101766
|
+
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
101767
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS7;
|
|
101768
|
+
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
101769
|
+
if (willRetry) {
|
|
101770
|
+
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS7}ms...`);
|
|
101771
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS7));
|
|
101933
101772
|
}
|
|
101934
|
-
currentConsecutive = 1;
|
|
101935
|
-
currentType = quizPlan[i2].plannedQuestionType;
|
|
101936
|
-
currentStartIndex = i2;
|
|
101937
101773
|
}
|
|
101938
101774
|
}
|
|
101939
|
-
|
|
101940
|
-
|
|
101941
|
-
|
|
101942
|
-
|
|
101943
|
-
}
|
|
101944
|
-
return {
|
|
101945
|
-
maxConsecutive,
|
|
101946
|
-
type: maxType,
|
|
101947
|
-
startIndex: maxStartIndex
|
|
101948
|
-
};
|
|
101775
|
+
DebugLogger.logAttemptSummary(attemptResults);
|
|
101776
|
+
const errorMessage = `Failed to generate Sequence question after ${MAX_RETRY_ATTEMPTS7} attempts. Last error: ${lastError?.message}`;
|
|
101777
|
+
console.error("\n\u274C Final Result: FAILED");
|
|
101778
|
+
console.error(errorMessage);
|
|
101779
|
+
return { error: errorMessage };
|
|
101949
101780
|
}
|
|
101950
101781
|
|
|
101951
|
-
// src/ai/flows/generate-
|
|
101782
|
+
// src/ai/flows/question-gen/generate-matching-question.ts
|
|
101952
101783
|
init_react_shim();
|
|
101953
101784
|
|
|
101954
|
-
// src/
|
|
101785
|
+
// src/ai/flows/question-gen/generate-matching-question-types.ts
|
|
101955
101786
|
init_react_shim();
|
|
101956
|
-
|
|
101957
|
-
|
|
101958
|
-
|
|
101959
|
-
|
|
101960
|
-
|
|
101961
|
-
|
|
101962
|
-
|
|
101963
|
-
|
|
101964
|
-
|
|
101965
|
-
|
|
101966
|
-
|
|
101967
|
-
|
|
101968
|
-
|
|
101969
|
-
|
|
101970
|
-
|
|
101971
|
-
|
|
101972
|
-
|
|
101973
|
-
|
|
101974
|
-
|
|
101975
|
-
|
|
101976
|
-
|
|
101977
|
-
|
|
101978
|
-
|
|
101979
|
-
|
|
101980
|
-
|
|
101981
|
-
|
|
101982
|
-
|
|
101983
|
-
|
|
101984
|
-
|
|
101985
|
-
|
|
101986
|
-
|
|
101987
|
-
|
|
101988
|
-
|
|
101989
|
-
|
|
101990
|
-
|
|
101991
|
-
|
|
101992
|
-
|
|
101993
|
-
|
|
101994
|
-
|
|
101995
|
-
|
|
101996
|
-
|
|
101787
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
101788
|
+
numberOfPairs: z.number().int().min(2).max(8).optional().default(4),
|
|
101789
|
+
shuffleOptions: z.boolean().optional().default(true)
|
|
101790
|
+
});
|
|
101791
|
+
var AIMatchingOutputFieldsSchema = z.object({
|
|
101792
|
+
prompt: z.string().describe("The instructional text for the user, e.g., 'Match the concept to its definition.'"),
|
|
101793
|
+
correctPairs: z.array(z.object({
|
|
101794
|
+
promptText: z.string().min(1).describe("The text for the left-hand side item (the prompt)."),
|
|
101795
|
+
optionText: z.string().min(1).describe("The text for the right-hand side item (the matching option).")
|
|
101796
|
+
})).min(2),
|
|
101797
|
+
explanation: z.string().optional(),
|
|
101798
|
+
points: z.number().optional().default(10),
|
|
101799
|
+
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
101800
|
+
topic: z.string().optional(),
|
|
101801
|
+
verifiedCategory: z.string().optional()
|
|
101802
|
+
// Thêm để xác thực
|
|
101803
|
+
});
|
|
101804
|
+
|
|
101805
|
+
// src/ai/flows/question-gen/generate-matching-question.ts
|
|
101806
|
+
var MAX_RETRY_ATTEMPTS8 = 3;
|
|
101807
|
+
var RETRY_DELAY_MS8 = 3e3;
|
|
101808
|
+
function buildEnhancedPrompt8(clientInput, attemptNumber) {
|
|
101809
|
+
const { quizContext, language: language3, difficulty, numberOfPairs, imageUrl } = clientInput;
|
|
101810
|
+
const category = quizContext?.originalCategory || "the specified technical category";
|
|
101811
|
+
const attemptInfo = attemptNumber > 1 ? `
|
|
101812
|
+
## DEBUG INFO - This is attempt #${attemptNumber}
|
|
101813
|
+
Previous attempts failed. Please ensure the 'correctPairs' array has exactly the required number of items and the JSON is valid.
|
|
101814
|
+
|
|
101815
|
+
` : "";
|
|
101816
|
+
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The matching pairs must be directly related to the content of this image.` : "";
|
|
101817
|
+
const contextStrings = [
|
|
101818
|
+
`**Required Category:** ${category}`,
|
|
101819
|
+
quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
|
|
101820
|
+
imageContextInstruction,
|
|
101821
|
+
quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
|
|
101822
|
+
quizContext?.targetMisconception && `**Target Misconception:** Design a pair that specifically tests this confusion: "${quizContext.targetMisconception}"`
|
|
101823
|
+
].filter(Boolean).map((s4) => `- ${s4}`).join("\n");
|
|
101824
|
+
const exampleJson = JSON.stringify({
|
|
101825
|
+
prompt: "Match each Swift collection type to its primary characteristic.",
|
|
101826
|
+
correctPairs: [
|
|
101827
|
+
{ "promptText": "Array", "optionText": "An ordered, random-access collection." },
|
|
101828
|
+
{ "promptText": "Set", "optionText": "An unordered collection of unique elements." },
|
|
101829
|
+
{ "promptText": "Dictionary", "optionText": "An unordered collection of key-value associations." }
|
|
101830
|
+
],
|
|
101831
|
+
explanation: "These are the fundamental characteristics of Swift's main collection types.",
|
|
101832
|
+
points: 10,
|
|
101833
|
+
difficulty: "easy",
|
|
101834
|
+
topic: "Swift Collection Types",
|
|
101835
|
+
verifiedCategory: category
|
|
101836
|
+
}, null, 2);
|
|
101837
|
+
return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
|
|
101838
|
+
Your mission is to create a high-quality, technically accurate Matching Question.
|
|
101839
|
+
|
|
101840
|
+
## Core Rules (Non-negotiable)
|
|
101841
|
+
1. **Category Purity:** The question MUST be exclusively about **${category}**.
|
|
101842
|
+
2. **Logical Pairs:** The items to be matched must have a clear, one-to-one relationship.
|
|
101843
|
+
3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
|
|
101844
|
+
|
|
101845
|
+
## CRITICAL CONTEXT FOR THIS QUESTION
|
|
101846
|
+
${contextStrings}
|
|
101847
|
+
|
|
101848
|
+
## Task: Generate the Question
|
|
101849
|
+
Based on all the rules and context above, generate a single Matching Question.
|
|
101850
|
+
|
|
101851
|
+
### Input Parameters
|
|
101852
|
+
- **Topic for Question:** ${quizContext?.plannedTopic || "General"}
|
|
101853
|
+
- **Language for Text:** ${language3}
|
|
101854
|
+
- **Difficulty Level:** ${difficulty}
|
|
101855
|
+
- **Number of Pairs:** Generate exactly ${numberOfPairs} correct pairs in the 'correctPairs' array.
|
|
101856
|
+
|
|
101857
|
+
### Required JSON Output Format
|
|
101858
|
+
Your response must be ONLY the JSON object, matching this exact structure:
|
|
101859
|
+
|
|
101860
|
+
${exampleJson}
|
|
101861
|
+
|
|
101862
|
+
Now, generate the JSON for the requested question.`;
|
|
101863
|
+
}
|
|
101864
|
+
async function generateMatchingQuestion(clientInput, apiKey) {
|
|
101865
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
101866
|
+
const model = "gemini-2.5-flash";
|
|
101867
|
+
const config3 = {
|
|
101868
|
+
temperature: 0.8,
|
|
101869
|
+
responseMimeType: "application/json",
|
|
101870
|
+
thinkingConfig: {
|
|
101871
|
+
thinkingBudget: 4e3
|
|
101997
101872
|
}
|
|
101998
|
-
}
|
|
101999
|
-
|
|
102000
|
-
|
|
102001
|
-
|
|
102002
|
-
|
|
101873
|
+
};
|
|
101874
|
+
const attemptResults = [];
|
|
101875
|
+
let lastError = null;
|
|
101876
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS8; attempt++) {
|
|
101877
|
+
const startTime = Date.now();
|
|
101878
|
+
const promptText = buildEnhancedPrompt8(clientInput, attempt);
|
|
101879
|
+
const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
|
|
102003
101880
|
try {
|
|
102004
|
-
|
|
102005
|
-
|
|
102006
|
-
|
|
102007
|
-
|
|
102008
|
-
|
|
102009
|
-
|
|
102010
|
-
/**
|
|
102011
|
-
* Parses TSV content into an array of LearningObjective objects.
|
|
102012
|
-
* @param tsvContent The raw string content from a .tsv file.
|
|
102013
|
-
* @returns An object containing the successfully parsed data and any errors encountered.
|
|
102014
|
-
*/
|
|
102015
|
-
static parseTSV(tsvContent) {
|
|
102016
|
-
const lines = tsvContent.split("\n").filter((line) => line.trim() !== "");
|
|
102017
|
-
if (lines.length < 2) {
|
|
102018
|
-
return { data: [], errors: ["File is empty or contains only a header."] };
|
|
102019
|
-
}
|
|
102020
|
-
const headerLine = lines.shift();
|
|
102021
|
-
const headers = headerLine.split(" ").map((h3) => h3.trim());
|
|
102022
|
-
if (headers.length !== this.EXPECTED_HEADERS.length || !this.EXPECTED_HEADERS.every((h3, i2) => h3 === headers[i2])) {
|
|
102023
|
-
const errorMsg = `Invalid TSV header. Expected: "${this.EXPECTED_HEADERS.join(" ")}". Received: "${headers.join(" ")}"`;
|
|
102024
|
-
return { data: [], errors: [errorMsg] };
|
|
102025
|
-
}
|
|
102026
|
-
const data = [];
|
|
102027
|
-
const errors2 = [];
|
|
102028
|
-
lines.forEach((line, index3) => {
|
|
102029
|
-
const values = line.split(" ").map((v) => v.trim());
|
|
102030
|
-
if (values.length !== this.EXPECTED_HEADERS.length) {
|
|
102031
|
-
errors2.push(`Line ${index3 + 2}: Incorrect number of columns. Expected ${this.EXPECTED_HEADERS.length}, but got ${values.length}.`);
|
|
102032
|
-
return;
|
|
101881
|
+
DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
|
|
101882
|
+
const parts = [{ text: promptText }];
|
|
101883
|
+
if (clientInput.imageUrl) {
|
|
101884
|
+
const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
101885
|
+
const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
|
|
101886
|
+
parts.unshift(imagePart);
|
|
102033
101887
|
}
|
|
102034
|
-
const [
|
|
102035
|
-
|
|
102036
|
-
|
|
102037
|
-
|
|
102038
|
-
|
|
102039
|
-
|
|
102040
|
-
|
|
102041
|
-
|
|
102042
|
-
|
|
102043
|
-
|
|
102044
|
-
|
|
102045
|
-
if (
|
|
102046
|
-
|
|
102047
|
-
return;
|
|
101888
|
+
const contents = [{ role: "user", parts }];
|
|
101889
|
+
const aiResult = await ai.models.generateContent({ model, config: config3, contents });
|
|
101890
|
+
const response = aiResult;
|
|
101891
|
+
const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
101892
|
+
const duration = Date.now() - startTime;
|
|
101893
|
+
DebugLogger.logResponse(attempt, rawText);
|
|
101894
|
+
if (!rawText) throw new Error("AI returned an empty response.");
|
|
101895
|
+
const parsedJson = JSON.parse(rawText);
|
|
101896
|
+
DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
|
|
101897
|
+
const aiGeneratedContent = AIMatchingOutputFieldsSchema.parse(parsedJson);
|
|
101898
|
+
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
101899
|
+
if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
|
|
101900
|
+
throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were required.`);
|
|
102048
101901
|
}
|
|
102049
|
-
|
|
102050
|
-
|
|
102051
|
-
|
|
102052
|
-
|
|
102053
|
-
|
|
102054
|
-
|
|
102055
|
-
|
|
102056
|
-
|
|
102057
|
-
|
|
102058
|
-
|
|
101902
|
+
if (clientInput.quizContext?.originalCategory) {
|
|
101903
|
+
const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
|
|
101904
|
+
const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
|
|
101905
|
+
if (verifiedCategory && verifiedCategory !== requiredCategory) {
|
|
101906
|
+
throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
|
|
101907
|
+
}
|
|
101908
|
+
}
|
|
101909
|
+
const finalPrompts = [];
|
|
101910
|
+
const finalOptions = [];
|
|
101911
|
+
const finalCorrectAnswerMap = [];
|
|
101912
|
+
aiGeneratedContent.correctPairs.forEach((pair2) => {
|
|
101913
|
+
const promptId = generateUniqueId("m_p_");
|
|
101914
|
+
const optionId = generateUniqueId("m_o_");
|
|
101915
|
+
finalPrompts.push({ id: promptId, content: pair2.promptText });
|
|
101916
|
+
finalOptions.push({ id: optionId, content: pair2.optionText });
|
|
101917
|
+
finalCorrectAnswerMap.push({ promptId, optionId });
|
|
101918
|
+
});
|
|
101919
|
+
const completeQuestion = {
|
|
101920
|
+
id: generateUniqueId("match_ai_"),
|
|
101921
|
+
questionType: "matching",
|
|
101922
|
+
prompt: aiGeneratedContent.prompt,
|
|
101923
|
+
prompts: finalPrompts,
|
|
101924
|
+
options: finalOptions,
|
|
101925
|
+
correctAnswerMap: finalCorrectAnswerMap,
|
|
101926
|
+
shuffleOptions: clientInput.shuffleOptions,
|
|
101927
|
+
explanation: aiGeneratedContent.explanation,
|
|
101928
|
+
points: aiGeneratedContent.points,
|
|
101929
|
+
topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
|
|
101930
|
+
difficulty: clientInput.difficulty,
|
|
101931
|
+
contextCode: clientInput.quizContext?.plannedContextId,
|
|
101932
|
+
bloomLevel: clientInput.quizContext?.plannedBloomLevel,
|
|
101933
|
+
learningObjective: clientInput.quizContext?.originalLoId,
|
|
101934
|
+
subject: clientInput.quizContext?.originalSubject,
|
|
101935
|
+
category: clientInput.quizContext?.originalCategory,
|
|
101936
|
+
imageUrl: clientInput.imageUrl
|
|
102059
101937
|
};
|
|
102060
|
-
|
|
102061
|
-
|
|
102062
|
-
|
|
102063
|
-
|
|
102064
|
-
|
|
102065
|
-
|
|
102066
|
-
|
|
102067
|
-
|
|
102068
|
-
|
|
102069
|
-
|
|
102070
|
-
|
|
102071
|
-
|
|
102072
|
-
|
|
102073
|
-
|
|
102074
|
-
|
|
102075
|
-
|
|
102076
|
-
|
|
102077
|
-
*/
|
|
102078
|
-
static getCategoriesBySubject(subject) {
|
|
102079
|
-
const data = this.getData();
|
|
102080
|
-
const categories = data.filter((item) => item.subject === subject).map((item) => item.category);
|
|
102081
|
-
return [...new Set(categories)].sort();
|
|
102082
|
-
}
|
|
102083
|
-
/**
|
|
102084
|
-
* Gets a unique list of topics for a given category.
|
|
102085
|
-
* @param category The category to filter by.
|
|
102086
|
-
* @returns An array of topic strings.
|
|
102087
|
-
*/
|
|
102088
|
-
static getTopicsByCategory(category) {
|
|
102089
|
-
const data = this.getData();
|
|
102090
|
-
const topics = data.filter((item) => item.category === category).map((item) => item.topic);
|
|
102091
|
-
return [...new Set(topics)].sort();
|
|
102092
|
-
}
|
|
102093
|
-
/**
|
|
102094
|
-
* Retrieves all LearningObjective details for a given list of topics.
|
|
102095
|
-
* @param topics An array of topic strings to search for.
|
|
102096
|
-
* @returns An array of matching LearningObjective objects.
|
|
102097
|
-
*/
|
|
102098
|
-
static getLearningObjectivesByTopics(topics) {
|
|
102099
|
-
const data = this.getData();
|
|
102100
|
-
const topicSet = new Set(topics);
|
|
102101
|
-
return data.filter((item) => topicSet.has(item.topic));
|
|
101938
|
+
const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
|
|
101939
|
+
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
101940
|
+
console.log(`
|
|
101941
|
+
\u2705 Matching generation successful on attempt ${attempt} (${duration}ms)`);
|
|
101942
|
+
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
101943
|
+
return { question: validatedQuestion };
|
|
101944
|
+
} catch (error) {
|
|
101945
|
+
lastError = error;
|
|
101946
|
+
const duration = Date.now() - startTime;
|
|
101947
|
+
attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
|
|
101948
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS8;
|
|
101949
|
+
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
101950
|
+
if (willRetry) {
|
|
101951
|
+
console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS8}ms...`);
|
|
101952
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS8));
|
|
101953
|
+
}
|
|
101954
|
+
}
|
|
102102
101955
|
}
|
|
102103
|
-
|
|
102104
|
-
|
|
102105
|
-
|
|
102106
|
-
|
|
102107
|
-
|
|
102108
|
-
|
|
102109
|
-
"Category",
|
|
102110
|
-
"Topic",
|
|
102111
|
-
"Keywords",
|
|
102112
|
-
"Grade",
|
|
102113
|
-
"STEM Element(s)",
|
|
102114
|
-
"Bloom\u2019s Level(s) Guideline"
|
|
102115
|
-
];
|
|
101956
|
+
DebugLogger.logAttemptSummary(attemptResults);
|
|
101957
|
+
const errorMessage = `Failed to generate Matching question after ${MAX_RETRY_ATTEMPTS8} attempts. Last error: ${lastError?.message}`;
|
|
101958
|
+
console.error("\n\u274C Final Result: FAILED");
|
|
101959
|
+
console.error(errorMessage);
|
|
101960
|
+
return { error: errorMessage };
|
|
101961
|
+
}
|
|
102116
101962
|
|
|
102117
101963
|
// src/ai/flows/question-gen/generate-coding-question.ts
|
|
102118
101964
|
init_react_shim();
|
|
@@ -102284,7 +102130,7 @@ async function generateCodingQuestion(clientInput, apiKey) {
|
|
|
102284
102130
|
var MAX_ATTEMPTS = 3;
|
|
102285
102131
|
var RETRY_DELAY_MS10 = 2e3;
|
|
102286
102132
|
var delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
102287
|
-
var
|
|
102133
|
+
var calculateCombinedDifficulty = (plannedQ) => {
|
|
102288
102134
|
const { plannedBloomLevel, plannedQuestionType, plannedContextId } = plannedQ;
|
|
102289
102135
|
let contextScore = 1;
|
|
102290
102136
|
if (["SPEC_CASE", "NAT_OBS", "DATA_MOD", "INTERDISC", "HYPO_COMP"].includes(plannedContextId || "")) contextScore = 2;
|
|
@@ -102341,7 +102187,7 @@ async function generateQuestionsFromQuizPlan(clientInput, apiKey) {
|
|
|
102341
102187
|
const imageUrl = plannedQ.imageId && imageContexts ? imageContexts.find((ctx) => ctx.id === plannedQ.imageId)?.imageUrl : void 0;
|
|
102342
102188
|
const baseClientInput = {
|
|
102343
102189
|
language: language3,
|
|
102344
|
-
difficulty:
|
|
102190
|
+
difficulty: calculateCombinedDifficulty(plannedQ),
|
|
102345
102191
|
quizContext,
|
|
102346
102192
|
imageUrl
|
|
102347
102193
|
};
|
|
@@ -102423,6 +102269,22 @@ async function generateQuestionsFromQuizPlan(clientInput, apiKey) {
|
|
|
102423
102269
|
return { generatedQuestions, errors: errors2.length > 0 ? errors2 : void 0 };
|
|
102424
102270
|
}
|
|
102425
102271
|
|
|
102272
|
+
// src/data/contextOptions.ts
|
|
102273
|
+
init_react_shim();
|
|
102274
|
+
var contextOptions = [
|
|
102275
|
+
{ shortContextId: "A", contextId: "THEO_ABS", contextDescription: "L\xFD thuy\u1EBFt/Tr\u1EEBu t\u01B0\u1EE3ng", contextExample: '"Ph\xE1t bi\u1EC3u \u0111\u1ECBnh lu\u1EADt III Newton." ho\u1EB7c "Gi\u1EA3i th\xEDch kh\xE1i ni\u1EC7m \u0111\u1EA1o h\xE0m."', keywords: "l\xFD thuy\u1EBFt, tr\u1EEBu t\u01B0\u1EE3ng, \u0111\u1ECBnh ngh\u0129a, kh\xE1i ni\u1EC7m, nguy\xEAn l\xFD, c\xF4ng th\u1EE9c, \u0111\u1ECBnh lu\u1EADt" },
|
|
102276
|
+
{ shortContextId: "B", contextId: "SPEC_CASE", contextDescription: "V\xED d\u1EE5 C\u1EE5 th\u1EC3/Tr\u01B0\u1EDDng h\u1EE3p Ri\xEAng", contextExample: '"T\xEDnh l\u1EF1c t\xE1c d\u1EE5ng l\xEAn v\u1EADt n\u1EB7ng 2kg r\u01A1i t\u1EF1 do." ho\u1EB7c "Cho h\xE0m s\u1ED1 y = x^2, t\xECm \u0111\u1EA1o h\xE0m t\u1EA1i x = 3."', keywords: "v\xED d\u1EE5, c\u1EE5 th\u1EC3, tr\u01B0\u1EDDng h\u1EE3p ri\xEAng, \xE1p d\u1EE5ng, t\xEDnh to\xE1n, minh h\u1ECDa, s\u1ED1 li\u1EC7u c\u1EE5 th\u1EC3" },
|
|
102277
|
+
{ shortContextId: "C", contextId: "NAT_OBS", contextDescription: "Hi\u1EC7n t\u01B0\u1EE3ng T\u1EF1 nhi\xEAn/Quan s\xE1t", contextExample: '"Gi\u1EA3i th\xEDch t\u1EA1i sao c\u1EA7u v\u1ED3ng xu\u1EA5t hi\u1EC7n sau c\u01A1n m\u01B0a." ho\u1EB7c "M\xF4 t\u1EA3 qu\xE1 tr\xECnh quang h\u1EE3p \u1EDF l\xE1 c\xE2y."', keywords: "hi\u1EC7n t\u01B0\u1EE3ng, t\u1EF1 nhi\xEAn, quan s\xE1t, gi\u1EA3i th\xEDch, m\xF4 t\u1EA3, th\u1EBF gi\u1EDBi th\u1EF1c, sinh h\u1ECDc, v\u1EADt l\xFD" },
|
|
102278
|
+
{ shortContextId: "D", contextId: "TECH_ENG", contextDescription: "\u1EE8ng d\u1EE5ng C\xF4ng ngh\u1EC7/K\u1EF9 thu\u1EADt", contextExample: '"Nguy\xEAn l\xFD ho\u1EA1t \u0111\u1ED9ng c\u1EE7a \u0111\u1ED9ng c\u01A1 \u0111\u1ED1t trong l\xE0 g\xEC?" ho\u1EB7c "Pin m\u1EB7t tr\u1EDDi chuy\u1EC3n \u0111\u1ED5i n\u0103ng l\u01B0\u1EE3ng \xE1nh s\xE1ng th\xE0nh \u0111i\u1EC7n n\u0103ng nh\u01B0 th\u1EBF n\xE0o?"', keywords: "\u1EE9ng d\u1EE5ng, c\xF4ng ngh\u1EC7, k\u1EF9 thu\u1EADt, thi\u1EBFt b\u1ECB, m\xE1y m\xF3c, ho\u1EA1t \u0111\u1ED9ng, nguy\xEAn l\xFD, ch\u1EBF t\u1EA1o" },
|
|
102279
|
+
{ shortContextId: "E", contextId: "EXP_INV", contextDescription: "Th\xED nghi\u1EC7m/\u0110i\u1EC1u tra Khoa h\u1ECDc", contextExample: '"Thi\u1EBFt k\u1EBF th\xED nghi\u1EC7m ch\u1EE9ng minh \u0111\u1ECBnh lu\u1EADt b\u1EA3o to\xE0n n\u0103ng l\u01B0\u1EE3ng." ho\u1EB7c "Ph\xE2n t\xEDch k\u1EBFt qu\u1EA3 th\xED nghi\u1EC7m v\u1EC1 t\u1ED1c \u0111\u1ED9 ph\u1EA3n \u1EE9ng."', keywords: "th\xED nghi\u1EC7m, \u0111i\u1EC1u tra, khoa h\u1ECDc, thi\u1EBFt k\u1EBF, quy tr\xECnh, k\u1EBFt qu\u1EA3, ph\xE2n t\xEDch, d\u1EEF li\u1EC7u" },
|
|
102280
|
+
{ shortContextId: "F", contextId: "REAL_PROB", contextDescription: "V\u1EA5n \u0111\u1EC1 Th\u1EF1c t\u1EBF/X\xE3 h\u1ED9i/M\xF4i tr\u01B0\u1EDDng", contextExample: '"\u0110\u1EC1 xu\u1EA5t gi\u1EA3i ph\xE1p gi\u1EA3m thi\u1EC3u \xF4 nhi\u1EC5m kh\xF4ng kh\xED t\u1EA1i \u0111\xF4 th\u1ECB." ho\u1EB7c "Ph\xE2n t\xEDch \u1EA3nh h\u01B0\u1EDFng c\u1EE7a bi\u1EBFn \u0111\u1ED5i kh\xED h\u1EADu \u0111\u1EBFn n\xF4ng nghi\u1EC7p."', keywords: "v\u1EA5n \u0111\u1EC1, th\u1EF1c t\u1EBF, x\xE3 h\u1ED9i, m\xF4i tr\u01B0\u1EDDng, gi\u1EA3i ph\xE1p, ph\xE2n t\xEDch, \u1EA3nh h\u01B0\u1EDFng, b\u1EC1n v\u1EEFng" },
|
|
102281
|
+
{ shortContextId: "G", contextId: "DATA_MOD", contextDescription: "Di\u1EC5n gi\u1EA3i D\u1EEF li\u1EC7u/M\xF4 h\xECnh h\xF3a", contextExample: '"D\u1EF1a v\xE0o bi\u1EC3u \u0111\u1ED3, nh\u1EADn x\xE9t xu h\u01B0\u1EDBng nhi\u1EC7t \u0111\u1ED9 to\xE0n c\u1EA7u." ho\u1EB7c "X\xE2y d\u1EF1ng m\xF4 h\xECnh to\xE1n h\u1ECDc m\xF4 t\u1EA3 s\u1EF1 t\u0103ng tr\u01B0\u1EDFng d\xE2n s\u1ED1."', keywords: "d\u1EEF li\u1EC7u, bi\u1EC3u \u0111\u1ED3, b\u1EA3ng s\u1ED1 li\u1EC7u, m\xF4 h\xECnh, di\u1EC5n gi\u1EA3i, ph\xE2n t\xEDch, xu h\u01B0\u1EDBng, d\u1EF1 \u0111o\xE1n" },
|
|
102282
|
+
{ shortContextId: "H", contextId: "HIST_SCI", contextDescription: "L\u1ECBch s\u1EED/Ph\xE1t tri\u1EC3n Khoa h\u1ECDc", contextExample: '"Tr\xECnh b\xE0y b\u1ED1i c\u1EA3nh ra \u0111\u1EDDi thuy\u1EBFt t\u01B0\u01A1ng \u0111\u1ED1i c\u1EE7a Einstein." ho\u1EB7c "Ai l\xE0 ng\u01B0\u1EDDi \u0111\u1EA7u ti\xEAn ph\xE1t hi\u1EC7n ra c\u1EA5u tr\xFAc DNA?"', keywords: "l\u1ECBch s\u1EED, ph\xE1t tri\u1EC3n, khoa h\u1ECDc, nh\xE0 khoa h\u1ECDc, ph\xE1t minh, kh\xE1m ph\xE1, b\u1ED1i c\u1EA3nh" },
|
|
102283
|
+
{ shortContextId: "I", contextId: "INTERDISC", contextDescription: "Li\xEAn ng\xE0nh (Interdisciplinary)", contextExample: '"S\u1EED d\u1EE5ng ki\u1EBFn th\u1EE9c To\xE1n v\xE0 V\u1EADt l\xFD \u0111\u1EC3 t\xEDnh qu\u1EF9 \u0111\u1EA1o c\u1EE7a v\u1EADt n\xE9m xi\xEAn." ho\u1EB7c "Ph\xE2n t\xEDch c\u01A1 ch\u1EBF h\xF3a sinh c\u1EE7a qu\xE1 tr\xECnh ti\xEAu h\xF3a."', keywords: "li\xEAn ng\xE0nh, t\xEDch h\u1EE3p, To\xE1n-L\xFD, Sinh-H\xF3a, Khoa h\u1ECDc-C\xF4ng ngh\u1EC7, k\u1EBFt n\u1ED1i ki\u1EBFn th\u1EE9c" },
|
|
102284
|
+
{ shortContextId: "J", contextId: "HYPO_COMP", contextDescription: "Gi\u1EA3 \u0111\u1ECBnh/So s\xE1nh T\xECnh hu\u1ED1ng", contextExample: '"N\u1EBFu Tr\xE1i \u0110\u1EA5t kh\xF4ng c\xF3 t\u1EEB tr\u01B0\u1EDDng th\xEC \u0111i\u1EC1u g\xEC s\u1EBD x\u1EA3y ra?" ho\u1EB7c "So s\xE1nh \u01B0u nh\u01B0\u1EE3c \u0111i\u1EC3m c\u1EE7a n\u0103ng l\u01B0\u1EE3ng h\u1EA1t nh\xE2n v\xE0 n\u0103ng l\u01B0\u1EE3ng m\u1EB7t tr\u1EDDi."', keywords: "gi\u1EA3 \u0111\u1ECBnh, so s\xE1nh, t\xECnh hu\u1ED1ng, n\u1EBFu...th\xEC, \u01B0u \u0111i\u1EC3m, nh\u01B0\u1EE3c \u0111i\u1EC3m, ph\xE2n t\xEDch, \u0111\xE1nh gi\xE1" },
|
|
102285
|
+
{ shortContextId: "__custom__", contextId: "__custom__", contextDescription: "Other (Custom Input)", contextExample: "", keywords: "custom, other, specific" }
|
|
102286
|
+
];
|
|
102287
|
+
|
|
102426
102288
|
// src/react-ui/components/authoring/AIFullQuizGeneratorModal.tsx
|
|
102427
102289
|
var availableQuestionTypesForFullQuiz = [
|
|
102428
102290
|
{ value: "true_false", label: "True/False" },
|