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