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