@thanh01.pmt/interactive-quiz-kit 1.0.65 → 1.0.66

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