@thanh01.pmt/interactive-quiz-kit 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { GoogleGenAI } from '@google/genai';
1
2
  import { z } from 'zod';
2
3
  import { genkit } from 'genkit';
3
4
  import { gemini20Flash, googleAI } from '@genkit-ai/googleai';
@@ -21,282 +22,520 @@ var __spreadValues = (a, b) => {
21
22
  return a;
22
23
  };
23
24
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __objRest = (source, exclude) => {
26
+ var target = {};
27
+ for (var prop in source)
28
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
29
+ target[prop] = source[prop];
30
+ if (source != null && __getOwnPropSymbols)
31
+ for (var prop of __getOwnPropSymbols(source)) {
32
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
33
+ target[prop] = source[prop];
34
+ }
35
+ return target;
36
+ };
24
37
 
25
38
  // src/utils/idGenerators.ts
26
39
  function generateUniqueId(prefix = "id_") {
27
40
  return prefix + Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
28
41
  }
42
+ var DebugLogger = class {
43
+ static formatTimestamp() {
44
+ return (/* @__PURE__ */ new Date()).toISOString();
45
+ }
46
+ static logPrompt(attempt, prompt, inputContext) {
47
+ console.log("\n" + "=".repeat(80));
48
+ console.log(`[${this.formatTimestamp()}] \u{1F50D} PROMPT DEBUG - Attempt ${attempt}`);
49
+ console.log("=".repeat(80));
50
+ console.log("Input Context:", JSON.stringify(inputContext, null, 2));
51
+ console.log("\n" + "-".repeat(40) + " FULL PROMPT " + "-".repeat(40));
52
+ console.log(prompt);
53
+ console.log("-".repeat(90));
54
+ console.log("Prompt Length:", prompt.length, "characters");
55
+ console.log("=".repeat(80) + "\n");
56
+ }
57
+ static logResponse(attempt, rawResponse) {
58
+ console.log("\n" + "=".repeat(80));
59
+ console.log(`[${this.formatTimestamp()}] \u{1F4DD} AI RESPONSE - Attempt ${attempt}`);
60
+ console.log("=".repeat(80));
61
+ console.log("Raw Response Length:", rawResponse.length, "characters");
62
+ console.log("\n" + "-".repeat(40) + " FULL RESPONSE " + "-".repeat(39));
63
+ console.log(rawResponse);
64
+ console.log("-".repeat(90));
65
+ console.log("=".repeat(80) + "\n");
66
+ }
67
+ static logValidation(attempt, step, data) {
68
+ console.log(`[${this.formatTimestamp()}] \u2705 VALIDATION - Attempt ${attempt} - ${step}`);
69
+ console.log(JSON.stringify(data, null, 2));
70
+ console.log("-".repeat(50));
71
+ }
72
+ static logRetryInfo(attempt, error, willRetry) {
73
+ console.log("\n" + "=".repeat(80));
74
+ console.log(`[${this.formatTimestamp()}] \u26A0\uFE0F RETRY INFO - Attempt ${attempt}`);
75
+ console.log("=".repeat(80));
76
+ console.log("Error Type:", error.constructor.name);
77
+ console.log("Error Message:", error.message);
78
+ console.log("Will Retry:", willRetry);
79
+ if (error.stack) {
80
+ console.log("Stack Trace:", error.stack);
81
+ }
82
+ console.log("=".repeat(80) + "\n");
83
+ }
84
+ static logAttemptSummary(attemptResults) {
85
+ console.log("\n" + "=".repeat(80));
86
+ console.log(`[${this.formatTimestamp()}] \u{1F4CA} ATTEMPT SUMMARY`);
87
+ console.log("=".repeat(80));
88
+ attemptResults.forEach((result, index) => {
89
+ console.log(`Attempt ${index + 1}:`);
90
+ console.log(` Status: ${result.success ? "\u2705 SUCCESS" : "\u274C FAILED"}`);
91
+ console.log(` Duration: ${result.duration}ms`);
92
+ console.log(` Error: ${result.error || "None"}`);
93
+ console.log(` Prompt Length: ${result.promptLength} chars`);
94
+ console.log(` Response Length: ${result.responseLength || 0} chars`);
95
+ console.log("");
96
+ });
97
+ console.log("=".repeat(80) + "\n");
98
+ }
99
+ };
29
100
 
30
- // src/ai/flows/generate-fitb-question.ts
31
- function extractJsonFromMarkdown(text) {
32
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
33
- return match ? match[1].trim() : text.trim();
101
+ // src/utils/aiUtils.ts
102
+ async function urlToGenerativePart(url, mimeType) {
103
+ const response = await fetch(url);
104
+ if (!response.ok) {
105
+ throw new Error(`Failed to fetch image from URL: ${url}. Status: ${response.status} ${response.statusText}`);
106
+ }
107
+ const buffer = await response.arrayBuffer();
108
+ return {
109
+ inlineData: {
110
+ data: Buffer.from(buffer).toString("base64"),
111
+ mimeType
112
+ }
113
+ };
34
114
  }
35
- z.object({
36
- topic: z.string().describe("The topic for the question."),
37
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
38
- // <-- ĐÃ THÊM
39
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
40
- numberOfBlanks: z.number().int().min(1).max(5).optional().default(1).describe("Number of blanks to include (1-5)."),
41
- isCaseSensitive: z.boolean().optional().default(false).describe("Whether answers should be case-sensitive."),
42
- contextDescription: z.string().optional().describe("A specific context or scenario for the question."),
43
- selectedContextId: z.string().optional().describe("The ID of the selected context.")
115
+ var QuizContextSchema = z.object({
116
+ plannedTopic: z.string().optional(),
117
+ plannedQuestionType: z.string().optional(),
118
+ plannedBloomLevel: z.string().optional(),
119
+ plannedContextId: z.string().optional(),
120
+ targetMisconception: z.string().optional(),
121
+ difficultyReason: z.string().optional(),
122
+ topicSpecificity: z.enum(["broad", "focused", "specific"]).optional(),
123
+ originalLoId: z.string().optional(),
124
+ originalSubject: z.string().optional(),
125
+ originalCategory: z.string().optional(),
126
+ originalTopic: z.string().optional(),
127
+ loDescription: z.string().optional().describe("The full description of the learning objective for deep context.")
44
128
  });
45
- z.object({
46
- prompt: z.string().describe("The overall instruction for the question."),
47
- sentenceWithPlaceholders: z.string().describe("The sentence containing placeholders like {{placeholder_name}}."),
48
- blanks: z.array(
49
- z.object({
50
- placeholder: z.string().describe("The placeholder name (without curly braces)."),
51
- acceptedAnswers: z.array(z.string().min(1)).min(1).describe("Array of acceptable answers.")
52
- })
53
- ).min(1),
54
- isCaseSensitive: z.boolean().optional(),
129
+ var BaseQuestionGenerationClientInputSchema = z.object({
130
+ language: z.string().optional().default("English"),
131
+ difficulty: z.enum(["easy", "medium", "hard"]),
132
+ quizContext: QuizContextSchema.optional(),
133
+ imageUrl: z.string().url().optional().describe("Optional URL of an image to be used as context.")
134
+ });
135
+ var BaseQuestionZodSchema = z.object({
136
+ id: z.string(),
137
+ prompt: z.string().min(1),
138
+ points: z.number().min(0).optional(),
55
139
  explanation: z.string().optional(),
56
- points: z.number().optional().default(10),
57
140
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
58
- topic: z.string().optional()
141
+ topic: z.string().optional(),
142
+ category: z.string().optional(),
143
+ subject: z.string().optional(),
144
+ learningObjective: z.string().optional(),
145
+ bloomLevel: z.string().optional(),
146
+ contextCode: z.string().optional(),
147
+ gradeBand: z.string().optional(),
148
+ course: z.string().optional(),
149
+ glossary: z.array(z.string()).optional()
59
150
  });
60
- var FillInTheBlanksQuestionZodSchema = z.object({
61
- id: z.string(),
151
+ var TrueFalseQuestionZodSchema = BaseQuestionZodSchema.extend({
152
+ questionType: z.literal("true_false"),
153
+ correctAnswer: z.boolean()
154
+ });
155
+ var MultipleChoiceQuestionZodSchema = BaseQuestionZodSchema.extend({
156
+ questionType: z.literal("multiple_choice"),
157
+ options: z.array(z.object({ id: z.string(), text: z.string().min(1) })).min(2),
158
+ correctAnswerId: z.string()
159
+ }).refine((data) => data.options.some((option) => option.id === data.correctAnswerId), {
160
+ message: "correctAnswerId must match one of the option IDs",
161
+ path: ["correctAnswerId"]
162
+ });
163
+ var MultipleResponseQuestionZodSchema = BaseQuestionZodSchema.extend({
164
+ questionType: z.literal("multiple_response"),
165
+ options: z.array(z.object({ id: z.string(), text: z.string().min(1) })).min(2),
166
+ correctAnswerIds: z.array(z.string()).min(1)
167
+ }).refine((data) => data.correctAnswerIds.every((id) => data.options.some((opt) => opt.id === id)), {
168
+ message: "All correctAnswerIds must exist in the options array",
169
+ path: ["correctAnswerIds"]
170
+ });
171
+ var ShortAnswerQuestionZodSchema = BaseQuestionZodSchema.extend({
172
+ questionType: z.literal("short_answer"),
173
+ acceptedAnswers: z.array(z.string().min(1)).min(1),
174
+ isCaseSensitive: z.boolean().optional()
175
+ });
176
+ var NumericQuestionZodSchema = BaseQuestionZodSchema.extend({
177
+ questionType: z.literal("numeric"),
178
+ answer: z.number(),
179
+ tolerance: z.number().min(0).optional()
180
+ });
181
+ var FillInTheBlanksQuestionZodSchema = BaseQuestionZodSchema.extend({
62
182
  questionType: z.literal("fill_in_the_blanks"),
63
- prompt: z.string().min(1),
64
183
  segments: z.array(z.object({
65
184
  type: z.enum(["text", "blank"]),
66
185
  content: z.string().optional(),
67
- // Only for 'text' type
68
186
  id: z.string().optional()
69
- // Only for 'blank' type
70
187
  })).min(1),
71
188
  answers: z.array(z.object({
72
189
  blankId: z.string(),
73
190
  acceptedValues: z.array(z.string().min(1)).min(1)
74
191
  })).min(1),
75
- isCaseSensitive: z.boolean().optional(),
76
- points: z.number().min(0).optional(),
77
- explanation: z.string().optional()
78
- // ... other fields
192
+ isCaseSensitive: z.boolean().optional()
79
193
  }).refine((data) => {
80
- const segmentBlankIds = new Set(data.segments.filter((s) => s.type === "blank").map((s) => s.id));
194
+ const segmentBlankIds = new Set(data.segments.filter((s) => s.type === "blank" && s.id).map((s) => s.id));
81
195
  const answerBlankIds = new Set(data.answers.map((a) => a.blankId));
82
- if (segmentBlankIds.size !== answerBlankIds.size) return false;
83
- for (const id of segmentBlankIds) {
84
- if (!answerBlankIds.has(id || "")) return false;
85
- }
86
- return true;
87
- }, {
88
- message: "There must be a 1-to-1 correspondence between blank segments and answer definitions.",
89
- path: ["answers"]
196
+ return segmentBlankIds.size === answerBlankIds.size && [...segmentBlankIds].every((id) => answerBlankIds.has(id));
197
+ }, { message: "Mismatch between defined blanks in segments and their answers.", path: ["answers"] });
198
+ var SequenceQuestionZodSchema = BaseQuestionZodSchema.extend({
199
+ questionType: z.literal("sequence"),
200
+ items: z.array(z.object({ id: z.string(), content: z.string().min(1) })).min(2),
201
+ correctOrder: z.array(z.string()).min(2)
202
+ }).refine((data) => new Set(data.correctOrder).size === data.items.length, {
203
+ message: "correctOrder must contain all unique item IDs exactly once.",
204
+ path: ["correctOrder"]
90
205
  });
91
- z.object({
92
- question: FillInTheBlanksQuestionZodSchema.optional().describe("The generated question.")
206
+ var MatchingQuestionZodSchema = BaseQuestionZodSchema.extend({
207
+ questionType: z.literal("matching"),
208
+ prompts: z.array(z.object({ id: z.string(), content: z.string().min(1) })).min(2),
209
+ options: z.array(z.object({ id: z.string(), content: z.string().min(1) })).min(2),
210
+ correctAnswerMap: z.array(z.object({ promptId: z.string(), optionId: z.string() })).min(2),
211
+ shuffleOptions: z.boolean().optional()
212
+ }).refine((data) => data.prompts.length === data.correctAnswerMap.length, {
213
+ message: "Each prompt must have exactly one corresponding answer in the map.",
214
+ path: ["correctAnswerMap"]
215
+ });
216
+ var CodingQuestionZodSchema = BaseQuestionZodSchema.extend({
217
+ questionType: z.literal("coding"),
218
+ codingLanguage: z.enum(["cpp", "javascript", "python", "swift", "csharp"]),
219
+ functionSignature: z.string().optional(),
220
+ solutionCode: z.string(),
221
+ testCases: z.array(z.object({
222
+ id: z.string(),
223
+ input: z.array(z.any()),
224
+ expectedOutput: z.any(),
225
+ isPublic: z.boolean()
226
+ })).min(1)
93
227
  });
94
- async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
95
- var _a;
96
- try {
97
- const ai = genkit({
98
- plugins: [googleAI({ apiKey })],
99
- model: gemini20Flash
100
- });
101
- const promptText = `You are an expert quiz question writer.
102
- Generate a single Fill-In-The-Blanks question in ${clientInput.language} with approximately ${clientInput.numberOfBlanks} blank(s).
103
228
 
104
- IMPORTANT: Return the response as JSON with this EXACT format:
105
- {
106
- "prompt": "Complete the famous saying.",
107
- "sentenceWithPlaceholders": "Roses are {{color1}}, violets are {{color2}}.",
108
- "blanks": [
109
- { "placeholder": "color1", "acceptedAnswers": ["red"] },
110
- { "placeholder": "color2", "acceptedAnswers": ["blue"] }
111
- ],
112
- "isCaseSensitive": false,
113
- "explanation": "This is a classic nursery rhyme.",
114
- "points": 10,
115
- "difficulty": "easy",
116
- "topic": "Nursery Rhymes"
117
- }
229
+ // src/ai/flows/question-gen/generate-fitb-question-types.ts
230
+ BaseQuestionGenerationClientInputSchema.extend({
231
+ numberOfBlanks: z.number().int().min(1).max(5).optional().default(1),
232
+ isCaseSensitive: z.boolean().optional().default(false)
233
+ });
234
+ var AIFillInTheBlanksOutputFieldsSchema = z.object({
235
+ prompt: z.string().describe("The instructional text for the user, e.g., 'Fill in the blanks to complete the sentence.'"),
236
+ // Yêu cầu AI trả về cấu trúc segments trực tiếp
237
+ segments: z.array(z.object({
238
+ type: z.enum(["text", "blank"]),
239
+ content: z.string().optional().describe("The text content for a 'text' segment."),
240
+ acceptedAnswers: z.array(z.string().min(1)).min(1).optional().describe("An array of correct answers for a 'blank' segment.")
241
+ })).min(1).describe("An array of text and blank segments representing the question."),
242
+ explanation: z.string().optional(),
243
+ points: z.number().optional().default(10),
244
+ difficulty: z.enum(["easy", "medium", "hard"]).optional(),
245
+ topic: z.string().optional(),
246
+ verifiedCategory: z.string().optional()
247
+ // Thêm để xác thực
248
+ });
118
249
 
119
- Requirements:
120
- - Use double curly braces for placeholders, like {{placeholder_name}}.
121
- - Each placeholder name inside the braces must be unique.
122
- - The 'blanks' array must define the accepted answers for every unique placeholder in the sentence.
123
- - The 'placeholder' value in the 'blanks' array should NOT include the curly braces.
124
- - The content of 'prompt', 'sentenceWithPlaceholders', 'blanks.acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
250
+ // src/ai/flows/question-gen/generate-fitb-question.ts
251
+ var MAX_RETRY_ATTEMPTS = 3;
252
+ var RETRY_DELAY_MS = 3e3;
253
+ function buildEnhancedPrompt(clientInput, attemptNumber) {
254
+ const { quizContext, language, difficulty, numberOfBlanks, imageUrl } = clientInput;
255
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
256
+ const attemptInfo = attemptNumber > 1 ? `
257
+ ## DEBUG INFO - This is attempt #${attemptNumber}
258
+ 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'.
125
259
 
126
- Topic: ${clientInput.topic}
127
- Language: ${clientInput.language}
128
- Difficulty: ${clientInput.difficulty}
129
- Target Number of Blanks: ${clientInput.numberOfBlanks}
130
- Case Sensitive: ${clientInput.isCaseSensitive}
260
+ ` : "";
261
+ 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.` : "";
262
+ const contextStrings = [
263
+ `**Required Category:** ${category}`,
264
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
265
+ imageContextInstruction,
266
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
267
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** Design the blank to test this specific point: "${quizContext.targetMisconception}"`
268
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
269
+ const exampleJson = JSON.stringify({
270
+ prompt: "Complete the following Swift code snippet.",
271
+ segments: [
272
+ { "type": "text", "content": "To declare a new function in Swift, you use the `" },
273
+ { "type": "blank", "acceptedAnswers": ["func"] },
274
+ { "type": "text", "content": "` keyword." }
275
+ ],
276
+ explanation: "The 'func' keyword is used to declare a function in the Swift programming language.",
277
+ points: 10,
278
+ difficulty: "easy",
279
+ topic: "Swift Function Declaration",
280
+ verifiedCategory: category
281
+ }, null, 2);
282
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
283
+ Your mission is to create a high-quality, technically accurate Fill-in-the-Blanks Question.
131
284
 
132
- Return only the JSON response.`;
133
- const response = await ai.generate(promptText);
134
- const rawText = response.text;
135
- const jsonText = extractJsonFromMarkdown(rawText);
136
- console.log("AI Response:", jsonText);
137
- const aiGeneratedContent = JSON.parse(jsonText);
138
- if (aiGeneratedContent) {
139
- const segments = [];
140
- const answers = [];
141
- const placeholderToBlankIdMap = {};
142
- aiGeneratedContent.blanks.forEach((blankInfo) => {
143
- const blankId = generateUniqueId("blank_");
144
- placeholderToBlankIdMap[blankInfo.placeholder] = blankId;
145
- answers.push({
146
- blankId,
147
- acceptedValues: blankInfo.acceptedAnswers
148
- });
149
- });
150
- const placeholderRegex = /\{\{([^}]+)\}\}/g;
151
- let lastIndex = 0;
152
- let match;
153
- while ((match = placeholderRegex.exec(aiGeneratedContent.sentenceWithPlaceholders)) !== null) {
154
- const placeholderName = match[1];
155
- const blankId = placeholderToBlankIdMap[placeholderName];
156
- if (match.index > lastIndex) {
157
- segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex, match.index) });
285
+ ## Core Rules (Non-negotiable)
286
+ 1. **Category Purity:** The question MUST be exclusively about **${category}**.
287
+ 2. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
288
+ 3. **Logical Segments:** For 'blank' segments, you MUST provide 'acceptedAnswers'. For 'text' segments, you MUST provide 'content'. Do not mix them.
289
+
290
+ ## CRITICAL CONTEXT FOR THIS QUESTION
291
+ ${contextStrings}
292
+
293
+ ## Task: Generate the Question
294
+ Based on all the rules and context above, generate a single Fill-in-the-Blanks Question.
295
+
296
+ ### Input Parameters
297
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
298
+ - **Language for Text:** ${language}
299
+ - **Difficulty Level:** ${difficulty}
300
+ - **Number of Blanks:** Generate exactly ${numberOfBlanks} segment(s) with type 'blank'.
301
+
302
+ ### Required JSON Output Format
303
+ Your response must be ONLY the JSON object, matching this exact structure:
304
+
305
+ ${exampleJson}
306
+
307
+ Now, generate the JSON for the requested question.`;
308
+ }
309
+ async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
310
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
311
+ const ai = new GoogleGenAI({ apiKey });
312
+ const model = "gemini-2.5-flash";
313
+ const config = {
314
+ temperature: 0.8,
315
+ responseMimeType: "application/json",
316
+ thinkingConfig: {
317
+ thinkingBudget: 4e3
318
+ }
319
+ };
320
+ const attemptResults = [];
321
+ let lastError = null;
322
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
323
+ const startTime = Date.now();
324
+ const promptText = buildEnhancedPrompt(clientInput, attempt);
325
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
326
+ try {
327
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
328
+ const parts = [{ text: promptText }];
329
+ if (clientInput.imageUrl) {
330
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
331
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
332
+ parts.unshift(imagePart);
333
+ }
334
+ const contents = [{ role: "user", parts }];
335
+ const aiResult = await ai.models.generateContent({ model, config, contents });
336
+ const response = aiResult;
337
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
338
+ const duration = Date.now() - startTime;
339
+ DebugLogger.logResponse(attempt, rawText);
340
+ if (!rawText) throw new Error("AI returned an empty response.");
341
+ const parsedJson = JSON.parse(rawText);
342
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
343
+ const aiGeneratedContent = AIFillInTheBlanksOutputFieldsSchema.parse(parsedJson);
344
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
345
+ const blankCount = aiGeneratedContent.segments.filter((s) => s.type === "blank").length;
346
+ if (blankCount !== clientInput.numberOfBlanks) {
347
+ throw new Error(`AI generated ${blankCount} blanks, but ${clientInput.numberOfBlanks} were required.`);
348
+ }
349
+ aiGeneratedContent.segments.forEach((segment, index) => {
350
+ if (segment.type === "blank" && (!segment.acceptedAnswers || segment.acceptedAnswers.length === 0)) {
351
+ throw new Error(`Segment ${index} is a 'blank' but is missing 'acceptedAnswers'.`);
158
352
  }
159
- if (blankId) {
160
- segments.push({ type: "blank", id: blankId });
161
- } else {
162
- console.warn(`Placeholder {{${placeholderName}}} found in sentence but not defined in blanks array. Treating as literal text.`);
163
- segments.push({ type: "text", content: match[0] });
353
+ if (segment.type === "text" && typeof segment.content !== "string") {
354
+ throw new Error(`Segment ${index} is 'text' but is missing 'content'.`);
355
+ }
356
+ });
357
+ if ((_f = clientInput.quizContext) == null ? void 0 : _f.originalCategory) {
358
+ const verifiedCategory = (_g = aiGeneratedContent.verifiedCategory) == null ? void 0 : _g.toLowerCase();
359
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
360
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
361
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
164
362
  }
165
- lastIndex = placeholderRegex.lastIndex;
166
- }
167
- if (lastIndex < aiGeneratedContent.sentenceWithPlaceholders.length) {
168
- segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex) });
169
363
  }
364
+ const finalSegments = [];
365
+ const finalAnswers = [];
366
+ aiGeneratedContent.segments.forEach((segment) => {
367
+ if (segment.type === "text") {
368
+ finalSegments.push({ type: "text", content: segment.content });
369
+ } else if (segment.type === "blank" && segment.acceptedAnswers) {
370
+ const blankId = generateUniqueId("blank_");
371
+ finalSegments.push({ type: "blank", id: blankId });
372
+ finalAnswers.push({ blankId, acceptedValues: segment.acceptedAnswers });
373
+ }
374
+ });
170
375
  const completeQuestion = {
171
376
  id: generateUniqueId("fitb_ai_"),
172
377
  questionType: "fill_in_the_blanks",
173
378
  prompt: aiGeneratedContent.prompt,
174
- segments,
175
- answers,
176
- isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
379
+ segments: finalSegments,
380
+ answers: finalAnswers,
381
+ isCaseSensitive: clientInput.isCaseSensitive,
177
382
  explanation: aiGeneratedContent.explanation,
178
383
  points: aiGeneratedContent.points,
179
- topic: aiGeneratedContent.topic || clientInput.topic,
180
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
181
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
384
+ topic: aiGeneratedContent.topic || ((_h = clientInput.quizContext) == null ? void 0 : _h.originalTopic),
385
+ difficulty: clientInput.difficulty,
386
+ contextCode: (_i = clientInput.quizContext) == null ? void 0 : _i.plannedContextId,
387
+ bloomLevel: (_j = clientInput.quizContext) == null ? void 0 : _j.plannedBloomLevel,
388
+ learningObjective: (_k = clientInput.quizContext) == null ? void 0 : _k.originalLoId,
389
+ subject: (_l = clientInput.quizContext) == null ? void 0 : _l.originalSubject,
390
+ category: (_m = clientInput.quizContext) == null ? void 0 : _m.originalCategory,
391
+ imageUrl: clientInput.imageUrl
182
392
  };
183
- try {
184
- const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
185
- return { question: validatedQuestion };
186
- } catch (validationError) {
187
- console.error("Question validation failed:", validationError);
188
- throw new Error(`Generated question failed validation: ${validationError}`);
393
+ const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
394
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
395
+ console.log(`
396
+ \u2705 FITB generation successful on attempt ${attempt} (${duration}ms)`);
397
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
398
+ return { question: validatedQuestion };
399
+ } catch (error) {
400
+ lastError = error;
401
+ const duration = Date.now() - startTime;
402
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
403
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS;
404
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
405
+ if (willRetry) {
406
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS}ms...`);
407
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
189
408
  }
190
- } else {
191
- throw new Error("AI did not return content for the Fill-In-The-Blanks question.");
192
409
  }
193
- } catch (error) {
194
- console.error("Error generating Fill-In-The-Blanks question:", error);
195
- throw new Error(`Failed to generate Fill-In-The-Blanks question: ${error.message}`);
196
410
  }
411
+ DebugLogger.logAttemptSummary(attemptResults);
412
+ const errorMessage = `Failed to generate FITB question after ${MAX_RETRY_ATTEMPTS} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
413
+ console.error("\n\u274C Final Result: FAILED");
414
+ console.error(errorMessage);
415
+ return { error: errorMessage };
197
416
  }
198
- function extractJsonFromMarkdown2(text) {
199
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
200
- return match ? match[1].trim() : text.trim();
201
- }
202
- z.object({
203
- topic: z.string().describe("The topic for the question."),
204
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
205
- // <-- ĐÃ THÊM
206
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
207
- numberOfPairs: z.number().int().min(2).max(8).optional().default(4).describe("Number of pairs to match (2-8)."),
208
- shuffleOptions: z.boolean().optional().default(true).describe("Whether the options should be shuffled."),
209
- contextDescription: z.string().optional().describe("A specific context or scenario for the question."),
210
- selectedContextId: z.string().optional().describe("The ID of the selected context.")
417
+ BaseQuestionGenerationClientInputSchema.extend({
418
+ numberOfPairs: z.number().int().min(2).max(8).optional().default(4),
419
+ shuffleOptions: z.boolean().optional().default(true)
211
420
  });
212
- z.object({
213
- prompt: z.string().describe("The overall instruction (e.g., 'Match the terms to their definitions.')."),
214
- correctPairs: z.array(
215
- z.object({
216
- promptText: z.string().min(1).describe("The text for a prompt item (e.g., a term)."),
217
- optionText: z.string().min(1).describe("The text for the corresponding option item (e.g., its definition).")
218
- })
219
- ).min(2),
421
+ var AIMatchingOutputFieldsSchema = z.object({
422
+ prompt: z.string().describe("The instructional text for the user, e.g., 'Match the concept to its definition.'"),
423
+ correctPairs: z.array(z.object({
424
+ promptText: z.string().min(1).describe("The text for the left-hand side item (the prompt)."),
425
+ optionText: z.string().min(1).describe("The text for the right-hand side item (the matching option).")
426
+ })).min(2),
220
427
  explanation: z.string().optional(),
221
428
  points: z.number().optional().default(10),
222
429
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
223
- topic: z.string().optional()
224
- });
225
- var MatchingQuestionZodSchema = z.object({
226
- id: z.string(),
227
- questionType: z.literal("matching"),
228
- prompt: z.string().min(1),
229
- prompts: z.array(z.object({ id: z.string(), content: z.string().min(1) })).min(2),
230
- options: z.array(z.object({ id: z.string(), content: z.string().min(1) })).min(2),
231
- correctAnswerMap: z.array(z.object({ promptId: z.string(), optionId: z.string() })).min(2),
232
- shuffleOptions: z.boolean().optional(),
233
- points: z.number().min(0).optional(),
234
- explanation: z.string().optional()
235
- // ... other fields
236
- }).refine((data) => {
237
- const promptIds = new Set(data.prompts.map((p) => p.id));
238
- const optionIds = new Set(data.options.map((o) => o.id));
239
- return data.correctAnswerMap.every(
240
- (map) => promptIds.has(map.promptId) && optionIds.has(map.optionId)
241
- );
242
- }, {
243
- message: "All IDs in correctAnswerMap must exist in the prompts and options arrays.",
244
- path: ["correctAnswerMap"]
245
- }).refine((data) => {
246
- const mappedPromptIds = new Set(data.correctAnswerMap.map((m) => m.promptId));
247
- return mappedPromptIds.size === data.prompts.length && data.correctAnswerMap.length === data.prompts.length;
248
- }, {
249
- message: "Each prompt must be mapped exactly once in the correctAnswerMap.",
250
- path: ["correctAnswerMap"]
251
- });
252
- z.object({
253
- question: MatchingQuestionZodSchema.optional().describe("The generated Matching question.")
430
+ topic: z.string().optional(),
431
+ verifiedCategory: z.string().optional()
432
+ // Thêm để xác thực
254
433
  });
255
- async function generateMatchingQuestion(clientInput, apiKey) {
256
- try {
257
- const ai = genkit({
258
- plugins: [googleAI({ apiKey })],
259
- model: gemini20Flash
260
- });
261
- const promptText = `You are an expert quiz question writer.
262
- Generate a single Matching question in ${clientInput.language} with exactly ${clientInput.numberOfPairs} correct pairs.
263
434
 
264
- IMPORTANT: Return the response as JSON with this EXACT format:
265
- {
266
- "prompt": "Match each country to its capital city.",
267
- "correctPairs": [
268
- { "promptText": "France", "optionText": "Paris" },
269
- { "promptText": "Japan", "optionText": "Tokyo" },
270
- { "promptText": "Egypt", "optionText": "Cairo" },
271
- { "promptText": "Brazil", "optionText": "Bras\xEDlia" }
272
- ],
273
- "explanation": "These are the capital cities for the respective countries.",
274
- "points": 10,
275
- "difficulty": "medium",
276
- "topic": "World Geography"
277
- }
435
+ // src/ai/flows/question-gen/generate-matching-question.ts
436
+ var MAX_RETRY_ATTEMPTS2 = 3;
437
+ var RETRY_DELAY_MS2 = 3e3;
438
+ function buildEnhancedPrompt2(clientInput, attemptNumber) {
439
+ const { quizContext, language, difficulty, numberOfPairs, imageUrl } = clientInput;
440
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
441
+ const attemptInfo = attemptNumber > 1 ? `
442
+ ## DEBUG INFO - This is attempt #${attemptNumber}
443
+ Previous attempts failed. Please ensure the 'correctPairs' array has exactly the required number of items and the JSON is valid.
278
444
 
279
- Requirements:
280
- - The 'correctPairs' array must contain exactly ${clientInput.numberOfPairs} objects.
281
- - Each object in 'correctPairs' must have a 'promptText' and its corresponding 'optionText'.
282
- - Ensure the content of all 'promptText' values are unique.
283
- - Ensure the content of all 'optionText' values are unique.
284
- - The content of 'prompt', 'correctPairs.promptText', 'correctPairs.optionText', 'explanation', and 'topic' must be in ${clientInput.language}.
445
+ ` : "";
446
+ const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The matching pairs must be directly related to the content of this image.` : "";
447
+ const contextStrings = [
448
+ `**Required Category:** ${category}`,
449
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
450
+ imageContextInstruction,
451
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
452
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** Design a pair that specifically tests this confusion: "${quizContext.targetMisconception}"`
453
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
454
+ const exampleJson = JSON.stringify({
455
+ prompt: "Match each Swift collection type to its primary characteristic.",
456
+ correctPairs: [
457
+ { "promptText": "Array", "optionText": "An ordered, random-access collection." },
458
+ { "promptText": "Set", "optionText": "An unordered collection of unique elements." },
459
+ { "promptText": "Dictionary", "optionText": "An unordered collection of key-value associations." }
460
+ ],
461
+ explanation: "These are the fundamental characteristics of Swift's main collection types.",
462
+ points: 10,
463
+ difficulty: "easy",
464
+ topic: "Swift Collection Types",
465
+ verifiedCategory: category
466
+ }, null, 2);
467
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
468
+ Your mission is to create a high-quality, technically accurate Matching Question.
285
469
 
286
- Topic: ${clientInput.topic}
287
- Language: ${clientInput.language}
288
- Difficulty: ${clientInput.difficulty}
289
- Number of Pairs: ${clientInput.numberOfPairs}
470
+ ## Core Rules (Non-negotiable)
471
+ 1. **Category Purity:** The question MUST be exclusively about **${category}**.
472
+ 2. **Logical Pairs:** The items to be matched must have a clear, one-to-one relationship.
473
+ 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
290
474
 
291
- Return only the JSON response.`;
292
- const response = await ai.generate(promptText);
293
- const rawText = response.text;
294
- const jsonText = extractJsonFromMarkdown2(rawText);
295
- console.log("AI Response:", jsonText);
296
- const aiGeneratedContent = JSON.parse(jsonText);
297
- if (aiGeneratedContent) {
475
+ ## CRITICAL CONTEXT FOR THIS QUESTION
476
+ ${contextStrings}
477
+
478
+ ## Task: Generate the Question
479
+ Based on all the rules and context above, generate a single Matching Question.
480
+
481
+ ### Input Parameters
482
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
483
+ - **Language for Text:** ${language}
484
+ - **Difficulty Level:** ${difficulty}
485
+ - **Number of Pairs:** Generate exactly ${numberOfPairs} correct pairs in the 'correctPairs' array.
486
+
487
+ ### Required JSON Output Format
488
+ Your response must be ONLY the JSON object, matching this exact structure:
489
+
490
+ ${exampleJson}
491
+
492
+ Now, generate the JSON for the requested question.`;
493
+ }
494
+ async function generateMatchingQuestion(clientInput, apiKey) {
495
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
496
+ const ai = new GoogleGenAI({ apiKey });
497
+ const model = "gemini-2.5-flash";
498
+ const config = {
499
+ temperature: 0.8,
500
+ responseMimeType: "application/json",
501
+ thinkingConfig: {
502
+ thinkingBudget: 4e3
503
+ }
504
+ };
505
+ const attemptResults = [];
506
+ let lastError = null;
507
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS2; attempt++) {
508
+ const startTime = Date.now();
509
+ const promptText = buildEnhancedPrompt2(clientInput, attempt);
510
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
511
+ try {
512
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
513
+ const parts = [{ text: promptText }];
514
+ if (clientInput.imageUrl) {
515
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
516
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
517
+ parts.unshift(imagePart);
518
+ }
519
+ const contents = [{ role: "user", parts }];
520
+ const aiResult = await ai.models.generateContent({ model, config, contents });
521
+ const response = aiResult;
522
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
523
+ const duration = Date.now() - startTime;
524
+ DebugLogger.logResponse(attempt, rawText);
525
+ if (!rawText) throw new Error("AI returned an empty response.");
526
+ const parsedJson = JSON.parse(rawText);
527
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
528
+ const aiGeneratedContent = AIMatchingOutputFieldsSchema.parse(parsedJson);
529
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
298
530
  if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
299
- throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were requested.`);
531
+ throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were required.`);
532
+ }
533
+ if ((_f = clientInput.quizContext) == null ? void 0 : _f.originalCategory) {
534
+ const verifiedCategory = (_g = aiGeneratedContent.verifiedCategory) == null ? void 0 : _g.toLowerCase();
535
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
536
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
537
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
538
+ }
300
539
  }
301
540
  const finalPrompts = [];
302
541
  const finalOptions = [];
@@ -318,38 +557,43 @@ Return only the JSON response.`;
318
557
  shuffleOptions: clientInput.shuffleOptions,
319
558
  explanation: aiGeneratedContent.explanation,
320
559
  points: aiGeneratedContent.points,
321
- topic: aiGeneratedContent.topic || clientInput.topic,
322
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
323
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
560
+ topic: aiGeneratedContent.topic || ((_h = clientInput.quizContext) == null ? void 0 : _h.originalTopic),
561
+ difficulty: clientInput.difficulty,
562
+ contextCode: (_i = clientInput.quizContext) == null ? void 0 : _i.plannedContextId,
563
+ bloomLevel: (_j = clientInput.quizContext) == null ? void 0 : _j.plannedBloomLevel,
564
+ learningObjective: (_k = clientInput.quizContext) == null ? void 0 : _k.originalLoId,
565
+ subject: (_l = clientInput.quizContext) == null ? void 0 : _l.originalSubject,
566
+ category: (_m = clientInput.quizContext) == null ? void 0 : _m.originalCategory,
567
+ imageUrl: clientInput.imageUrl
324
568
  };
325
- try {
326
- const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
327
- return { question: validatedQuestion };
328
- } catch (validationError) {
329
- console.error("Question validation failed:", validationError);
330
- throw new Error(`Generated question failed validation: ${validationError}`);
569
+ const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
570
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
571
+ console.log(`
572
+ \u2705 Matching generation successful on attempt ${attempt} (${duration}ms)`);
573
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
574
+ return { question: validatedQuestion };
575
+ } catch (error) {
576
+ lastError = error;
577
+ const duration = Date.now() - startTime;
578
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
579
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS2;
580
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
581
+ if (willRetry) {
582
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS2}ms...`);
583
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS2));
331
584
  }
332
- } else {
333
- throw new Error("AI did not return content for the Matching question.");
334
585
  }
335
- } catch (error) {
336
- console.error("Error generating Matching question:", error);
337
- throw new Error(`Failed to generate Matching question: ${error.message}`);
338
586
  }
587
+ DebugLogger.logAttemptSummary(attemptResults);
588
+ const errorMessage = `Failed to generate Matching question after ${MAX_RETRY_ATTEMPTS2} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
589
+ console.error("\n\u274C Final Result: FAILED");
590
+ console.error(errorMessage);
591
+ return { error: errorMessage };
339
592
  }
340
- function extractJsonFromMarkdown3(text) {
341
- return text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
342
- }
343
- z.object({
344
- topic: z.string().describe("The topic for the question."),
345
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
346
- // <-- ĐÃ THÊM
347
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
348
- numberOfOptions: z.number().int().min(2).max(6).optional().default(4).describe("Number of answer options to generate (2-6)."),
349
- contextDescription: z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
350
- selectedContextId: z.string().optional().describe("The ID of the selected context, if any.")
593
+ BaseQuestionGenerationClientInputSchema.extend({
594
+ numberOfOptions: z.number().int().min(2).max(6).optional().default(4)
351
595
  });
352
- z.object({
596
+ var AIMCQOutputFieldsSchema = z.object({
353
597
  prompt: z.string().describe("The question statement itself."),
354
598
  options: z.array(
355
599
  z.object({
@@ -359,99 +603,123 @@ z.object({
359
603
  ).min(2).max(6),
360
604
  correctTempOptionId: z.string().describe("The temporary ID of the correct option from the generated options array."),
361
605
  explanation: z.string().optional().describe("A brief explanation of why the answer is correct."),
362
- points: z.number().optional().default(10).describe("Points for a correct answer."),
363
- difficulty: z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
364
- topic: z.string().optional().describe("Refined topic.")
365
- });
366
- var MultipleChoiceQuestionZodSchema = z.object({
367
- id: z.string(),
368
- questionType: z.literal("multiple_choice"),
369
- prompt: z.string().min(1),
370
- options: z.array(z.object({ id: z.string(), text: z.string().min(1) })).min(2).max(10),
371
- correctAnswerId: z.string(),
372
- points: z.number().min(0).optional(),
373
- explanation: z.string().optional(),
374
- learningObjective: z.string().optional(),
375
- glossary: z.array(z.string()).optional(),
376
- bloomLevel: z.string().optional(),
606
+ points: z.number().optional().default(10),
377
607
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
378
- contextCode: z.string().optional(),
379
- gradeBand: z.string().optional(),
380
- course: z.string().optional(),
381
- category: z.string().optional(),
382
- topic: z.string().optional()
383
- }).refine((data) => {
384
- return data.options.some((option) => option.id === data.correctAnswerId);
385
- }, {
386
- message: "correctAnswerId must match one of the option IDs",
387
- path: ["correctAnswerId"]
388
- }).refine((data) => {
389
- const optionIds = data.options.map((opt) => opt.id);
390
- return optionIds.length === new Set(optionIds).size;
391
- }, {
392
- message: "All option IDs must be unique",
393
- path: ["options"]
394
- });
395
- z.object({
396
- question: MultipleChoiceQuestionZodSchema.optional().describe("The generated Multiple Choice question.")
608
+ topic: z.string().optional(),
609
+ verifiedCategory: z.string().optional().describe("The category this question actually addresses.")
397
610
  });
398
- async function generateMCQQuestion(clientInput, apiKey) {
399
- try {
400
- const ai = genkit({
401
- plugins: [googleAI({ apiKey })],
402
- model: gemini20Flash
403
- });
404
- const promptText = `You are an expert quiz question writer.
405
- Generate a single Multiple Choice question in ${clientInput.language} based on the following inputs.
406
611
 
407
- IMPORTANT: Return the response as JSON with this EXACT format:
408
- {
409
- "prompt": "Your question here",
410
- "options": [
411
- { "tempId": "A", "text": "First option text" },
412
- { "tempId": "B", "text": "Second option text" },
413
- { "tempId": "C", "text": "Third option text" },
414
- { "tempId": "D", "text": "Fourth option text" }
415
- ],
416
- "correctTempOptionId": "C",
417
- "explanation": "Brief explanation",
418
- "points": 10,
419
- "difficulty": "medium",
420
- "topic": "refined topic"
421
- }
612
+ // src/ai/flows/question-gen/generate-mcq-question.ts
613
+ var MAX_RETRY_ATTEMPTS3 = 3;
614
+ var RETRY_DELAY_MS3 = 3e3;
615
+ function buildEnhancedPrompt3(clientInput, attemptNumber) {
616
+ const { quizContext, language, difficulty, numberOfOptions, imageUrl } = clientInput;
617
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
618
+ const attemptInfo = attemptNumber > 1 ? `
619
+ ## DEBUG INFO - This is attempt #${attemptNumber}
620
+ Previous attempts failed...
422
621
 
423
- Requirements:
424
- - Generate exactly ${clientInput.numberOfOptions} options.
425
- - Use tempId values like "A", "B", "C", "D" or "option_1", "option_2", etc.
426
- - Make sure correctTempOptionId matches one of the tempId values in options array.
427
- - Each option must have both "tempId" and "text" fields.
428
- - The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
622
+ ` : "";
623
+ 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.` : "";
624
+ const contextStrings = [
625
+ `**Required Category:** ${category}`,
626
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
627
+ imageContextInstruction,
628
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
629
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** Use this to create plausible incorrect answers: "${quizContext.targetMisconception}"`,
630
+ (quizContext == null ? void 0 : quizContext.difficultyReason) && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
631
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
632
+ const exampleJson = JSON.stringify({
633
+ prompt: `In ${category}, what is the primary purpose of the 'guard' statement?`,
634
+ options: [
635
+ { tempId: "A", text: "To execute a block of code repeatedly." },
636
+ { tempId: "B", text: "To define a new custom data type." },
637
+ { tempId: "C", text: "To exit a scope early if a condition is not met." },
638
+ { tempId: "D", text: "To handle errors thrown by a function." }
639
+ ],
640
+ correctTempOptionId: "C",
641
+ explanation: `The 'guard' statement in ${category} provides an early exit from a scope (like a function) if a condition is false, enhancing code readability by handling required conditions upfront.`,
642
+ points: 10,
643
+ difficulty: "easy",
644
+ topic: `Control Flow in ${category}`,
645
+ verifiedCategory: category
646
+ }, null, 2);
647
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in the programming language: ${category}.
648
+ Your sole mission is to create a high-quality, technically accurate Multiple Choice Question. You must adhere to the following rules at all times.
429
649
 
430
- Topic: ${clientInput.topic}
431
- Language: ${clientInput.language}
432
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
433
- Difficulty: ${clientInput.difficulty}
434
- Number of Options: ${clientInput.numberOfOptions}
650
+ ## Core Rules (Non-negotiable)
651
+ 1. **Category Purity:** The question, options, and explanation MUST be exclusively about **${category}**. Do NOT mention or use syntax from other languages.
652
+ 2. **Context Adherence:** The question's content must directly align with all provided context.
653
+ 3. **Format Integrity:** You MUST return ONLY a single, valid JSON object that strictly follows the provided schema. Do not include any extra text, comments, or markdown formatting.
435
654
 
436
- Return only the JSON response.`;
437
- const response = await ai.generate(promptText);
438
- const rawText = response.text;
439
- const jsonText = extractJsonFromMarkdown3(rawText);
440
- console.log("AI Response:", jsonText);
441
- let aiGeneratedContent = JSON.parse(jsonText);
442
- if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
443
- const normalizedOptions = aiGeneratedContent.options.map((option, index) => {
444
- if (typeof option === "object" && !option.tempId) {
445
- const key = Object.keys(option)[0];
446
- const text = option[key];
447
- return { tempId: key, text };
448
- }
449
- return option;
450
- });
451
- aiGeneratedContent.options = normalizedOptions;
655
+ ## CRITICAL CONTEXT FOR THIS QUESTION
656
+ ${contextStrings}
657
+
658
+ ## Task: Generate the Question
659
+ Based on all the rules and context above, generate a single Multiple Choice Question.
660
+
661
+ ### Input Parameters
662
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
663
+ - **Language for Text:** ${language}
664
+ - **Difficulty Level:** ${difficulty}
665
+ - **Number of Options:** ${numberOfOptions}
666
+
667
+ ### Required JSON Output Format
668
+ Your response must be ONLY the JSON object, matching this exact structure and field names.
669
+
670
+ ${exampleJson}
671
+
672
+ Now, generate the JSON for the requested question.`;
673
+ }
674
+ async function generateMCQQuestion(clientInput, apiKey) {
675
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
676
+ const ai = new GoogleGenAI({ apiKey });
677
+ const model = "gemini-2.5-flash";
678
+ const config = {
679
+ temperature: 0.8,
680
+ responseMimeType: "application/json",
681
+ thinkingConfig: {
682
+ thinkingBudget: 4e3
452
683
  }
453
- console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
454
- if (aiGeneratedContent) {
684
+ };
685
+ const attemptResults = [];
686
+ let lastError = null;
687
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS3; attempt++) {
688
+ const startTime = Date.now();
689
+ const promptText = buildEnhancedPrompt3(clientInput, attempt);
690
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
691
+ try {
692
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
693
+ const parts = [{ text: promptText }];
694
+ if (clientInput.imageUrl) {
695
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
696
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
697
+ parts.unshift(imagePart);
698
+ }
699
+ const contents = [{ role: "user", parts }];
700
+ const aiResult = await ai.models.generateContent({
701
+ model,
702
+ config,
703
+ contents
704
+ });
705
+ const response = aiResult;
706
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
707
+ const duration = Date.now() - startTime;
708
+ DebugLogger.logResponse(attempt, rawText);
709
+ if (!rawText) {
710
+ throw new Error("AI returned an empty response.");
711
+ }
712
+ const parsedJson = JSON.parse(rawText);
713
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
714
+ const aiGeneratedContent = AIMCQOutputFieldsSchema.parse(parsedJson);
715
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
716
+ if ((_f = clientInput.quizContext) == null ? void 0 : _f.originalCategory) {
717
+ const verifiedCategory = (_g = aiGeneratedContent.verifiedCategory) == null ? void 0 : _g.toLowerCase();
718
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
719
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
720
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
721
+ }
722
+ }
455
723
  const finalOptions = [];
456
724
  const tempIdToFinalIdMap = {};
457
725
  aiGeneratedContent.options.forEach((aiOption) => {
@@ -461,7 +729,7 @@ Return only the JSON response.`;
461
729
  });
462
730
  const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
463
731
  if (!finalCorrectAnswerId) {
464
- throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid or does not match any generated option tempId.`);
732
+ throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid`);
465
733
  }
466
734
  const completeQuestion = {
467
735
  id: generateUniqueId("mcq_ai_"),
@@ -471,173 +739,196 @@ Return only the JSON response.`;
471
739
  correctAnswerId: finalCorrectAnswerId,
472
740
  explanation: aiGeneratedContent.explanation,
473
741
  points: aiGeneratedContent.points,
474
- topic: aiGeneratedContent.topic || clientInput.topic,
475
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
476
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
742
+ topic: aiGeneratedContent.topic || ((_h = clientInput.quizContext) == null ? void 0 : _h.originalTopic),
743
+ difficulty: clientInput.difficulty,
744
+ contextCode: (_i = clientInput.quizContext) == null ? void 0 : _i.plannedContextId,
745
+ bloomLevel: (_j = clientInput.quizContext) == null ? void 0 : _j.plannedBloomLevel,
746
+ learningObjective: (_k = clientInput.quizContext) == null ? void 0 : _k.originalLoId,
747
+ subject: (_l = clientInput.quizContext) == null ? void 0 : _l.originalSubject,
748
+ category: (_m = clientInput.quizContext) == null ? void 0 : _m.originalCategory,
749
+ imageUrl: clientInput.imageUrl
477
750
  };
478
- try {
479
- const validatedQuestion = MultipleChoiceQuestionZodSchema.parse(completeQuestion);
480
- return { question: validatedQuestion };
481
- } catch (validationError) {
482
- console.error("Question validation failed:", validationError);
483
- throw new Error(`Generated question failed validation: ${validationError}`);
751
+ const validatedQuestion = MultipleChoiceQuestionZodSchema.parse(completeQuestion);
752
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
753
+ console.log(`
754
+ \u2705 MCQ generation successful on attempt ${attempt} (${duration}ms)`);
755
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
756
+ return { question: validatedQuestion };
757
+ } catch (error) {
758
+ lastError = error;
759
+ const duration = Date.now() - startTime;
760
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
761
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS3;
762
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
763
+ if (willRetry) {
764
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS3}ms...`);
765
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS3));
484
766
  }
485
- } else {
486
- throw new Error("AI did not return content for the MCQ question.");
487
767
  }
488
- } catch (error) {
489
- console.error("Error generating MCQ question:", error);
490
- throw new Error(`Failed to generate MCQ question: ${error.message}`);
491
768
  }
769
+ DebugLogger.logAttemptSummary(attemptResults);
770
+ const errorMessage = `Failed to generate MCQ question after ${MAX_RETRY_ATTEMPTS3} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
771
+ console.error("\n\u274C Final Result: FAILED");
772
+ console.error(errorMessage);
773
+ return { error: errorMessage };
492
774
  }
493
- function extractJsonFromMarkdown4(text) {
494
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
495
- return match ? match[1].trim() : text.trim();
496
- }
497
- z.object({
498
- topic: z.string().describe("The topic for the question."),
499
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
500
- // <-- ĐÃ THÊM
501
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
502
- numberOfOptions: z.number().int().min(2).max(8).optional().default(5).describe("Number of answer options to generate (2-8)."),
503
- minCorrectAnswers: z.number().int().min(1).optional().default(2).describe("Minimum number of correct answers among the options."),
504
- maxCorrectAnswers: z.number().int().min(1).optional().default(3).describe("Maximum number of correct answers (must be <= numberOfOptions)."),
505
- contextDescription: z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
506
- selectedContextId: z.string().optional().describe("The ID of the selected context, if any.")
507
- });
508
- z.object({
509
- prompt: z.string().describe("The question statement itself."),
510
- options: z.array(
511
- z.object({
512
- tempId: z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B')."),
513
- text: z.string().describe("The text content of this answer option.")
514
- })
515
- ).min(2).max(8),
516
- correctTempOptionIds: z.array(z.string()).min(1).describe("An array of temporary IDs of the correct options."),
517
- explanation: z.string().optional().describe("A brief explanation of why the answers are correct."),
518
- points: z.number().optional().default(10).describe("Points for a correct answer."),
519
- difficulty: z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
520
- topic: z.string().optional().describe("Refined topic.")
775
+ BaseQuestionGenerationClientInputSchema.extend({
776
+ numberOfOptions: z.number().int().min(2).max(8).optional().default(5),
777
+ minCorrectAnswers: z.number().int().min(1).optional().default(2),
778
+ maxCorrectAnswers: z.number().int().min(1).optional().default(3)
521
779
  });
522
- var MultipleResponseQuestionZodSchema = z.object({
523
- id: z.string(),
524
- questionType: z.literal("multiple_response"),
525
- prompt: z.string().min(1),
526
- options: z.array(z.object({ id: z.string(), text: z.string().min(1) })).min(2).max(10),
527
- correctAnswerIds: z.array(z.string()).min(1),
528
- points: z.number().min(0).optional(),
780
+ var AIMRQOutputFieldsSchema = z.object({
781
+ prompt: z.string(),
782
+ options: z.array(z.object({ tempId: z.string(), text: z.string() })).min(2).max(8),
783
+ correctTempOptionIds: z.array(z.string()).min(1),
529
784
  explanation: z.string().optional(),
530
- learningObjective: z.string().optional(),
531
- glossary: z.array(z.string()).optional(),
532
- bloomLevel: z.string().optional(),
785
+ points: z.number().optional().default(10),
533
786
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
534
- contextCode: z.string().optional(),
535
- gradeBand: z.string().optional(),
536
- course: z.string().optional(),
537
- category: z.string().optional(),
538
- topic: z.string().optional()
539
- }).refine((data) => {
540
- const optionIds = new Set(data.options.map((option) => option.id));
541
- return data.correctAnswerIds.every((correctId) => optionIds.has(correctId));
542
- }, {
543
- message: "All correctAnswerIds must match one of the option IDs",
544
- path: ["correctAnswerIds"]
545
- }).refine((data) => {
546
- const optionIds = data.options.map((opt) => opt.id);
547
- return optionIds.length === new Set(optionIds).size;
548
- }, {
549
- message: "All option IDs must be unique",
550
- path: ["options"]
551
- });
552
- z.object({
553
- question: MultipleResponseQuestionZodSchema.optional().describe("The generated Multiple Response question.")
787
+ topic: z.string().optional(),
788
+ verifiedCategory: z.string().optional().describe("The category this question actually addresses.")
554
789
  });
555
- async function generateMRQQuestion(clientInput, apiKey) {
556
- try {
557
- if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
558
- throw new Error("Minimum correct answers cannot exceed maximum correct answers.");
559
- }
560
- if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
561
- throw new Error("Maximum correct answers must be less than the total number of options.");
562
- }
563
- const ai = genkit({
564
- plugins: [googleAI({ apiKey })],
565
- model: gemini20Flash
566
- });
567
- const promptText = `You are an expert quiz question writer specializing in Multiple Response questions.
568
- Generate a single Multiple Response question in ${clientInput.language} based on the following inputs.
569
790
 
570
- IMPORTANT: Return the response as JSON with this EXACT format:
571
- {
572
- "prompt": "Your question here (e.g., 'Which of the following are primary colors?')",
573
- "options": [
574
- { "tempId": "A", "text": "First option" },
575
- { "tempId": "B", "text": "Second option" },
576
- { "tempId": "C", "text": "Third option" },
577
- { "tempId": "D", "text": "Fourth option" },
578
- { "tempId": "E", "text": "Fifth option" }
579
- ],
580
- "correctTempOptionIds": ["A", "C"],
581
- "explanation": "Brief explanation of all correct answers.",
582
- "points": 10,
583
- "difficulty": "medium",
584
- "topic": "refined topic"
585
- }
791
+ // src/ai/flows/question-gen/generate-mrq-question.ts
792
+ var MAX_RETRY_ATTEMPTS4 = 3;
793
+ var RETRY_DELAY_MS4 = 3e3;
794
+ function buildEnhancedPrompt4(clientInput, attemptNumber) {
795
+ const { quizContext, language, difficulty, numberOfOptions, minCorrectAnswers, maxCorrectAnswers, imageUrl } = clientInput;
796
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
797
+ const attemptInfo = attemptNumber > 1 ? `
798
+ ## DEBUG INFO - This is attempt #${attemptNumber}
799
+ Previous attempts failed due to validation errors. Pay close attention to the number of correct answers and the JSON schema.
586
800
 
587
- Requirements:
588
- - Generate exactly ${clientInput.numberOfOptions} options.
589
- - Generate between ${clientInput.minCorrectAnswers} and ${clientInput.maxCorrectAnswers} correct answers.
590
- - Use tempId values like "A", "B", "C", etc.
591
- - Make sure all IDs in correctTempOptionIds match a tempId in the options array.
592
- - Each option must have both "tempId" and "text" fields.
593
- - The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
594
-
595
- Topic: ${clientInput.topic}
596
- Language: ${clientInput.language}
597
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
598
- Difficulty: ${clientInput.difficulty}
599
- Number of Options: ${clientInput.numberOfOptions}
600
- Min Correct Answers: ${clientInput.minCorrectAnswers}
601
- Max Correct Answers: ${clientInput.maxCorrectAnswers}
801
+ ` : "";
802
+ 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.` : "";
803
+ const contextStrings = [
804
+ `**Required Category:** ${category} (This is the ONLY language to be used)`,
805
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
806
+ imageContextInstruction,
807
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
808
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** Use this to create plausible incorrect answers (distractors). The misconception is: "${quizContext.targetMisconception}"`,
809
+ (quizContext == null ? void 0 : quizContext.difficultyReason) && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
810
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
811
+ const exampleJson = JSON.stringify({
812
+ prompt: "Which of the following are considered programming paradigms?",
813
+ options: [
814
+ { "tempId": "A", "text": "Object-Oriented" },
815
+ { "tempId": "B", "text": "Assembly" },
816
+ { "tempId": "C", "text": "Functional" },
817
+ { "tempId": "D", "text": "Procedural" },
818
+ { "tempId": "E", "text": "Middleware" }
819
+ ],
820
+ correctTempOptionIds: ["A", "C", "D"],
821
+ 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.",
822
+ points: 10,
823
+ difficulty: "medium",
824
+ topic: "Programming Paradigms",
825
+ verifiedCategory: category
826
+ }, null, 2);
827
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in the programming language: ${category}.
828
+ Your sole mission is to create a high-quality, technically accurate Multiple Response Question. You must adhere to the following rules at all times.
602
829
 
603
- Return only the JSON response.`;
604
- const response = await ai.generate(promptText);
605
- const rawText = response.text;
606
- const jsonText = extractJsonFromMarkdown4(rawText);
607
- console.log("AI Response:", jsonText);
608
- let aiGeneratedContent = JSON.parse(jsonText);
609
- if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
610
- const normalizedOptions = aiGeneratedContent.options.map((option) => {
611
- if (typeof option === "object" && !option.tempId && Object.keys(option).length === 1) {
612
- const key = Object.keys(option)[0];
613
- return { tempId: key, text: option[key] };
614
- }
615
- return option;
616
- });
617
- aiGeneratedContent.options = normalizedOptions;
830
+ ## Core Rules (Non-negotiable)
831
+ 1. **Category Purity:** The question, options, and explanation MUST be exclusively about **${category}**.
832
+ 2. **Context Adherence:** The question's content must directly align with all provided context.
833
+ 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.
834
+
835
+ ## CRITICAL CONTEXT FOR THIS QUESTION
836
+ ${contextStrings}
837
+
838
+ ## Task: Generate the Question
839
+ Based on all the rules and context above, generate a single Multiple Response Question.
840
+
841
+ ### Input Parameters
842
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
843
+ - **Language for Text:** ${language}
844
+ - **Difficulty Level:** ${difficulty}
845
+ - **Number of Options:** Generate exactly ${numberOfOptions} options.
846
+ - **Number of Correct Answers:** The 'correctTempOptionIds' array MUST contain between ${minCorrectAnswers} and ${maxCorrectAnswers} valid IDs from the options you generate.
847
+
848
+ ### Required JSON Output Format
849
+ Your response must be ONLY the JSON object, matching this exact structure and field names.
850
+
851
+ ${exampleJson}
852
+
853
+ Now, generate the JSON for the requested question.`;
854
+ }
855
+ async function generateMRQQuestion(clientInput, apiKey) {
856
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
857
+ if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
858
+ return { error: `Invalid input: minCorrectAnswers (${clientInput.minCorrectAnswers}) cannot be greater than maxCorrectAnswers (${clientInput.maxCorrectAnswers}).` };
859
+ }
860
+ if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
861
+ return { error: `Invalid input: maxCorrectAnswers (${clientInput.maxCorrectAnswers}) must be less than the total numberOfOptions (${clientInput.numberOfOptions}).` };
862
+ }
863
+ const ai = new GoogleGenAI({ apiKey });
864
+ const model = "gemini-2.5-flash";
865
+ const config = {
866
+ temperature: 0.8,
867
+ responseMimeType: "application/json",
868
+ thinkingConfig: {
869
+ thinkingBudget: 4e3
618
870
  }
619
- console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
620
- if (aiGeneratedContent) {
621
- const finalOptions = [];
622
- const tempIdToFinalIdMap = {};
871
+ };
872
+ const attemptResults = [];
873
+ let lastError = null;
874
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS4; attempt++) {
875
+ const startTime = Date.now();
876
+ const promptText = buildEnhancedPrompt4(clientInput, attempt);
877
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
878
+ try {
879
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
880
+ const parts = [{ text: promptText }];
881
+ if (clientInput.imageUrl) {
882
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
883
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
884
+ parts.unshift(imagePart);
885
+ }
886
+ const contents = [{ role: "user", parts }];
887
+ const aiResult = await ai.models.generateContent({
888
+ model,
889
+ config,
890
+ contents
891
+ });
892
+ const response = aiResult;
893
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
894
+ const duration = Date.now() - startTime;
895
+ DebugLogger.logResponse(attempt, rawText);
896
+ if (!rawText) {
897
+ throw new Error("AI returned an empty response.");
898
+ }
899
+ const parsedJson = JSON.parse(rawText);
900
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
901
+ const aiGeneratedContent = AIMRQOutputFieldsSchema.parse(parsedJson);
902
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
903
+ if (aiGeneratedContent.options.length !== clientInput.numberOfOptions) {
904
+ throw new Error(`AI generated ${aiGeneratedContent.options.length} options, but ${clientInput.numberOfOptions} were required.`);
905
+ }
906
+ const correctCount = aiGeneratedContent.correctTempOptionIds.length;
907
+ if (correctCount < clientInput.minCorrectAnswers || correctCount > clientInput.maxCorrectAnswers) {
908
+ throw new Error(`AI provided ${correctCount} correct answers, which is outside the required range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
909
+ }
910
+ if ((_f = clientInput.quizContext) == null ? void 0 : _f.originalCategory) {
911
+ const verifiedCategory = (_g = aiGeneratedContent.verifiedCategory) == null ? void 0 : _g.toLowerCase();
912
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
913
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
914
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
915
+ }
916
+ }
917
+ const finalOptions = [];
918
+ const tempIdToFinalIdMap = {};
919
+ const allTempIds = /* @__PURE__ */ new Set();
623
920
  aiGeneratedContent.options.forEach((aiOption) => {
624
921
  const finalId = generateUniqueId("opt_mr_");
625
922
  finalOptions.push({ id: finalId, text: aiOption.text });
626
923
  tempIdToFinalIdMap[aiOption.tempId] = finalId;
924
+ allTempIds.add(aiOption.tempId);
627
925
  });
628
926
  const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
629
- const finalId = tempIdToFinalIdMap[tempId];
630
- if (!finalId) {
631
- throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') that does not map to any generated option.`);
927
+ if (!allTempIds.has(tempId)) {
928
+ throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') which does not exist in the generated options.`);
632
929
  }
633
- return finalId;
930
+ return tempIdToFinalIdMap[tempId];
634
931
  });
635
- if (finalCorrectAnswerIds.length < clientInput.minCorrectAnswers || finalCorrectAnswerIds.length > clientInput.maxCorrectAnswers) {
636
- throw new Error(`AI generated ${finalCorrectAnswerIds.length} correct answers, which is outside the requested range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
637
- }
638
- if (finalOptions.length !== clientInput.numberOfOptions) {
639
- throw new Error(`AI generated ${finalOptions.length} options, but ${clientInput.numberOfOptions} were requested.`);
640
- }
641
932
  const completeQuestion = {
642
933
  id: generateUniqueId("mrq_ai_"),
643
934
  questionType: "multiple_response",
@@ -646,280 +937,348 @@ Return only the JSON response.`;
646
937
  correctAnswerIds: finalCorrectAnswerIds,
647
938
  explanation: aiGeneratedContent.explanation,
648
939
  points: aiGeneratedContent.points,
649
- topic: aiGeneratedContent.topic || clientInput.topic,
940
+ topic: aiGeneratedContent.topic || ((_h = clientInput.quizContext) == null ? void 0 : _h.originalTopic),
650
941
  difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
651
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
942
+ contextCode: (_i = clientInput.quizContext) == null ? void 0 : _i.plannedContextId,
943
+ bloomLevel: (_j = clientInput.quizContext) == null ? void 0 : _j.plannedBloomLevel,
944
+ learningObjective: (_k = clientInput.quizContext) == null ? void 0 : _k.originalLoId,
945
+ subject: (_l = clientInput.quizContext) == null ? void 0 : _l.originalSubject,
946
+ category: (_m = clientInput.quizContext) == null ? void 0 : _m.originalCategory,
947
+ imageUrl: clientInput.imageUrl
652
948
  };
653
- try {
654
- const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
655
- return { question: validatedQuestion };
656
- } catch (validationError) {
657
- console.error("Question validation failed:", validationError);
658
- throw new Error(`Generated question failed validation: ${validationError}`);
949
+ const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
950
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
951
+ console.log(`
952
+ \u2705 MRQ generation successful on attempt ${attempt} (${duration}ms)`);
953
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
954
+ return { question: validatedQuestion };
955
+ } catch (error) {
956
+ lastError = error;
957
+ const duration = Date.now() - startTime;
958
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
959
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS4;
960
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
961
+ if (willRetry) {
962
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS4}ms...`);
963
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS4));
659
964
  }
660
- } else {
661
- throw new Error("AI did not return content for the MRQ question.");
662
965
  }
663
- } catch (error) {
664
- console.error("Error generating MRQ question:", error);
665
- throw new Error(`Failed to generate MRQ question: ${error.message}`);
666
966
  }
967
+ DebugLogger.logAttemptSummary(attemptResults);
968
+ const errorMessage = `Failed to generate MRQ question after ${MAX_RETRY_ATTEMPTS4} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
969
+ console.error("\n\u274C Final Result: FAILED");
970
+ console.error(errorMessage);
971
+ return { error: errorMessage };
667
972
  }
668
- function extractJsonFromMarkdown5(text) {
669
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
670
- return match ? match[1].trim() : text.trim();
671
- }
672
- z.object({
673
- topic: z.string().describe("The topic for the question."),
674
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
675
- // <-- ĐÃ THÊM
676
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
677
- allowDecimals: z.boolean().optional().default(true).describe("Whether the answer can be a decimal."),
678
- minRange: z.number().optional().describe("Optional minimum value for the answer."),
679
- maxRange: z.number().optional().describe("Optional maximum value for the answer."),
680
- contextDescription: z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
681
- selectedContextId: z.string().optional().describe("The ID of the selected context, if any.")
682
- });
683
- z.object({
684
- prompt: z.string().describe("The question statement that expects a numerical answer."),
685
- answer: z.number().describe("The precise numerical correct answer."),
686
- tolerance: z.number().min(0).optional().default(0).describe("The acceptable range of error (plus or minus). Default is 0 for exact match."),
687
- explanation: z.string().optional().describe("Explanation for the correct answer."),
688
- points: z.number().optional().default(10),
689
- difficulty: z.enum(["easy", "medium", "hard"]).optional(),
690
- topic: z.string().optional()
973
+ BaseQuestionGenerationClientInputSchema.extend({
974
+ allowDecimals: z.boolean().optional().default(true),
975
+ minRange: z.number().optional(),
976
+ maxRange: z.number().optional(),
977
+ // Thêm trường tolerance để client có thể tùy chỉnh
978
+ tolerance: z.number().min(0).optional().default(0)
691
979
  });
692
- var NumericQuestionZodSchema = z.object({
693
- id: z.string(),
694
- questionType: z.literal("numeric"),
695
- prompt: z.string().min(1),
980
+ var AINumericOutputFieldsSchema = z.object({
981
+ prompt: z.string(),
696
982
  answer: z.number(),
983
+ // AI có thể đề xuất một tolerance, nhưng chúng ta sẽ ưu tiên của client
697
984
  tolerance: z.number().min(0).optional(),
698
- points: z.number().min(0).optional(),
699
985
  explanation: z.string().optional(),
700
- learningObjective: z.string().optional(),
701
- glossary: z.array(z.string()).optional(),
702
- bloomLevel: z.string().optional(),
986
+ points: z.number().optional().default(10),
703
987
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
704
- contextCode: z.string().optional(),
705
- gradeBand: z.string().optional(),
706
- course: z.string().optional(),
707
- category: z.string().optional(),
708
- topic: z.string().optional()
988
+ topic: z.string().optional(),
989
+ verifiedCategory: z.string().optional()
990
+ // Thêm để xác thực
709
991
  });
710
- z.object({
711
- question: NumericQuestionZodSchema.optional().describe("The generated Numeric question.")
712
- });
713
- async function generateNumericQuestion(clientInput, apiKey) {
714
- try {
715
- if (clientInput.minRange !== void 0 && clientInput.maxRange !== void 0 && clientInput.minRange > clientInput.maxRange) {
716
- throw new Error("minRange cannot be greater than maxRange.");
717
- }
718
- const ai = genkit({
719
- plugins: [googleAI({ apiKey })],
720
- model: gemini20Flash
721
- });
722
- const promptText = `You are an expert quiz question writer.
723
- Generate a single Numeric question in ${clientInput.language} based on the following inputs. The question must require a numerical answer.
724
992
 
725
- IMPORTANT: Return the response as JSON with this EXACT format:
726
- {
727
- "prompt": "Your question here (e.g., 'What is the value of Pi rounded to two decimal places?')",
728
- "answer": 3.14,
729
- "tolerance": 0.01,
730
- "explanation": "Pi is an irrational number, approximately 3.14159. Rounding to two decimal places gives 3.14.",
731
- "points": 10,
732
- "difficulty": "easy",
733
- "topic": "Mathematics"
734
- }
993
+ // src/ai/flows/question-gen/generate-numeric-question.ts
994
+ var MAX_RETRY_ATTEMPTS5 = 3;
995
+ var RETRY_DELAY_MS5 = 3e3;
996
+ function buildEnhancedPrompt5(clientInput, attemptNumber) {
997
+ const { quizContext, language, difficulty, minRange, maxRange, allowDecimals, imageUrl } = clientInput;
998
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
999
+ const attemptInfo = attemptNumber > 1 ? `
1000
+ ## DEBUG INFO - This is attempt #${attemptNumber}
1001
+ Previous attempts failed. Ensure the 'answer' is a valid number and fits within the specified constraints.
735
1002
 
736
- Requirements:
737
- - The 'answer' must be a number.
738
- - If 'allowDecimals' is false, the 'answer' must be an integer.
739
- - 'tolerance' is the acceptable error range (+/-). A tolerance of 0 means the answer must be exact.
740
- - The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
741
- ${clientInput.minRange !== void 0 ? `- The answer should ideally be >= ${clientInput.minRange}.` : ""}
742
- ${clientInput.maxRange !== void 0 ? `- The answer should ideally be <= ${clientInput.maxRange}.` : ""}
1003
+ ` : "";
1004
+ 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.` : "";
1005
+ const contextStrings = [
1006
+ `**Required Category:** ${category}`,
1007
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
1008
+ imageContextInstruction,
1009
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
1010
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** The question should clarify this numerical error: "${quizContext.targetMisconception}"`
1011
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
1012
+ const constraintStrings = [
1013
+ minRange !== void 0 && `The final 'answer' MUST be greater than or equal to ${minRange}.`,
1014
+ maxRange !== void 0 && `The final 'answer' MUST be less than or equal to ${maxRange}.`,
1015
+ !allowDecimals && `The final 'answer' MUST be an integer (whole number).`
1016
+ ].filter(Boolean).join("\n");
1017
+ const exampleJson = JSON.stringify({
1018
+ 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`?",
1019
+ answer: 127,
1020
+ tolerance: 0,
1021
+ 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).",
1022
+ points: 10,
1023
+ difficulty: "medium",
1024
+ topic: "Swift Data Types",
1025
+ verifiedCategory: category
1026
+ }, null, 2);
1027
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
1028
+ Your mission is to create a high-quality, technically accurate Numeric Question.
743
1029
 
744
- Topic: ${clientInput.topic}
745
- Language: ${clientInput.language}
746
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
747
- Difficulty: ${clientInput.difficulty}
748
- Allow Decimals: ${clientInput.allowDecimals}
1030
+ ## Core Rules (Non-negotiable)
1031
+ 1. **Category Purity:** The question MUST be exclusively about **${category}**.
1032
+ 2. **Quantitative Answer:** The question MUST ask for a specific, objective numerical answer.
1033
+ 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
749
1034
 
750
- Return only the JSON response.`;
751
- const response = await ai.generate(promptText);
752
- const rawText = response.text;
753
- const jsonText = extractJsonFromMarkdown5(rawText);
754
- console.log("AI Response:", jsonText);
755
- const aiGeneratedContent = JSON.parse(jsonText);
756
- if (aiGeneratedContent) {
757
- let finalAnswer = aiGeneratedContent.answer;
758
- if (!clientInput.allowDecimals) {
759
- finalAnswer = Math.round(finalAnswer);
1035
+ ## CRITICAL CONTEXT FOR THIS QUESTION
1036
+ ${contextStrings}
1037
+
1038
+ ## Task: Generate the Question
1039
+ Based on all the rules and context above, generate a single Numeric Question.
1040
+
1041
+ ### Input Parameters & Constraints
1042
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
1043
+ - **Language for Text:** ${language}
1044
+ - **Difficulty Level:** ${difficulty}
1045
+ ${constraintStrings ? `
1046
+ ### CRITICAL CONSTRAINTS ON THE ANSWER
1047
+ ${constraintStrings}` : ""}
1048
+
1049
+ ### Required JSON Output Format
1050
+ Your response must be ONLY the JSON object, matching this exact structure:
1051
+
1052
+ ${exampleJson}
1053
+
1054
+ Now, generate the JSON for the requested question.`;
1055
+ }
1056
+ async function generateNumericQuestion(clientInput, apiKey) {
1057
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
1058
+ if (clientInput.minRange !== void 0 && clientInput.maxRange !== void 0 && clientInput.minRange > clientInput.maxRange) {
1059
+ return { error: `Invalid input: minRange (${clientInput.minRange}) cannot be greater than maxRange (${clientInput.maxRange}).` };
1060
+ }
1061
+ const ai = new GoogleGenAI({ apiKey });
1062
+ const model = "gemini-2.5-flash";
1063
+ const config = {
1064
+ temperature: 0.4,
1065
+ // Giữ nhiệt độ thấp cho các câu trả lời chính xác
1066
+ responseMimeType: "application/json",
1067
+ thinkingConfig: {
1068
+ thinkingBudget: 4e3
1069
+ }
1070
+ };
1071
+ const attemptResults = [];
1072
+ let lastError = null;
1073
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS5; attempt++) {
1074
+ const startTime = Date.now();
1075
+ const promptText = buildEnhancedPrompt5(clientInput, attempt);
1076
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1077
+ try {
1078
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
1079
+ const parts = [{ text: promptText }];
1080
+ if (clientInput.imageUrl) {
1081
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
1082
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
1083
+ parts.unshift(imagePart);
1084
+ }
1085
+ const contents = [{ role: "user", parts }];
1086
+ const aiResult = await ai.models.generateContent({ model, config, contents });
1087
+ const response = aiResult;
1088
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
1089
+ const duration = Date.now() - startTime;
1090
+ DebugLogger.logResponse(attempt, rawText);
1091
+ if (!rawText) throw new Error("AI returned an empty response.");
1092
+ const parsedJson = JSON.parse(rawText);
1093
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
1094
+ const aiGeneratedContent = AINumericOutputFieldsSchema.parse(parsedJson);
1095
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
1096
+ const answer = aiGeneratedContent.answer;
1097
+ if (clientInput.minRange !== void 0 && answer < clientInput.minRange) {
1098
+ throw new Error(`AI answer ${answer} is less than the required minRange of ${clientInput.minRange}.`);
760
1099
  }
761
- if (clientInput.minRange !== void 0 && finalAnswer < clientInput.minRange) {
762
- console.warn(`AI generated answer ${finalAnswer} is below the requested minRange of ${clientInput.minRange}.`);
1100
+ if (clientInput.maxRange !== void 0 && answer > clientInput.maxRange) {
1101
+ throw new Error(`AI answer ${answer} is greater than the required maxRange of ${clientInput.maxRange}.`);
763
1102
  }
764
- if (clientInput.maxRange !== void 0 && finalAnswer > clientInput.maxRange) {
765
- console.warn(`AI generated answer ${finalAnswer} is above the requested maxRange of ${clientInput.maxRange}.`);
1103
+ if (!clientInput.allowDecimals && !Number.isInteger(answer)) {
1104
+ throw new Error(`AI answer ${answer} is not an integer, but decimals are not allowed.`);
1105
+ }
1106
+ if ((_f = clientInput.quizContext) == null ? void 0 : _f.originalCategory) {
1107
+ const verifiedCategory = (_g = aiGeneratedContent.verifiedCategory) == null ? void 0 : _g.toLowerCase();
1108
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
1109
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
1110
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
1111
+ }
766
1112
  }
767
1113
  const completeQuestion = {
768
1114
  id: generateUniqueId("num_ai_"),
769
1115
  questionType: "numeric",
770
1116
  prompt: aiGeneratedContent.prompt,
771
- answer: finalAnswer,
772
- tolerance: aiGeneratedContent.tolerance,
1117
+ answer: aiGeneratedContent.answer,
1118
+ tolerance: (_i = (_h = clientInput.tolerance) != null ? _h : aiGeneratedContent.tolerance) != null ? _i : 0,
773
1119
  explanation: aiGeneratedContent.explanation,
774
1120
  points: aiGeneratedContent.points,
775
- topic: aiGeneratedContent.topic || clientInput.topic,
776
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
777
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1121
+ topic: aiGeneratedContent.topic || ((_j = clientInput.quizContext) == null ? void 0 : _j.originalTopic),
1122
+ difficulty: clientInput.difficulty,
1123
+ contextCode: (_k = clientInput.quizContext) == null ? void 0 : _k.plannedContextId,
1124
+ bloomLevel: (_l = clientInput.quizContext) == null ? void 0 : _l.plannedBloomLevel,
1125
+ learningObjective: (_m = clientInput.quizContext) == null ? void 0 : _m.originalLoId,
1126
+ subject: (_n = clientInput.quizContext) == null ? void 0 : _n.originalSubject,
1127
+ category: (_o = clientInput.quizContext) == null ? void 0 : _o.originalCategory,
1128
+ imageUrl: clientInput.imageUrl
778
1129
  };
779
- try {
780
- const validatedQuestion = NumericQuestionZodSchema.parse(completeQuestion);
781
- return { question: validatedQuestion };
782
- } catch (validationError) {
783
- console.error("Question validation failed:", validationError);
784
- throw new Error(`Generated question failed validation: ${validationError}`);
1130
+ const validatedQuestion = NumericQuestionZodSchema.parse(completeQuestion);
1131
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
1132
+ console.log(`
1133
+ \u2705 Numeric generation successful on attempt ${attempt} (${duration}ms)`);
1134
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
1135
+ return { question: validatedQuestion };
1136
+ } catch (error) {
1137
+ lastError = error;
1138
+ const duration = Date.now() - startTime;
1139
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1140
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS5;
1141
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
1142
+ if (willRetry) {
1143
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS5}ms...`);
1144
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS5));
785
1145
  }
786
- } else {
787
- throw new Error("AI did not return content for the Numeric question.");
788
1146
  }
789
- } catch (error) {
790
- console.error("Error generating Numeric question:", error);
791
- throw new Error(`Failed to generate Numeric question: ${error.message}`);
792
1147
  }
1148
+ DebugLogger.logAttemptSummary(attemptResults);
1149
+ const errorMessage = `Failed to generate Numeric question after ${MAX_RETRY_ATTEMPTS5} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
1150
+ console.error("\n\u274C Final Result: FAILED");
1151
+ console.error(errorMessage);
1152
+ return { error: errorMessage };
793
1153
  }
794
- function extractJsonFromMarkdown6(text) {
795
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
796
- return match ? match[1].trim() : text.trim();
797
- }
798
- z.object({
799
- topic: z.string().describe("The topic for the question."),
800
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
801
- // <-- ĐÃ THÊM
802
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
803
- numberOfItems: z.number().int().min(2).max(10).optional().default(4).describe("Number of items to sequence (2-10)."),
804
- contextDescription: z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
805
- selectedContextId: z.string().optional().describe("The ID of the selected context, if any.")
806
- });
807
- z.object({
808
- prompt: z.string().describe("The instruction for sequencing."),
809
- itemsContent: z.array(z.string().min(1)).min(2).describe("An array of strings for each item to be sequenced."),
810
- correctOrderContent: z.array(z.string().min(1)).min(2).describe("An array of the same strings from 'itemsContent', but in the correct sequence."),
811
- explanation: z.string().optional().describe("Explanation for the correct sequence."),
812
- points: z.number().optional().default(10),
813
- difficulty: z.enum(["easy", "medium", "hard"]).optional(),
814
- topic: z.string().optional()
1154
+ BaseQuestionGenerationClientInputSchema.extend({
1155
+ numberOfItems: z.number().int().min(2).max(10).optional().default(4)
815
1156
  });
816
- var SequenceQuestionZodSchema = z.object({
817
- id: z.string(),
818
- questionType: z.literal("sequence"),
819
- prompt: z.string().min(1),
820
- items: z.array(z.object({ id: z.string(), content: z.string().min(1) })).min(2),
821
- correctOrder: z.array(z.string()).min(2),
822
- // Array of item IDs
823
- points: z.number().min(0).optional(),
1157
+ var AISequenceOutputFieldsSchema = z.object({
1158
+ prompt: z.string().describe("The instructional text for the user, e.g., 'Arrange the steps in the correct order.'"),
1159
+ // 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ự
1160
+ // Điều này đơn giản hóa logic và giảm rủi ro
1161
+ 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."),
824
1162
  explanation: z.string().optional(),
825
- learningObjective: z.string().optional(),
826
- glossary: z.array(z.string()).optional(),
827
- bloomLevel: z.string().optional(),
1163
+ points: z.number().optional().default(10),
828
1164
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
829
- contextCode: z.string().optional(),
830
- gradeBand: z.string().optional(),
831
- course: z.string().optional(),
832
- category: z.string().optional(),
833
- topic: z.string().optional()
834
- }).refine((data) => {
835
- const itemIds = new Set(data.items.map((item) => item.id));
836
- return data.correctOrder.every((id) => itemIds.has(id));
837
- }, {
838
- message: "Every ID in correctOrder must correspond to an item in the items array.",
839
- path: ["correctOrder"]
840
- }).refine((data) => {
841
- return new Set(data.correctOrder).size === data.items.length && data.correctOrder.length === data.items.length;
842
- }, {
843
- message: "The correctOrder array must contain all item IDs exactly once.",
844
- path: ["correctOrder"]
845
- }).refine((data) => {
846
- const itemIds = data.items.map((item) => item.id);
847
- return itemIds.length === new Set(itemIds).size;
848
- }, {
849
- message: "All item IDs must be unique.",
850
- path: ["items"]
851
- });
852
- z.object({
853
- question: SequenceQuestionZodSchema.optional().describe("The generated Sequence question.")
1165
+ topic: z.string().optional(),
1166
+ verifiedCategory: z.string().optional()
1167
+ // Thêm để xác thực
854
1168
  });
855
- async function generateSequenceQuestion(clientInput, apiKey) {
856
- try {
857
- const ai = genkit({
858
- plugins: [googleAI({ apiKey })],
859
- model: gemini20Flash
860
- });
861
- const promptText = `You are an expert quiz question writer specializing in Sequence questions.
862
- Generate a single Sequence question in ${clientInput.language} based on the following inputs.
863
1169
 
864
- IMPORTANT: Return the response as JSON with this EXACT format:
865
- {
866
- "prompt": "Arrange the following events of World War II in chronological order.",
867
- "itemsContent": [
868
- "D-Day (Normandy Landings)",
869
- "Invasion of Poland",
870
- "Attack on Pearl Harbor",
871
- "Battle of Stalingrad"
872
- ],
873
- "correctOrderContent": [
874
- "Invasion of Poland",
875
- "Attack on Pearl Harbor",
876
- "Battle of Stalingrad",
877
- "D-Day (Normandy Landings)"
878
- ],
879
- "explanation": "The Invasion of Poland started the war in Europe, followed by the US entry after Pearl Harbor, the turning point at Stalingrad, and finally the D-Day invasion.",
880
- "points": 10,
881
- "difficulty": "medium",
882
- "topic": "World War II History"
883
- }
1170
+ // src/ai/flows/question-gen/generate-sequence-question.ts
1171
+ var MAX_RETRY_ATTEMPTS6 = 3;
1172
+ var RETRY_DELAY_MS6 = 3e3;
1173
+ function buildEnhancedPrompt6(clientInput, attemptNumber) {
1174
+ const { quizContext, language, difficulty, numberOfItems, imageUrl } = clientInput;
1175
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
1176
+ const attemptInfo = attemptNumber > 1 ? `
1177
+ ## DEBUG INFO - This is attempt #${attemptNumber}
1178
+ Previous attempts failed. Ensure the 'itemsInCorrectOrder' array has exactly the required number of items and the JSON is valid.
884
1179
 
885
- Requirements:
886
- - Generate exactly ${clientInput.numberOfItems} items to be sequenced.
887
- - The 'itemsContent' array should contain the text for each item.
888
- - The 'correctOrderContent' array must contain the exact same strings as 'itemsContent', but arranged in the correct sequence.
889
- - Both arrays must have the same number of elements.
890
- - The content of 'prompt', 'itemsContent', 'correctOrderContent', 'explanation', and 'topic' must be in ${clientInput.language}.
1180
+ ` : "";
1181
+ 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.` : "";
1182
+ const contextStrings = [
1183
+ `**Required Category:** ${category}`,
1184
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
1185
+ imageContextInstruction,
1186
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
1187
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** The sequence should clarify this specific process error: "${quizContext.targetMisconception}"`
1188
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
1189
+ const exampleJson = JSON.stringify({
1190
+ prompt: "Arrange the steps to make a network request in Swift using URLSession.",
1191
+ itemsInCorrectOrder: [
1192
+ "Create a URL object.",
1193
+ "Create a URLSessionDataTask with the URL.",
1194
+ "Start the task by calling its resume() method.",
1195
+ "Handle the completion handler with data, response, and error."
1196
+ ],
1197
+ explanation: "This is the fundamental sequence for a basic data task in URLSession.",
1198
+ points: 10,
1199
+ difficulty: "medium",
1200
+ topic: "Swift Networking",
1201
+ verifiedCategory: category
1202
+ }, null, 2);
1203
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
1204
+ Your mission is to create a high-quality, technically accurate Sequence Question.
891
1205
 
892
- Topic: ${clientInput.topic}
893
- Language: ${clientInput.language}
894
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
895
- Difficulty: ${clientInput.difficulty}
896
- Number of Items: ${clientInput.numberOfItems}
1206
+ ## Core Rules (Non-negotiable)
1207
+ 1. **Category Purity:** The question MUST be exclusively about **${category}**.
1208
+ 2. **Unambiguous Order:** The items MUST represent a clear, objective process, timeline, or order with only one correct sequence.
1209
+ 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
897
1210
 
898
- Return only the JSON response.`;
899
- const response = await ai.generate(promptText);
900
- const rawText = response.text;
901
- const jsonText = extractJsonFromMarkdown6(rawText);
902
- console.log("AI Response:", jsonText);
903
- const aiGeneratedContent = JSON.parse(jsonText);
904
- if (aiGeneratedContent) {
905
- if (aiGeneratedContent.itemsContent.length !== clientInput.numberOfItems || aiGeneratedContent.correctOrderContent.length !== clientInput.numberOfItems) {
906
- throw new Error(`AI generated an incorrect number of items. Requested: ${clientInput.numberOfItems}, Got: ${aiGeneratedContent.itemsContent.length} items and ${aiGeneratedContent.correctOrderContent.length} in correct order.`);
1211
+ ## CRITICAL CONTEXT FOR THIS QUESTION
1212
+ ${contextStrings}
1213
+
1214
+ ## Task: Generate the Question
1215
+ Based on all the rules and context above, generate a single Sequence Question.
1216
+
1217
+ ### Input Parameters
1218
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
1219
+ - **Language for Text:** ${language}
1220
+ - **Difficulty Level:** ${difficulty}
1221
+ - **Number of Items:** Generate an array 'itemsInCorrectOrder' containing exactly ${numberOfItems} items. The array itself MUST be in the correct final sequence.
1222
+
1223
+ ### Required JSON Output Format
1224
+ Your response must be ONLY the JSON object, matching this exact structure:
1225
+
1226
+ ${exampleJson}
1227
+
1228
+ Now, generate the JSON for the requested question.`;
1229
+ }
1230
+ async function generateSequenceQuestion(clientInput, apiKey) {
1231
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
1232
+ const ai = new GoogleGenAI({ apiKey });
1233
+ const model = "gemini-2.5-flash";
1234
+ const config = {
1235
+ temperature: 0.7,
1236
+ responseMimeType: "application/json",
1237
+ thinkingConfig: {
1238
+ thinkingBudget: 4e3
1239
+ }
1240
+ };
1241
+ const attemptResults = [];
1242
+ let lastError = null;
1243
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS6; attempt++) {
1244
+ const startTime = Date.now();
1245
+ const promptText = buildEnhancedPrompt6(clientInput, attempt);
1246
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1247
+ try {
1248
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
1249
+ const parts = [{ text: promptText }];
1250
+ if (clientInput.imageUrl) {
1251
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
1252
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
1253
+ parts.unshift(imagePart);
907
1254
  }
908
- if (new Set(aiGeneratedContent.itemsContent).size !== new Set(aiGeneratedContent.correctOrderContent).size) {
909
- throw new Error("The set of items in 'itemsContent' and 'correctOrderContent' do not match.");
1255
+ const contents = [{ role: "user", parts }];
1256
+ const aiResult = await ai.models.generateContent({ model, config, contents });
1257
+ const response = aiResult;
1258
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
1259
+ const duration = Date.now() - startTime;
1260
+ DebugLogger.logResponse(attempt, rawText);
1261
+ if (!rawText) throw new Error("AI returned an empty response.");
1262
+ const parsedJson = JSON.parse(rawText);
1263
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
1264
+ const aiGeneratedContent = AISequenceOutputFieldsSchema.parse(parsedJson);
1265
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
1266
+ if (aiGeneratedContent.itemsInCorrectOrder.length !== clientInput.numberOfItems) {
1267
+ throw new Error(`AI generated ${aiGeneratedContent.itemsInCorrectOrder.length} items, but ${clientInput.numberOfItems} were required.`);
910
1268
  }
911
- const contentToIdMap = {};
912
- const finalItems = aiGeneratedContent.itemsContent.map((content) => {
913
- const id = generateUniqueId("seqi_");
914
- contentToIdMap[content] = id;
915
- return { id, content };
916
- });
917
- const finalCorrectOrder = aiGeneratedContent.correctOrderContent.map((content) => {
918
- const id = contentToIdMap[content];
919
- if (!id) {
920
- throw new Error(`Content "${content}" from 'correctOrderContent' was not found in 'itemsContent'.`);
1269
+ if ((_f = clientInput.quizContext) == null ? void 0 : _f.originalCategory) {
1270
+ const verifiedCategory = (_g = aiGeneratedContent.verifiedCategory) == null ? void 0 : _g.toLowerCase();
1271
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
1272
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
1273
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
921
1274
  }
922
- return id;
1275
+ }
1276
+ const finalItems = [];
1277
+ const finalCorrectOrder = [];
1278
+ aiGeneratedContent.itemsInCorrectOrder.forEach((content) => {
1279
+ const id = generateUniqueId("seqi_");
1280
+ finalItems.push({ id, content });
1281
+ finalCorrectOrder.push(id);
923
1282
  });
924
1283
  const completeQuestion = {
925
1284
  id: generateUniqueId("seq_ai_"),
@@ -929,212 +1288,307 @@ Return only the JSON response.`;
929
1288
  correctOrder: finalCorrectOrder,
930
1289
  explanation: aiGeneratedContent.explanation,
931
1290
  points: aiGeneratedContent.points,
932
- topic: aiGeneratedContent.topic || clientInput.topic,
933
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
934
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1291
+ topic: aiGeneratedContent.topic || ((_h = clientInput.quizContext) == null ? void 0 : _h.originalTopic),
1292
+ difficulty: clientInput.difficulty,
1293
+ contextCode: (_i = clientInput.quizContext) == null ? void 0 : _i.plannedContextId,
1294
+ bloomLevel: (_j = clientInput.quizContext) == null ? void 0 : _j.plannedBloomLevel,
1295
+ learningObjective: (_k = clientInput.quizContext) == null ? void 0 : _k.originalLoId,
1296
+ subject: (_l = clientInput.quizContext) == null ? void 0 : _l.originalSubject,
1297
+ category: (_m = clientInput.quizContext) == null ? void 0 : _m.originalCategory,
1298
+ imageUrl: clientInput.imageUrl
935
1299
  };
936
- try {
937
- const validatedQuestion = SequenceQuestionZodSchema.parse(completeQuestion);
938
- return { question: validatedQuestion };
939
- } catch (validationError) {
940
- console.error("Question validation failed:", validationError);
941
- throw new Error(`Generated question failed validation: ${validationError}`);
1300
+ const validatedQuestion = SequenceQuestionZodSchema.parse(completeQuestion);
1301
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
1302
+ console.log(`
1303
+ \u2705 Sequence generation successful on attempt ${attempt} (${duration}ms)`);
1304
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
1305
+ return { question: validatedQuestion };
1306
+ } catch (error) {
1307
+ lastError = error;
1308
+ const duration = Date.now() - startTime;
1309
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1310
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS6;
1311
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
1312
+ if (willRetry) {
1313
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS6}ms...`);
1314
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS6));
942
1315
  }
943
- } else {
944
- throw new Error("AI did not return content for the Sequence question.");
945
1316
  }
946
- } catch (error) {
947
- console.error("Error generating Sequence question:", error);
948
- throw new Error(`Failed to generate Sequence question: ${error.message}`);
949
1317
  }
1318
+ DebugLogger.logAttemptSummary(attemptResults);
1319
+ const errorMessage = `Failed to generate Sequence question after ${MAX_RETRY_ATTEMPTS6} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
1320
+ console.error("\n\u274C Final Result: FAILED");
1321
+ console.error(errorMessage);
1322
+ return { error: errorMessage };
950
1323
  }
951
- function extractJsonFromMarkdown7(text) {
952
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
953
- return match ? match[1].trim() : text.trim();
954
- }
955
- z.object({
956
- topic: z.string().describe("The topic for the question."),
957
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
958
- // <-- ĐÃ THÊM
959
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
960
- isCaseSensitive: z.boolean().optional().default(false).describe("Whether the answer should be case-sensitive."),
961
- contextDescription: z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
962
- selectedContextId: z.string().optional().describe("The ID of the selected context, if any.")
963
- });
964
- z.object({
965
- prompt: z.string().describe("The question statement."),
966
- acceptedAnswers: z.array(z.string().min(1)).min(1).describe("An array of acceptable short answers."),
967
- isCaseSensitive: z.boolean().optional().describe("Should the answer evaluation be case sensitive?"),
968
- explanation: z.string().optional().describe("Explanation for the correct answer(s)."),
969
- points: z.number().optional().default(10),
970
- difficulty: z.enum(["easy", "medium", "hard"]).optional(),
971
- topic: z.string().optional()
1324
+ BaseQuestionGenerationClientInputSchema.extend({
1325
+ isCaseSensitive: z.boolean().optional().default(false)
972
1326
  });
973
- var ShortAnswerQuestionZodSchema = z.object({
974
- id: z.string(),
975
- questionType: z.literal("short_answer"),
976
- prompt: z.string().min(1),
977
- acceptedAnswers: z.array(z.string().min(1)).min(1),
978
- isCaseSensitive: z.boolean().optional(),
979
- points: z.number().min(0).optional(),
1327
+ var AIShortAnswerOutputFieldsSchema = z.object({
1328
+ prompt: z.string().describe("The question text that prompts the user for a short answer."),
1329
+ acceptedAnswers: z.array(z.string().min(1)).min(1).describe("An array of one or more acceptable short answers. Include common variations if applicable."),
1330
+ // isCaseSensitive không cần thiết ở đây, chúng ta sẽ quản lý nó ở phía client
980
1331
  explanation: z.string().optional(),
981
- learningObjective: z.string().optional(),
982
- glossary: z.array(z.string()).optional(),
983
- bloomLevel: z.string().optional(),
1332
+ points: z.number().optional().default(10),
984
1333
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
985
- contextCode: z.string().optional(),
986
- gradeBand: z.string().optional(),
987
- course: z.string().optional(),
988
- category: z.string().optional(),
989
- topic: z.string().optional()
990
- });
991
- z.object({
992
- question: ShortAnswerQuestionZodSchema.optional().describe("The generated Short Answer question.")
1334
+ topic: z.string().optional(),
1335
+ verifiedCategory: z.string().optional()
1336
+ // Thêm để xác thực
993
1337
  });
994
- async function generateShortAnswerQuestion(clientInput, apiKey) {
995
- var _a;
996
- try {
997
- const ai = genkit({
998
- plugins: [googleAI({ apiKey })],
999
- model: gemini20Flash
1000
- });
1001
- const promptText = `You are an expert quiz question writer.
1002
- Generate a single Short Answer question in ${clientInput.language} based on the following inputs.
1003
1338
 
1004
- IMPORTANT: Return the response as JSON with this EXACT format:
1005
- {
1006
- "prompt": "What is the capital of France?",
1007
- "acceptedAnswers": ["Paris"],
1008
- "isCaseSensitive": false,
1009
- "explanation": "Paris has been the capital of France since the 10th century.",
1010
- "points": 5,
1011
- "difficulty": "easy",
1012
- "topic": "World Capitals"
1013
- }
1339
+ // src/ai/flows/question-gen/generate-short-answer-question.ts
1340
+ var MAX_RETRY_ATTEMPTS7 = 3;
1341
+ var RETRY_DELAY_MS7 = 3e3;
1342
+ function buildEnhancedPrompt7(clientInput, attemptNumber) {
1343
+ const { quizContext, language, difficulty, imageUrl } = clientInput;
1344
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
1345
+ const attemptInfo = attemptNumber > 1 ? `
1346
+ ## DEBUG INFO - This is attempt #${attemptNumber}
1347
+ Previous attempts failed. Ensure 'acceptedAnswers' is a non-empty array of strings and the JSON is valid.
1014
1348
 
1015
- Requirements:
1016
- - The 'acceptedAnswers' array must contain at least one possible correct answer.
1017
- - If there are multiple common ways to phrase the answer (e.g., "USA", "United States"), include them in the array.
1018
- - Set 'isCaseSensitive' to true or false based on the nature of the answer. Default to the input value.
1019
- - The content of 'prompt', 'acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
1349
+ ` : "";
1350
+ 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.` : "";
1351
+ const contextStrings = [
1352
+ `**Required Category:** ${category}`,
1353
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
1354
+ imageContextInstruction,
1355
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
1356
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** The question should require an answer that corrects this specific misconception: "${quizContext.targetMisconception}"`
1357
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
1358
+ const exampleJson = JSON.stringify({
1359
+ prompt: "In Swift, what keyword is used to declare a constant?",
1360
+ acceptedAnswers: ["let"],
1361
+ 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.",
1362
+ points: 10,
1363
+ difficulty: "easy",
1364
+ topic: "Swift Constants",
1365
+ verifiedCategory: category
1366
+ }, null, 2);
1367
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
1368
+ Your mission is to create a high-quality, technically accurate Short Answer Question.
1020
1369
 
1021
- Topic: ${clientInput.topic}
1022
- Language: ${clientInput.language}
1023
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
1024
- Difficulty: ${clientInput.difficulty}
1025
- Case Sensitive: ${clientInput.isCaseSensitive}
1370
+ ## Core Rules (Non-negotiable)
1371
+ 1. **Category Purity:** The question MUST be exclusively about **${category}**.
1372
+ 2. **Objective Answer:** The question must have a short, factual, and objective answer. Avoid questions that are subjective or require long explanations.
1373
+ 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
1026
1374
 
1027
- Return only the JSON response.`;
1028
- const response = await ai.generate(promptText);
1029
- const rawText = response.text;
1030
- const jsonText = extractJsonFromMarkdown7(rawText);
1031
- console.log("AI Response:", jsonText);
1032
- const aiGeneratedContent = JSON.parse(jsonText);
1033
- if (aiGeneratedContent) {
1375
+ ## CRITICAL CONTEXT FOR THIS QUESTION
1376
+ ${contextStrings}
1377
+
1378
+ ## Task: Generate the Question
1379
+ Based on all the rules and context above, generate a single Short Answer Question.
1380
+
1381
+ ### Input Parameters
1382
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
1383
+ - **Language for Text:** ${language}
1384
+ - **Difficulty Level:** ${difficulty}
1385
+
1386
+ ### Required JSON Output Format
1387
+ Your response must be ONLY the JSON object, matching this exact structure:
1388
+
1389
+ ${exampleJson}
1390
+
1391
+ Now, generate the JSON for the requested question.`;
1392
+ }
1393
+ async function generateShortAnswerQuestion(clientInput, apiKey) {
1394
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
1395
+ const ai = new GoogleGenAI({ apiKey });
1396
+ const model = "gemini-2.5-flash";
1397
+ const config = {
1398
+ temperature: 0.5,
1399
+ responseMimeType: "application/json",
1400
+ thinkingConfig: {
1401
+ thinkingBudget: 4e3
1402
+ }
1403
+ };
1404
+ const attemptResults = [];
1405
+ let lastError = null;
1406
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS7; attempt++) {
1407
+ const startTime = Date.now();
1408
+ const promptText = buildEnhancedPrompt7(clientInput, attempt);
1409
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1410
+ try {
1411
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
1412
+ const parts = [{ text: promptText }];
1413
+ if (clientInput.imageUrl) {
1414
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
1415
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
1416
+ parts.unshift(imagePart);
1417
+ }
1418
+ const contents = [{ role: "user", parts }];
1419
+ const aiResult = await ai.models.generateContent({ model, config, contents });
1420
+ const response = aiResult;
1421
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
1422
+ const duration = Date.now() - startTime;
1423
+ DebugLogger.logResponse(attempt, rawText);
1424
+ if (!rawText) throw new Error("AI returned an empty response.");
1425
+ const parsedJson = JSON.parse(rawText);
1426
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
1427
+ const aiGeneratedContent = AIShortAnswerOutputFieldsSchema.parse(parsedJson);
1428
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
1429
+ if ((_f = clientInput.quizContext) == null ? void 0 : _f.originalCategory) {
1430
+ const verifiedCategory = (_g = aiGeneratedContent.verifiedCategory) == null ? void 0 : _g.toLowerCase();
1431
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
1432
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
1433
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
1434
+ }
1435
+ }
1034
1436
  const completeQuestion = {
1035
1437
  id: generateUniqueId("saq_ai_"),
1036
1438
  questionType: "short_answer",
1037
1439
  prompt: aiGeneratedContent.prompt,
1038
1440
  acceptedAnswers: aiGeneratedContent.acceptedAnswers,
1039
- // Ưu tiên giá trị từ AI, nếu không có thì dùng giá trị đầu vào
1040
- isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
1441
+ isCaseSensitive: clientInput.isCaseSensitive,
1041
1442
  explanation: aiGeneratedContent.explanation,
1042
1443
  points: aiGeneratedContent.points,
1043
- topic: aiGeneratedContent.topic || clientInput.topic,
1044
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
1045
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1444
+ topic: aiGeneratedContent.topic || ((_h = clientInput.quizContext) == null ? void 0 : _h.originalTopic),
1445
+ difficulty: clientInput.difficulty,
1446
+ contextCode: (_i = clientInput.quizContext) == null ? void 0 : _i.plannedContextId,
1447
+ bloomLevel: (_j = clientInput.quizContext) == null ? void 0 : _j.plannedBloomLevel,
1448
+ learningObjective: (_k = clientInput.quizContext) == null ? void 0 : _k.originalLoId,
1449
+ subject: (_l = clientInput.quizContext) == null ? void 0 : _l.originalSubject,
1450
+ category: (_m = clientInput.quizContext) == null ? void 0 : _m.originalCategory,
1451
+ imageUrl: clientInput.imageUrl
1046
1452
  };
1047
- try {
1048
- const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
1049
- return { question: validatedQuestion };
1050
- } catch (validationError) {
1051
- console.error("Question validation failed:", validationError);
1052
- throw new Error(`Generated question failed validation: ${validationError}`);
1453
+ const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
1454
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
1455
+ console.log(`
1456
+ \u2705 Short Answer generation successful on attempt ${attempt} (${duration}ms)`);
1457
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
1458
+ return { question: validatedQuestion };
1459
+ } catch (error) {
1460
+ lastError = error;
1461
+ const duration = Date.now() - startTime;
1462
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1463
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS7;
1464
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
1465
+ if (willRetry) {
1466
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS7}ms...`);
1467
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS7));
1053
1468
  }
1054
- } else {
1055
- throw new Error("AI did not return content for the Short Answer question.");
1056
1469
  }
1057
- } catch (error) {
1058
- console.error("Error generating Short Answer question:", error);
1059
- throw new Error(`Failed to generate Short Answer question: ${error.message}`);
1060
1470
  }
1471
+ DebugLogger.logAttemptSummary(attemptResults);
1472
+ const errorMessage = `Failed to generate Short Answer question after ${MAX_RETRY_ATTEMPTS7} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
1473
+ console.error("\n\u274C Final Result: FAILED");
1474
+ console.error(errorMessage);
1475
+ return { error: errorMessage };
1061
1476
  }
1062
- function extractJsonFromMarkdown8(text) {
1063
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1064
- return match ? match[1].trim() : text.trim();
1065
- }
1066
- z.object({
1067
- topic: z.string().describe("The topic for the question."),
1068
- language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
1069
- // <-- ĐÃ THÊM
1070
- difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
1071
- contextDescription: z.string().optional().describe("A specific context or scenario for the question."),
1072
- selectedContextId: z.string().optional().describe("The ID of the selected context.")
1073
- });
1074
- z.object({
1075
- prompt: z.string().describe("The statement to be evaluated as true or false."),
1076
- correctAnswer: z.boolean().describe("The correct answer (true or false)."),
1077
- explanation: z.string().optional().describe("A brief explanation of why the answer is correct."),
1078
- points: z.number().optional().default(10),
1079
- difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1080
- topic: z.string().optional()
1081
- });
1082
- var TrueFalseQuestionZodSchema = z.object({
1083
- id: z.string(),
1084
- questionType: z.literal("true_false"),
1085
- prompt: z.string().min(1),
1477
+ BaseQuestionGenerationClientInputSchema.extend({});
1478
+ var AITrueFalseOutputFieldsSchema = z.object({
1479
+ prompt: z.string().describe("The statement that the user will evaluate as true or false."),
1086
1480
  correctAnswer: z.boolean(),
1087
- points: z.number().min(0).optional(),
1088
- explanation: z.string().optional(),
1089
- learningObjective: z.string().optional(),
1090
- glossary: z.array(z.string()).optional(),
1091
- bloomLevel: z.string().optional(),
1481
+ explanation: z.string().optional().describe("An explanation of why the statement is true or false, especially important if false."),
1482
+ points: z.number().optional().default(10),
1092
1483
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1093
- contextCode: z.string().optional(),
1094
- gradeBand: z.string().optional(),
1095
- course: z.string().optional(),
1096
- category: z.string().optional(),
1097
- topic: z.string().optional()
1484
+ topic: z.string().optional(),
1485
+ verifiedCategory: z.string().optional()
1486
+ // Thêm để xác thực
1098
1487
  });
1099
- z.object({
1100
- question: TrueFalseQuestionZodSchema.optional().describe("The generated True/False question.")
1101
- });
1102
- async function generateTrueFalseQuestion(clientInput, apiKey) {
1103
- try {
1104
- const ai = genkit({
1105
- plugins: [googleAI({ apiKey })],
1106
- model: gemini20Flash
1107
- });
1108
- const promptText = `You are an expert quiz question writer.
1109
- Generate a single True/False question in ${clientInput.language} based on the following inputs.
1110
1488
 
1111
- IMPORTANT: Return the response as JSON with this EXACT format:
1112
- {
1113
- "prompt": "The Earth is the fourth planet from the Sun.",
1114
- "correctAnswer": false,
1115
- "explanation": "The Earth is the third planet from the Sun. Mars is the fourth.",
1116
- "points": 10,
1117
- "difficulty": "easy",
1118
- "topic": "Astronomy"
1119
- }
1489
+ // src/ai/flows/question-gen/generate-true-false-question.ts
1490
+ var MAX_RETRY_ATTEMPTS8 = 3;
1491
+ var RETRY_DELAY_MS8 = 3e3;
1492
+ function buildEnhancedPrompt8(clientInput, attemptNumber) {
1493
+ const { quizContext, language, difficulty, imageUrl } = clientInput;
1494
+ const category = (quizContext == null ? void 0 : quizContext.originalCategory) || "the specified technical category";
1495
+ const attemptInfo = attemptNumber > 1 ? `
1496
+ ## DEBUG INFO - This is attempt #${attemptNumber}
1497
+ Previous attempts failed. Ensure the JSON is valid and 'correctAnswer' is a boolean.
1120
1498
 
1121
- Requirements:
1122
- - The 'prompt' must be a clear statement that is definitively true or false.
1123
- - 'correctAnswer' must be a boolean value (true or false).
1124
- - The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
1499
+ ` : "";
1500
+ const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The True/False statement must be directly related to the content of this image.` : "";
1501
+ const misconceptionGuidance = (quizContext == null ? void 0 : quizContext.targetMisconception) ? `**Target Misconception:** The statement you create MUST be FALSE and based on this common mistake: "${quizContext.targetMisconception}"` : "";
1502
+ const contextStrings = [
1503
+ `**Required Category:** ${category}`,
1504
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
1505
+ imageContextInstruction,
1506
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
1507
+ misconceptionGuidance,
1508
+ (quizContext == null ? void 0 : quizContext.difficultyReason) && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
1509
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
1510
+ const exampleJson = JSON.stringify({
1511
+ prompt: "In Swift, you must explicitly unwrap an Optional value before you can use its stored value.",
1512
+ correctAnswer: true,
1513
+ explanation: "Optional values in Swift represent the presence or absence of a value. To access the value when it exists, you must unwrap it using methods like 'if let', 'guard let', or the force unwrap operator '!'.",
1514
+ points: 10,
1515
+ difficulty: "easy",
1516
+ topic: "Swift Optionals",
1517
+ verifiedCategory: category
1518
+ }, null, 2);
1519
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
1520
+ Your mission is to create a high-quality, technically accurate True/False Question.
1125
1521
 
1126
- Topic: ${clientInput.topic}
1127
- Language: ${clientInput.language}
1128
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
1129
- Difficulty: ${clientInput.difficulty}
1522
+ ## Core Rules (Non-negotiable)
1523
+ 1. **Category Purity:** The statement ('prompt') MUST be exclusively about **${category}**.
1524
+ 2. **Clarity:** The statement must be definitively true or false, with no ambiguity.
1525
+ 3. **Misconception Priority:** If a Target Misconception is provided, the statement MUST be FALSE and reflect that misconception. This is a critical rule.
1526
+ 4. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
1130
1527
 
1131
- Return only the JSON response.`;
1132
- const response = await ai.generate(promptText);
1133
- const rawText = response.text;
1134
- const jsonText = extractJsonFromMarkdown8(rawText);
1135
- console.log("AI Response:", jsonText);
1136
- const aiGeneratedContent = JSON.parse(jsonText);
1137
- if (aiGeneratedContent) {
1528
+ ## CRITICAL CONTEXT FOR THIS QUESTION
1529
+ ${contextStrings}
1530
+
1531
+ ## Task: Generate the Question
1532
+ Based on all the rules and context above, generate a single True/False Question.
1533
+
1534
+ ### Input Parameters
1535
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
1536
+ - **Language for Text:** ${language}
1537
+ - **Difficulty Level:** ${difficulty}
1538
+
1539
+ ### Required JSON Output Format
1540
+ Your response must be ONLY the JSON object, matching this exact structure:
1541
+
1542
+ ${exampleJson}
1543
+
1544
+ Now, generate the JSON for the requested question.`;
1545
+ }
1546
+ async function generateTrueFalseQuestion(clientInput, apiKey) {
1547
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
1548
+ const ai = new GoogleGenAI({ apiKey });
1549
+ const model = "gemini-2.5-flash";
1550
+ const config = {
1551
+ temperature: 0.6,
1552
+ responseMimeType: "application/json",
1553
+ thinkingConfig: {
1554
+ thinkingBudget: 4e3
1555
+ }
1556
+ };
1557
+ const attemptResults = [];
1558
+ let lastError = null;
1559
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS8; attempt++) {
1560
+ const startTime = Date.now();
1561
+ const promptText = buildEnhancedPrompt8(clientInput, attempt);
1562
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1563
+ try {
1564
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
1565
+ const parts = [{ text: promptText }];
1566
+ if (clientInput.imageUrl) {
1567
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
1568
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
1569
+ parts.unshift(imagePart);
1570
+ }
1571
+ const contents = [{ role: "user", parts }];
1572
+ const aiResult = await ai.models.generateContent({ model, config, contents });
1573
+ const response = aiResult;
1574
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
1575
+ const duration = Date.now() - startTime;
1576
+ DebugLogger.logResponse(attempt, rawText);
1577
+ if (!rawText) throw new Error("AI returned an empty response.");
1578
+ const parsedJson = JSON.parse(rawText);
1579
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
1580
+ const aiGeneratedContent = AITrueFalseOutputFieldsSchema.parse(parsedJson);
1581
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
1582
+ if (((_f = clientInput.quizContext) == null ? void 0 : _f.targetMisconception) && aiGeneratedContent.correctAnswer === true) {
1583
+ throw new Error("AI failed to follow the Misconception Priority rule. The answer should have been false.");
1584
+ }
1585
+ if ((_g = clientInput.quizContext) == null ? void 0 : _g.originalCategory) {
1586
+ const verifiedCategory = (_h = aiGeneratedContent.verifiedCategory) == null ? void 0 : _h.toLowerCase();
1587
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
1588
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
1589
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
1590
+ }
1591
+ }
1138
1592
  const completeQuestion = {
1139
1593
  id: generateUniqueId("tf_ai_"),
1140
1594
  questionType: "true_false",
@@ -1142,164 +1596,1126 @@ Return only the JSON response.`;
1142
1596
  correctAnswer: aiGeneratedContent.correctAnswer,
1143
1597
  explanation: aiGeneratedContent.explanation,
1144
1598
  points: aiGeneratedContent.points,
1145
- topic: aiGeneratedContent.topic || clientInput.topic,
1146
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
1147
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1599
+ topic: aiGeneratedContent.topic || ((_i = clientInput.quizContext) == null ? void 0 : _i.originalTopic),
1600
+ difficulty: clientInput.difficulty,
1601
+ contextCode: (_j = clientInput.quizContext) == null ? void 0 : _j.plannedContextId,
1602
+ bloomLevel: (_k = clientInput.quizContext) == null ? void 0 : _k.plannedBloomLevel,
1603
+ learningObjective: (_l = clientInput.quizContext) == null ? void 0 : _l.originalLoId,
1604
+ subject: (_m = clientInput.quizContext) == null ? void 0 : _m.originalSubject,
1605
+ category: (_n = clientInput.quizContext) == null ? void 0 : _n.originalCategory,
1606
+ imageUrl: clientInput.imageUrl
1148
1607
  };
1149
- try {
1150
- const validatedQuestion = TrueFalseQuestionZodSchema.parse(completeQuestion);
1151
- return { question: validatedQuestion };
1152
- } catch (validationError) {
1153
- console.error("Question validation failed:", validationError);
1154
- throw new Error(`Generated question failed validation: ${validationError}`);
1608
+ const validatedQuestion = TrueFalseQuestionZodSchema.parse(completeQuestion);
1609
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
1610
+ console.log(`
1611
+ \u2705 True/False generation successful on attempt ${attempt} (${duration}ms)`);
1612
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
1613
+ return { question: validatedQuestion };
1614
+ } catch (error) {
1615
+ lastError = error;
1616
+ const duration = Date.now() - startTime;
1617
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1618
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS8;
1619
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
1620
+ if (willRetry) {
1621
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS8}ms...`);
1622
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS8));
1155
1623
  }
1156
- } else {
1157
- throw new Error("AI did not return content for the True/False question.");
1158
1624
  }
1159
- } catch (error) {
1160
- console.error("Error generating True/False question:", error);
1161
- throw new Error(`Failed to generate True/False question: ${error.message}`);
1162
1625
  }
1626
+ DebugLogger.logAttemptSummary(attemptResults);
1627
+ const errorMessage = `Failed to generate True/False question after ${MAX_RETRY_ATTEMPTS8} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
1628
+ console.error("\n\u274C Final Result: FAILED");
1629
+ console.error(errorMessage);
1630
+ return { error: errorMessage };
1163
1631
  }
1164
- function extractJsonFromMarkdown9(text) {
1165
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1166
- return match ? match[1].trim() : text.trim();
1167
- }
1168
- var fullQuizSupportedQuestionTypesArray = [
1169
- "true_false",
1170
- "multiple_choice",
1171
- "multiple_response",
1172
- "short_answer",
1173
- "numeric",
1174
- "fill_in_the_blanks",
1175
- "sequence",
1176
- "matching"
1177
- ];
1178
- var BloomLevelStringsEnum = z.enum(["remembering", "understanding", "applying"]);
1179
- z.object({
1180
- language: z.string().optional().default("English").describe('The language for the quiz plan (e.g., "Vietnamese", "English").'),
1181
- // <-- ĐÃ THÊM
1182
- totalQuestions: z.number().int().min(1).max(50),
1183
- topics: z.array(z.object({
1184
- topic: z.string().min(1),
1185
- ratio: z.number().min(0).max(100)
1186
- })).min(1),
1187
- bloomLevels: z.array(z.object({
1188
- level: BloomLevelStringsEnum,
1189
- ratio: z.number().min(0).max(100)
1190
- })).min(1),
1191
- selectedContextIds: z.array(z.string()).optional(),
1192
- selectedQuestionTypes: z.array(z.enum(fullQuizSupportedQuestionTypesArray)).min(1)
1193
- });
1194
- var PlannedQuestionSchema = z.object({
1195
- plannedTopic: z.string().min(1).describe("The specific topic for this question."),
1196
- plannedQuestionType: z.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
1197
- plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned.")
1198
- });
1199
- var GenerateQuizPlanOutputSchema = z.object({
1200
- quizPlan: z.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz.")
1201
- });
1202
- async function generateQuizPlan(clientInput, apiKey) {
1203
- try {
1204
- const totalTopicRatio = clientInput.topics.reduce((sum, t) => sum + t.ratio, 0);
1205
- if (Math.abs(totalTopicRatio - 100) > 1) {
1206
- throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
1632
+
1633
+ // src/utils/jsonUtils.ts
1634
+ var JsonRepairEngine = class {
1635
+ /**
1636
+ * Attempts to repair unterminated strings in JSON.
1637
+ * NOTE: This is a heuristic approach and may not be perfect for all cases.
1638
+ */
1639
+ static repairUnterminatedStrings(jsonStr) {
1640
+ let repaired = jsonStr;
1641
+ let inString = false;
1642
+ let escaped = false;
1643
+ let lastQuoteIndex = -1;
1644
+ for (let i = 0; i < repaired.length; i++) {
1645
+ const char = repaired[i];
1646
+ if (escaped) {
1647
+ escaped = false;
1648
+ continue;
1649
+ }
1650
+ if (char === "\\") {
1651
+ escaped = true;
1652
+ continue;
1653
+ }
1654
+ if (char === '"') {
1655
+ inString = !inString;
1656
+ if (inString) {
1657
+ lastQuoteIndex = i;
1658
+ }
1659
+ }
1207
1660
  }
1208
- const totalBloomRatio = clientInput.bloomLevels.reduce((sum, b) => sum + b.ratio, 0);
1209
- if (Math.abs(totalBloomRatio - 100) > 1) {
1210
- throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
1661
+ if (inString && lastQuoteIndex !== -1) {
1662
+ const beforeUnterminated = repaired.substring(0, lastQuoteIndex + 1);
1663
+ const afterUnterminated = repaired.substring(lastQuoteIndex + 1);
1664
+ const breakPoints = [",", "}", "]", "\n"];
1665
+ let breakIndex = -1;
1666
+ for (let i = 0; i < afterUnterminated.length; i++) {
1667
+ if (breakPoints.includes(afterUnterminated[i])) {
1668
+ breakIndex = i;
1669
+ break;
1670
+ }
1671
+ }
1672
+ if (breakIndex !== -1) {
1673
+ const stringContent = afterUnterminated.substring(0, breakIndex);
1674
+ const remainder = afterUnterminated.substring(breakIndex);
1675
+ const escapedContent = stringContent.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
1676
+ repaired = beforeUnterminated + escapedContent + '"' + remainder;
1677
+ } else {
1678
+ const escapedContent = afterUnterminated.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
1679
+ repaired = beforeUnterminated + escapedContent + '"';
1680
+ }
1211
1681
  }
1212
- const ai = genkit({
1213
- plugins: [googleAI({ apiKey })],
1214
- model: gemini20Flash
1682
+ return repaired;
1683
+ }
1684
+ // FIX: Replaced unsafe single quote replacement with a stateful parser.
1685
+ /**
1686
+ * Safely replaces single quotes with double quotes only for keys and string values,
1687
+ * ignoring apostrophes inside already double-quoted strings.
1688
+ */
1689
+ static safelyFixQuotes(jsonStr) {
1690
+ let result = "";
1691
+ let inDoubleQuoteString = false;
1692
+ let escaped = false;
1693
+ for (let i = 0; i < jsonStr.length; i++) {
1694
+ const char = jsonStr[i];
1695
+ if (escaped) {
1696
+ result += char;
1697
+ escaped = false;
1698
+ continue;
1699
+ }
1700
+ if (char === "\\") {
1701
+ escaped = true;
1702
+ result += char;
1703
+ continue;
1704
+ }
1705
+ if (char === '"') {
1706
+ inDoubleQuoteString = !inDoubleQuoteString;
1707
+ }
1708
+ if (char === "'" && !inDoubleQuoteString) {
1709
+ result += '"';
1710
+ } else {
1711
+ result += char;
1712
+ }
1713
+ }
1714
+ return result;
1715
+ }
1716
+ /**
1717
+ * Fixes common JSON formatting issues using more robust methods.
1718
+ */
1719
+ static applyCommonFixes(jsonStr) {
1720
+ let fixed = jsonStr;
1721
+ fixed = this.safelyFixQuotes(fixed);
1722
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
1723
+ fixed = fixed.replace(/("|}|\d|]|true|false|null)\s*\n\s*(")/g, "$1,\n$2");
1724
+ fixed = fixed.replace(/"[\s\S]*?"/g, (match) => {
1725
+ const content = match.substring(1, match.length - 1);
1726
+ const fixedContent = content.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1727
+ return `"${fixedContent}"`;
1215
1728
  });
1216
- const topicsDistribution = clientInput.topics.map((t) => `- Topic: "${t.topic}", Ratio: ${t.ratio}%`).join("\n ");
1217
- const bloomDistribution = clientInput.bloomLevels.map((b) => `- Level: "${b.level}", Ratio: ${b.ratio}%`).join("\n ");
1218
- const allowedQuestionTypes = clientInput.selectedQuestionTypes.map((t) => `'${t}'`).join(", ");
1219
- const promptText = `You are an expert educational content planner. Your task is to generate a detailed plan for a quiz in ${clientInput.language}.
1729
+ fixed = fixed.replace(/"(true|false|null)"/g, "$1");
1730
+ return fixed;
1731
+ }
1732
+ /**
1733
+ * Validates JSON by attempting to parse and providing detailed error info.
1734
+ */
1735
+ static validateAndGetError(jsonStr) {
1736
+ try {
1737
+ JSON.parse(jsonStr);
1738
+ return { isValid: true };
1739
+ } catch (error) {
1740
+ const errorMessage = error.message || "";
1741
+ const positionMatch = errorMessage.match(/position (\d+)/);
1742
+ const position = positionMatch ? parseInt(positionMatch[1], 10) : void 0;
1743
+ return {
1744
+ isValid: false,
1745
+ error: errorMessage,
1746
+ position
1747
+ };
1748
+ }
1749
+ }
1750
+ /**
1751
+ * Main repair function that attempts multiple strategies.
1752
+ */
1753
+ static repairJson(jsonStr) {
1754
+ var _a;
1755
+ let current = jsonStr.trim();
1756
+ const maxAttempts = 5;
1757
+ let lastError = "";
1758
+ let lastPosition = -1;
1759
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1760
+ const validation = this.validateAndGetError(current);
1761
+ if (validation.isValid) {
1762
+ return current;
1763
+ }
1764
+ console.warn(`JSON repair attempt ${attempt + 1}: ${validation.error}`);
1765
+ if (validation.error === lastError && validation.position === lastPosition) {
1766
+ console.error("Repair attempt stuck on the same error, aborting this strategy.");
1767
+ if (validation.position) {
1768
+ const truncated = current.substring(0, validation.position);
1769
+ const openBraces = (truncated.match(/{/g) || []).length;
1770
+ const closeBraces = (truncated.match(/}/g) || []).length;
1771
+ const openBrackets = (truncated.match(/\[/g) || []).length;
1772
+ const closeBrackets = (truncated.match(/\]/g) || []).length;
1773
+ let repaired = truncated.replace(/,\s*$/, "");
1774
+ for (let i = 0; i < openBrackets - closeBrackets; i++) repaired += "]";
1775
+ for (let i = 0; i < openBraces - closeBraces; i++) repaired += "}";
1776
+ current = repaired;
1777
+ const finalValidation = this.validateAndGetError(current);
1778
+ if (finalValidation.isValid) return current;
1779
+ }
1780
+ break;
1781
+ }
1782
+ lastError = validation.error || "";
1783
+ lastPosition = validation.position;
1784
+ if ((_a = validation.error) == null ? void 0 : _a.includes("Unterminated string")) {
1785
+ current = this.repairUnterminatedStrings(current);
1786
+ } else {
1787
+ current = this.applyCommonFixes(current);
1788
+ }
1789
+ }
1790
+ try {
1791
+ let finalAttempt = this.applyCommonFixes(jsonStr.trim());
1792
+ finalAttempt = this.repairUnterminatedStrings(finalAttempt);
1793
+ JSON.parse(finalAttempt);
1794
+ return finalAttempt;
1795
+ } catch (e) {
1796
+ throw new Error(`Unable to repair JSON after ${maxAttempts} attempts. Last known error: ${lastError}`);
1797
+ }
1798
+ }
1799
+ };
1800
+ function extractJsonFromMarkdown(text) {
1801
+ if (!text) {
1802
+ throw new Error("Input text is empty or null.");
1803
+ }
1804
+ const trimmedText = text.trim();
1805
+ try {
1806
+ JSON.parse(trimmedText);
1807
+ return trimmedText;
1808
+ } catch (e) {
1809
+ }
1810
+ const markdownPatterns = [
1811
+ /```(?:json|JSON)\s*([\s\S]*?)\s*```/,
1812
+ // ```json ... ```
1813
+ /```\s*({[\s\S]*?}|\[[\s\S]*?\])\s*```/
1814
+ // ``` { ... } ``` or ``` [ ... ] ```
1815
+ ];
1816
+ for (const pattern of markdownPatterns) {
1817
+ const match = trimmedText.match(pattern);
1818
+ if (match && match[1]) {
1819
+ const content = match[1].trim();
1820
+ try {
1821
+ JSON.parse(content);
1822
+ return content;
1823
+ } catch (e) {
1824
+ console.warn("JSON inside markdown block is invalid, attempting repair...");
1825
+ try {
1826
+ return JsonRepairEngine.repairJson(content);
1827
+ } catch (repairError) {
1828
+ console.warn(`Markdown block repair failed: ${repairError.message}. Trying other strategies...`);
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+ const firstBrace = trimmedText.indexOf("{");
1834
+ const firstBracket = trimmedText.indexOf("[");
1835
+ let startIndex = -1;
1836
+ if (firstBrace === -1 && firstBracket === -1) ; else if (firstBrace === -1) {
1837
+ startIndex = firstBracket;
1838
+ } else if (firstBracket === -1) {
1839
+ startIndex = firstBrace;
1840
+ } else {
1841
+ startIndex = Math.min(firstBrace, firstBracket);
1842
+ }
1843
+ if (startIndex !== -1) {
1844
+ const textToProcess = trimmedText.substring(startIndex);
1845
+ let balance = 0;
1846
+ let inString = false;
1847
+ let escaped = false;
1848
+ const startChar = textToProcess[0];
1849
+ const endChar = startChar === "{" ? "}" : "]";
1850
+ for (let i = 0; i < textToProcess.length; i++) {
1851
+ const char = textToProcess[i];
1852
+ if (escaped) {
1853
+ escaped = false;
1854
+ continue;
1855
+ }
1856
+ if (char === "\\") {
1857
+ escaped = true;
1858
+ continue;
1859
+ }
1860
+ if (char === '"') {
1861
+ inString = !inString;
1862
+ }
1863
+ if (!inString) {
1864
+ if (char === startChar) balance++;
1865
+ if (char === endChar) balance--;
1866
+ }
1867
+ if (balance === 0 && i > 0) {
1868
+ const potentialJson = textToProcess.substring(0, i + 1);
1869
+ try {
1870
+ JSON.parse(potentialJson);
1871
+ return potentialJson;
1872
+ } catch (e) {
1873
+ console.warn(`Balanced JSON segment is invalid, attempting repair...`);
1874
+ try {
1875
+ return JsonRepairEngine.repairJson(potentialJson);
1876
+ } catch (repairError) {
1877
+ console.warn(`Repair failed for balanced segment: ${repairError.message}`);
1878
+ }
1879
+ }
1880
+ break;
1881
+ }
1882
+ }
1883
+ }
1884
+ console.warn("All extraction strategies failed, attempting to repair the entire input text as a last resort.");
1885
+ try {
1886
+ return JsonRepairEngine.repairJson(trimmedText);
1887
+ } catch (finalError) {
1888
+ throw new Error(`Unable to extract or repair valid JSON from AI response. Preview: "${trimmedText.substring(0, 100)}...". Final error: ${finalError.message}`);
1889
+ }
1890
+ }
1891
+ var TopicWithMetadataSchema = z.object({
1892
+ topic: z.string().min(1),
1893
+ ratio: z.number().min(0).max(100),
1894
+ originalLoId: z.string().optional(),
1895
+ originalSubject: z.string().optional(),
1896
+ originalCategory: z.string().optional(),
1897
+ originalTopic: z.string().optional(),
1898
+ commonMisconceptions: z.array(z.string()).optional()
1899
+ });
1900
+ var BloomLevelStringsEnum = z.enum(["remembering", "understanding", "applying", "analyzing", "evaluating", "creating"]);
1901
+ var fullQuizSupportedQuestionTypesArray = [
1902
+ "true_false",
1903
+ "multiple_choice",
1904
+ "multiple_response",
1905
+ "short_answer",
1906
+ "numeric",
1907
+ "fill_in_the_blanks",
1908
+ "sequence",
1909
+ "matching",
1910
+ "drag_and_drop",
1911
+ "coding"
1912
+ ];
1913
+ z.object({
1914
+ language: z.string().optional().default("English"),
1915
+ totalQuestions: z.number().int().min(1).max(50),
1916
+ numCodingQuestions: z.number().optional().default(0),
1917
+ topics: z.array(TopicWithMetadataSchema).min(1),
1918
+ bloomLevels: z.array(z.object({
1919
+ level: BloomLevelStringsEnum,
1920
+ ratio: z.number().min(0).max(100)
1921
+ })).min(1),
1922
+ selectedContextIds: z.array(z.string()).optional(),
1923
+ selectedQuestionTypes: z.array(z.enum(fullQuizSupportedQuestionTypesArray)).min(1),
1924
+ imageContexts: z.array(z.custom()).optional().describe("Library of available image contexts for the AI to use.")
1925
+ });
1926
+ var PlannedQuestionSchema = z.object({
1927
+ plannedTopic: z.string().min(1).describe("The specific, assessable topic for this question."),
1928
+ plannedQuestionType: z.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
1929
+ plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned."),
1930
+ plannedContextId: z.string().optional().describe("The specific context ID chosen for this question."),
1931
+ imageId: z.string().nullable().optional().describe("The ID of the image from the context library to be used for this question."),
1932
+ targetMisconception: z.string().optional().describe("A specific common misconception this question should target."),
1933
+ difficultyReason: z.string().optional().describe("Strategic explanation of difficulty choice and placement."),
1934
+ topicSpecificity: z.enum(["broad", "focused", "specific"]).optional().describe("How specific the topic coverage should be."),
1935
+ originalLoId: z.string().optional(),
1936
+ originalSubject: z.string().optional(),
1937
+ originalCategory: z.string().optional(),
1938
+ originalTopic: z.string().optional()
1939
+ });
1940
+ var GenerateQuizPlanOutputSchema = z.object({
1941
+ quizPlan: z.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz."),
1942
+ diversityMetrics: z.object({
1943
+ questionTypeDistribution: z.record(z.number()).optional(),
1944
+ bloomLevelDistribution: z.record(z.number()).optional(),
1945
+ maxConsecutiveSameType: z.number().optional()
1946
+ }).optional().describe("Metrics showing the diversity achieved in the plan."),
1947
+ planningStrategy: z.object({
1948
+ overallApproach: z.string().optional(),
1949
+ keyDecisions: z.array(z.string()).optional()
1950
+ }).optional()
1951
+ });
1220
1952
 
1221
- IMPORTANT: Return the response as JSON with this EXACT format:
1222
- {
1223
- "quizPlan": [
1224
- { "plannedTopic": "Specific Topic A", "plannedQuestionType": "multiple_choice", "plannedBloomLevel": "remembering" },
1225
- { "plannedTopic": "Specific Topic B", "plannedQuestionType": "true_false", "plannedBloomLevel": "understanding" }
1226
- ]
1953
+ // src/ai/flows/generate-quiz-plan.ts
1954
+ var QuizPlanLogger = class {
1955
+ constructor() {
1956
+ this.logs = [];
1957
+ this.startTime = Date.now();
1958
+ }
1959
+ log(phase, data, duration) {
1960
+ this.logs.push({
1961
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1962
+ phase,
1963
+ data,
1964
+ duration
1965
+ });
1966
+ if (duration !== void 0) {
1967
+ console.log(`[${phase}] Completed in ${duration}ms:`, data);
1968
+ } else {
1969
+ console.log(`[${phase}]:`, data);
1970
+ }
1971
+ }
1972
+ getLogs() {
1973
+ return this.logs;
1974
+ }
1975
+ getTotalDuration() {
1976
+ return Date.now() - this.startTime;
1977
+ }
1978
+ };
1979
+ function generateQuestionTypeSelectionGuidance() {
1980
+ return `
1981
+ QUESTION TYPE SELECTION BEST PRACTICES:
1982
+
1983
+ **TRUE_FALSE (true_false)**
1984
+ - Best for: Binary concepts, fact verification, common misconceptions
1985
+ - Bloom levels: Primarily Remembering, Understanding
1986
+ - Use when: Testing definitive statements, clarifying misconceptions
1987
+ - Example: "Photosynthesis only occurs during daytime" (targets timing misconception)
1988
+
1989
+ **MULTIPLE CHOICE (multiple_choice)**
1990
+ - Best for: Concept selection, process understanding, comparison
1991
+ - Bloom levels: All levels, especially Understanding and Applying
1992
+ - Use when: Testing conceptual understanding with clear alternatives
1993
+ - Example: "Which factor most affects enzyme activity?" (applying knowledge)
1994
+
1995
+ **MULTIPLE RESPONSE (multiple_response)**
1996
+ - Best for: Identifying multiple correct factors, comprehensive understanding
1997
+ - Bloom levels: Understanding, Analyzing, Evaluating
1998
+ - Use when: Multiple correct answers exist, testing thorough knowledge
1999
+ - Example: "Select all factors that influence plant growth" (analyzing components)
2000
+
2001
+ **FILL IN THE BLANKS (fill_in_the_blanks)**
2002
+ - Best for: Key terminology, formulas, specific facts
2003
+ - Bloom levels: Remembering, Understanding
2004
+ - Use when: Testing precise recall of important terms/concepts
2005
+ - Example: "The process of _____ converts light energy to chemical energy"
2006
+
2007
+ **NUMERIC (numeric)**
2008
+ - Best for: Calculations, quantitative problems, formula application
2009
+ - Bloom levels: Applying, Analyzing
2010
+ - Use when: Mathematical computations are required
2011
+ - Example: "Calculate the molarity of a 2L solution containing 0.5 moles of NaCl"
2012
+
2013
+ **MATCHING (matching)**
2014
+ - Best for: Connecting related concepts, terminology pairs
2015
+ - Bloom levels: Remembering, Understanding
2016
+ - Use when: Testing relationships between terms and definitions
2017
+ - Example: Match organelles with their functions
2018
+
2019
+ **SEQUENCE (sequence)**
2020
+ - Best for: Process steps, chronological order, procedural knowledge
2021
+ - Bloom levels: Understanding, Applying, Analyzing
2022
+ - Use when: Order or sequence is critical to understanding
2023
+ - Example: "Arrange the steps of mitosis in correct order"
2024
+
2025
+ **DRAG AND DROP (drag_and_drop)**
2026
+ - Best for: Categorization, classification, spatial relationships
2027
+ - Bloom levels: Understanding, Applying, Analyzing
2028
+ - Use when: Grouping or organizing information is key
2029
+ - Example: "Classify these compounds as acids, bases, or neutral"
2030
+
2031
+ **SHORT ANSWER (short_answer)**
2032
+ - Best for: Explanations, definitions, problem-solving steps
2033
+ - Bloom levels: Understanding, Applying, Analyzing, Evaluating, Creating
2034
+ - Use when: Requiring explanatory responses, open-ended thinking
2035
+ - Example: "Explain why enzymes are specific to certain substrates"
2036
+
2037
+ **CODING (coding)**
2038
+ - Best for: Programming problems, algorithm implementation
2039
+ - Bloom levels: Applying, Analyzing, Evaluating, Creating
2040
+ - Use when: Technical implementation or code analysis is required
2041
+ - Example: "Write a function to calculate factorial recursively"
2042
+ `;
2043
+ }
2044
+ function generateAdvancedBloomGuidance() {
2045
+ return `
2046
+ ADVANCED BLOOM'S TAXONOMY & QUESTION TYPE OPTIMIZATION:
2047
+
2048
+ **REMEMBERING (Knowledge Recall)**
2049
+ - Primary types: true_false, fill_in_the_blanks, matching
2050
+ - Secondary types: multiple_choice (simple recall)
2051
+ - Focus: Facts, terms, basic concepts, definitions
2052
+ - Misconception addressing: Use true_false to clarify common confusions
2053
+
2054
+ **UNDERSTANDING (Comprehension)**
2055
+ - Primary types: multiple_choice, short_answer, matching
2056
+ - Secondary types: multiple_response, sequence
2057
+ - Focus: Explanations, interpretations, examples, classifications
2058
+ - Best for: "Explain why...", "What does this mean...", "Give an example..."
2059
+
2060
+ **APPLYING (Using Knowledge)**
2061
+ - Primary types: numeric, short_answer, sequence, coding
2062
+ - Secondary types: multiple_choice, drag_and_drop
2063
+ - Focus: Problem-solving, implementing procedures, using methods
2064
+ - Best for: Calculations, step-by-step processes, practical applications
2065
+
2066
+ **ANALYZING (Breaking Down Information)**
2067
+ - Primary types: multiple_response, short_answer, sequence, coding
2068
+ - Secondary types: drag_and_drop, multiple_choice
2069
+ - Focus: Identifying components, relationships, cause-effect
2070
+ - Best for: "What are the factors...", "How do these relate...", "Break down..."
2071
+
2072
+ **EVALUATING (Making Judgments)**
2073
+ - Primary types: short_answer, multiple_response, coding
2074
+ - Secondary types: multiple_choice (with justification)
2075
+ - Focus: Critiquing, judging, comparing alternatives, decision-making
2076
+ - Best for: "Which is better and why...", "Evaluate the approach...", "Critique..."
2077
+
2078
+ **CREATING (Producing New Work)**
2079
+ - Primary types: short_answer, coding, sequence
2080
+ - Secondary types: drag_and_drop (design tasks)
2081
+ - Focus: Designing, planning, producing, constructing
2082
+ - Best for: "Design a solution...", "Create a plan...", "Develop a strategy..."
2083
+ `;
1227
2084
  }
2085
+ function generateDiversityRules() {
2086
+ return `
2087
+ ENHANCED DIVERSITY & QUALITY ASSURANCE RULES:
2088
+
2089
+ **Question Type Distribution Strategy:**
2090
+ 1. Never place more than 3 consecutive questions of the same type
2091
+ 2. Distribute question types based on their cognitive complexity
2092
+ 3. For quizzes with 10+ questions, use at least 4 different question types
2093
+ 4. For quizzes with 20+ questions, use at least 6 different question types
2094
+ 5. Balance quick-answer types (true_false, multiple_choice) with deeper types (short_answer, coding)
2095
+
2096
+ **Intelligent Difficulty Progression:**
2097
+ 1. Opening (20%): Start with confidence-building questions (Remembering/Understanding)
2098
+ 2. Building (40%): Gradually increase complexity (Understanding/Applying)
2099
+ 3. Peak (30%): Most challenging questions (Analyzing/Evaluating/Creating)
2100
+ 4. Closing (10%): Moderate challenge to end positively
1228
2101
 
1229
- Constraints and Guidelines:
1230
- 1. **Total Questions**: The output 'quizPlan' array must contain exactly ${clientInput.totalQuestions} elements.
1231
- 2. **Topic Distribution**: Distribute the questions across the following topics according to their specified ratios. Match these ratios as closely as possible.
2102
+ **Misconception-Driven Planning:**
2103
+ - When common misconceptions are provided, prioritize question types that can effectively address them
2104
+ - Use true_false for binary misconceptions
2105
+ - Use multiple_choice for concept selection misconceptions
2106
+ - Use short_answer for complex misconceptions requiring explanation
2107
+
2108
+ **Contextual Intelligence:**
2109
+ - Programming/Technical topics: Favor coding, numeric, short_answer
2110
+ - Process-oriented topics: Favor sequence, short_answer, drag_and_drop
2111
+ - Conceptual topics: Favor multiple_choice, multiple_response, true_false
2112
+ - Factual topics: Favor fill_in_the_blanks, matching, true_false
2113
+ `;
2114
+ }
2115
+ async function generateQuizPlan(clientInput, apiKey, imageContexts = []) {
2116
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
2117
+ const logger = new QuizPlanLogger();
2118
+ try {
2119
+ logger.log("VALIDATION_START", {
2120
+ totalQuestions: clientInput.totalQuestions,
2121
+ availableTypes: clientInput.selectedQuestionTypes,
2122
+ topicCount: clientInput.topics.length,
2123
+ bloomLevelCount: clientInput.bloomLevels.length
2124
+ });
2125
+ const totalTopicRatio = clientInput.topics.reduce((sum, t) => sum + t.ratio, 0);
2126
+ if (Math.abs(totalTopicRatio - 100) > 1) {
2127
+ throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
2128
+ }
2129
+ const totalBloomRatio = clientInput.bloomLevels.reduce((sum, b) => sum + b.ratio, 0);
2130
+ if (Math.abs(totalBloomRatio - 100) > 1) {
2131
+ throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
2132
+ }
2133
+ logger.log("VALIDATION_SUCCESS", {
2134
+ topicRatioSum: totalTopicRatio,
2135
+ bloomRatioSum: totalBloomRatio
2136
+ });
2137
+ const aiStartTime = Date.now();
2138
+ const ai = new GoogleGenAI({
2139
+ apiKey
2140
+ });
2141
+ const model = "gemini-2.5-pro";
2142
+ const config = {
2143
+ temperature: 0.8,
2144
+ thinkingConfig: {
2145
+ thinkingBudget: 4096
2146
+ },
2147
+ responseMimeType: "application/json"
2148
+ };
2149
+ logger.log("AI_INITIALIZATION", { model }, Date.now() - aiStartTime);
2150
+ const { language, totalQuestions, numCodingQuestions = 0 } = clientInput;
2151
+ const promptStartTime = Date.now();
2152
+ const topicsDistribution = clientInput.topics.map((t) => {
2153
+ let topicString = `- Topic Context: "${t.topic}", LoId: "${t.originalLoId || "nil"}", Subject: "${t.originalSubject || "nil"}", Category: "${t.originalCategory || "nil"}", Topic: "${t.originalTopic || "nil"}", Ratio: ${t.ratio}%`;
2154
+ if (t.commonMisconceptions && t.commonMisconceptions.length > 0) {
2155
+ topicString += `
2156
+ - Common Misconceptions: [${t.commonMisconceptions.join(", ")}]`;
2157
+ }
2158
+ return topicString;
2159
+ }).join("\n ");
2160
+ const bloomDistribution = clientInput.bloomLevels.map(
2161
+ (b) => `- Level: "${b.level}", Ratio: ${b.ratio}%`
2162
+ ).join("\n ");
2163
+ let questionTypesForPrompt = [...clientInput.selectedQuestionTypes];
2164
+ if (numCodingQuestions === 0) {
2165
+ questionTypesForPrompt = questionTypesForPrompt.filter(
2166
+ (type) => type !== "short_answer" && type !== "coding"
2167
+ );
2168
+ }
2169
+ const allowedQuestionTypes = questionTypesForPrompt.map((t) => `'${t}'`).join(", ");
2170
+ const codingRequirement = numCodingQuestions > 0 ? `
2171
+ **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.` : "";
2172
+ const imageContextSection = imageContexts && imageContexts.length > 0 ? `
2173
+ ## AVAILABLE IMAGE CONTEXT LIBRARY
2174
+ 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.
2175
+
2176
+ \`\`\`json
2177
+ ${JSON.stringify(imageContexts.map((img) => ({ imageId: img.id, subject: img.subject, category: img.category, topic: img.topic, description: img.detailedDescription })), null, 2)}
2178
+ \`\`\`
2179
+ ` : "";
2180
+ 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.
2181
+
2182
+ ${generateQuestionTypeSelectionGuidance()}
2183
+
2184
+ ${generateAdvancedBloomGuidance()}
2185
+
2186
+ ${generateDiversityRules()}
2187
+
2188
+ ## COMPREHENSIVE QUIZ REQUIREMENTS:
2189
+
2190
+ **Target Language**: ${language}
2191
+ **Total Questions**: ${totalQuestions}${codingRequirement}
2192
+
2193
+ **Topic Distribution & Learning Context** (follow precisely):
1232
2194
  ${topicsDistribution}
1233
- 3. **Bloom Level Distribution**: Distribute the questions across the following Bloom's Taxonomy levels according to their specified ratios.
2195
+
2196
+ **Cognitive Complexity Distribution** (follow precisely):
1234
2197
  ${bloomDistribution}
1235
- 4. **Allowed Question Types**: For each planned question, 'plannedQuestionType' must be one of these types: ${allowedQuestionTypes}. Use a variety of these types.
1236
- 5. **Topic Specificity**: The 'plannedTopic' for each question should be a specific sub-topic or aspect related to one of the main topics provided, and must be in ${clientInput.language}.
1237
2198
 
1238
- Carefully calculate the number of questions for each topic and Bloom level based on the total number of questions and the provided ratios. If ratios lead to fractional questions, round to the nearest whole number while ensuring the total number of questions remains exactly ${clientInput.totalQuestions}.
1239
- The final 'quizPlan' array must have exactly ${clientInput.totalQuestions} elements.
2199
+ **Available Question Arsenal**: ${allowedQuestionTypes}
1240
2200
 
1241
- Return only the JSON response.`;
1242
- const response = await ai.generate(promptText);
1243
- const rawText = response.text;
1244
- const jsonText = extractJsonFromMarkdown9(rawText);
1245
- console.log("AI Response:", jsonText);
1246
- const aiGeneratedContent = JSON.parse(jsonText);
1247
- if (!aiGeneratedContent || !aiGeneratedContent.quizPlan) {
1248
- throw new Error("AI did not return a valid quiz plan structure.");
2201
+ **Image Resources**: ${imageContextSection}
2202
+
2203
+ ## STRATEGIC PLANNING METHODOLOGY:
2204
+
2205
+ 1. **Misconception Analysis**: If common misconceptions are provided, design questions specifically to address and correct them
2206
+ 2. **Question Type Intelligence**: Select question types based on the cognitive demands and content nature
2207
+ 3. **Difficulty Orchestration**: Create a learning journey that builds confidence while challenging appropriately
2208
+ 4. **Diversity Optimization**: Ensure variety prevents monotony and maintains engagement
2209
+ 5. **Context Sensitivity**: Match question types to topic characteristics (technical, conceptual, procedural)
2210
+ 6. **Image Context Integration**: Intelligently associate questions with relevant images from the provided library to enhance contextual understanding.
2211
+
2212
+ ## ENHANCED OUTPUT FORMAT:
2213
+
2214
+ Return ONLY a JSON object with this EXACT structure:
2215
+
2216
+ \`\`\`json
2217
+ {
2218
+ "quizPlan": [
2219
+ {
2220
+ "plannedTopic": "Specific, assessable topic derived from provided context",
2221
+ "plannedQuestionType": "question_type_from_allowed_list",
2222
+ "plannedBloomLevel": "bloom_level_from_requirements",
2223
+ "plannedContextId": "THEO_ABS",
2224
+ "imageId": "imgctx_12345abcde", // or null
2225
+ "targetMisconception": "Specific misconception this question addresses (or 'none' if not applicable)",
2226
+ "difficultyReason": "Strategic explanation of difficulty choice and placement",
2227
+ "topicSpecificity": "broad|focused|specific",
2228
+ "originalLoId": "corresponding_LoId_from_input",
2229
+ "originalSubject": "corresponding_Subject_from_input",
2230
+ "originalCategory": "corresponding_Category_from_input",
2231
+ "originalTopic": "corresponding_Topic_from_input"
2232
+ }
2233
+ ],
2234
+ "diversityMetrics": {
2235
+ "questionTypeDistribution": {"type1": count1, "type2": count2},
2236
+ "bloomLevelDistribution": {"level1": count1, "level2": count2},
2237
+ "maxConsecutiveSameType": number,
2238
+ "difficultyProgression": "description of how difficulty progresses",
2239
+ "misconceptionCoverage": number_of_misconceptions_addressed
2240
+ },
2241
+ "planningStrategy": {
2242
+ "overallApproach": "Brief description of the strategic approach taken",
2243
+ "keyDecisions": ["Major decision 1", "Major decision 2", "Major decision 3"]
2244
+ }
2245
+ }
2246
+ \`\`\`
2247
+
2248
+ Execute this plan with pedagogical precision. The quiz should feel like a carefully crafted learning journey that challenges and educates simultaneously.`;
2249
+ logger.log("PROMPT_PREPARATION", {
2250
+ promptLength: enhancedPromptText.length,
2251
+ topicCount: clientInput.topics.length,
2252
+ misconceptionCount: clientInput.topics.reduce((sum, t) => {
2253
+ var _a2;
2254
+ return sum + (((_a2 = t.commonMisconceptions) == null ? void 0 : _a2.length) || 0);
2255
+ }, 0)
2256
+ }, Date.now() - promptStartTime);
2257
+ const generationStartTime = Date.now();
2258
+ const contents = [
2259
+ {
2260
+ role: "user",
2261
+ parts: [
2262
+ {
2263
+ text: enhancedPromptText
2264
+ }
2265
+ ]
2266
+ }
2267
+ ];
2268
+ const aiResult = await ai.models.generateContent({
2269
+ model,
2270
+ config,
2271
+ contents
2272
+ });
2273
+ const response = aiResult;
2274
+ const generationDuration = Date.now() - generationStartTime;
2275
+ logger.log("AI_GENERATION", {
2276
+ responseLength: ((_f = (_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) == null ? void 0 : _f.length) || 0,
2277
+ duration: generationDuration
2278
+ }, generationDuration);
2279
+ const processingStartTime = Date.now();
2280
+ const rawText = ((_k = (_j = (_i = (_h = (_g = response.candidates) == null ? void 0 : _g[0]) == null ? void 0 : _h.content) == null ? void 0 : _i.parts) == null ? void 0 : _j[0]) == null ? void 0 : _k.text) || "";
2281
+ let jsonText = rawText;
2282
+ if (!rawText.trim().startsWith("{") && !rawText.trim().startsWith("[")) {
2283
+ jsonText = extractJsonFromMarkdown(rawText);
1249
2284
  }
2285
+ logger.log("JSON_EXTRACTION", {
2286
+ rawTextLength: rawText.length,
2287
+ extractedJsonLength: jsonText.length
2288
+ });
2289
+ const aiGeneratedContent = GenerateQuizPlanOutputSchema.parse(JSON.parse(jsonText));
2290
+ logger.log("SCHEMA_VALIDATION", { success: true }, Date.now() - processingStartTime);
2291
+ const validationStartTime = Date.now();
1250
2292
  if (aiGeneratedContent.quizPlan.length !== clientInput.totalQuestions) {
1251
- throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested. The plan must match the total number of questions.`);
2293
+ throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested.`);
1252
2294
  }
2295
+ const invalidTypes = [];
1253
2296
  aiGeneratedContent.quizPlan.forEach((item, index) => {
1254
2297
  if (!clientInput.selectedQuestionTypes.includes(item.plannedQuestionType)) {
1255
- throw new Error(`AI planned question ${index + 1} with a disallowed question type: '${item.plannedQuestionType}'`);
2298
+ invalidTypes.push(`Question ${index + 1}: '${item.plannedQuestionType}'`);
1256
2299
  }
1257
2300
  });
1258
- try {
1259
- const validatedPlan = GenerateQuizPlanOutputSchema.parse(aiGeneratedContent);
1260
- return validatedPlan;
1261
- } catch (validationError) {
1262
- console.error("Quiz plan validation failed:", validationError);
1263
- throw new Error(`Generated quiz plan failed validation: ${validationError}`);
2301
+ if (invalidTypes.length > 0) {
2302
+ throw new Error(`Invalid question types found: ${invalidTypes.join(", ")}`);
1264
2303
  }
2304
+ const codingQuestions = aiGeneratedContent.quizPlan.filter((q) => q.plannedQuestionType === "coding");
2305
+ if (numCodingQuestions > 0 && codingQuestions.length !== numCodingQuestions) {
2306
+ throw new Error(`Expected ${numCodingQuestions} coding questions, but got ${codingQuestions.length}`);
2307
+ }
2308
+ const diversityAnalysis = validateConsecutiveTypes(aiGeneratedContent.quizPlan);
2309
+ logger.log("VALIDATION_COMPLETE", {
2310
+ questionCount: aiGeneratedContent.quizPlan.length,
2311
+ codingQuestionCount: codingQuestions.length,
2312
+ maxConsecutiveType: diversityAnalysis.maxConsecutive,
2313
+ questionTypeDistribution: ((_l = aiGeneratedContent.diversityMetrics) == null ? void 0 : _l.questionTypeDistribution) || {}
2314
+ }, Date.now() - validationStartTime);
2315
+ const finalResult = __spreadProps(__spreadValues({}, aiGeneratedContent), {
2316
+ logs: logger.getLogs()
2317
+ });
2318
+ logger.log("GENERATION_COMPLETE", {
2319
+ totalDuration: logger.getTotalDuration(),
2320
+ success: true,
2321
+ finalQuestionCount: finalResult.quizPlan.length
2322
+ }, logger.getTotalDuration());
2323
+ console.log("\n=== QUIZ PLAN GENERATION SUMMARY ===");
2324
+ console.log(`\u2705 Successfully generated ${finalResult.quizPlan.length} questions`);
2325
+ console.log(`\u23F1\uFE0F Total generation time: ${logger.getTotalDuration()}ms`);
2326
+ console.log(`\u{1F3AF} Question types: ${Object.keys(((_m = finalResult.diversityMetrics) == null ? void 0 : _m.questionTypeDistribution) || {}).join(", ")}`);
2327
+ console.log(`\u{1F9E0} Bloom levels: ${Object.keys(((_n = finalResult.diversityMetrics) == null ? void 0 : _n.bloomLevelDistribution) || {}).join(", ")}`);
2328
+ if (numCodingQuestions > 0) {
2329
+ console.log(`\u{1F4BB} Coding questions: ${codingQuestions.length}/${numCodingQuestions} required`);
2330
+ }
2331
+ console.log(JSON.stringify(finalResult));
2332
+ console.log("=====================================\n");
2333
+ return finalResult;
1265
2334
  } catch (error) {
1266
- console.error("Error generating Quiz Plan:", error);
2335
+ logger.log("ERROR", {
2336
+ message: error.message,
2337
+ stack: error.stack,
2338
+ totalDuration: logger.getTotalDuration()
2339
+ });
2340
+ console.error("\u274C Quiz Plan Generation Failed:", error.message);
2341
+ console.error("\u{1F4CB} Full logs available in returned object");
1267
2342
  throw new Error(`Failed to generate Quiz Plan: ${error.message}`);
1268
2343
  }
1269
2344
  }
1270
- var internalContextOptions = [
1271
- // ... (giữ nguyên mảng này)
1272
- { contextId: "THEO_ABS", contextDescription: "L\xFD thuy\u1EBFt/Tr\u1EEBu t\u01B0\u1EE3ng" },
1273
- { contextId: "SPEC_CASE", contextDescription: "V\xED d\u1EE5 C\u1EE5 th\u1EC3/Tr\u01B0\u1EDDng h\u1EE3p Ri\xEAng" },
1274
- { contextId: "NAT_OBS", contextDescription: "Hi\u1EC7n t\u01B0\u1EE3ng T\u1EF1 nhi\xEAn/Quan s\xE1t" },
1275
- { contextId: "TECH_ENG", contextDescription: "\u1EE8ng d\u1EE5ng C\xF4ng ngh\u1EC7/K\u1EF9 thu\u1EADt" },
1276
- { contextId: "EXP_INV", contextDescription: "Th\xED nghi\u1EC7m/\u0110i\u1EC1u tra Khoa h\u1ECDc" },
1277
- { contextId: "REAL_PROB", contextDescription: "V\u1EA5n \u0111\u1EC1 Th\u1EF1c t\u1EBF/X\xE3 h\u1ED9i/M\xF4i tr\u01B0\u1EDDng" },
1278
- { contextId: "DATA_MOD", contextDescription: "Di\u1EC5n gi\u1EA3i D\u1EEF li\u1EC7u/M\xF4 h\xECnh h\xF3a" },
1279
- { contextId: "HIST_SCI", contextDescription: "L\u1ECBch s\u1EED/Ph\xE1t tri\u1EC3n Khoa h\u1ECDc" },
1280
- { contextId: "INTERDISC", contextDescription: "Li\xEAn ng\xE0nh (Interdisciplinary)" },
1281
- { contextId: "HYPO_COMP", contextDescription: "Gi\u1EA3 \u0111\u1ECBnh/So s\xE1nh T\xECnh hu\u1ED1ng" }
2345
+ function validateConsecutiveTypes(quizPlan) {
2346
+ var _a, _b;
2347
+ let maxConsecutive = 1;
2348
+ let maxType = ((_a = quizPlan[0]) == null ? void 0 : _a.plannedQuestionType) || "";
2349
+ let maxStartIndex = 0;
2350
+ let currentConsecutive = 1;
2351
+ let currentType = ((_b = quizPlan[0]) == null ? void 0 : _b.plannedQuestionType) || "";
2352
+ let currentStartIndex = 0;
2353
+ for (let i = 1; i < quizPlan.length; i++) {
2354
+ if (quizPlan[i].plannedQuestionType === currentType) {
2355
+ currentConsecutive++;
2356
+ } else {
2357
+ if (currentConsecutive > maxConsecutive) {
2358
+ maxConsecutive = currentConsecutive;
2359
+ maxType = currentType;
2360
+ maxStartIndex = currentStartIndex;
2361
+ }
2362
+ currentConsecutive = 1;
2363
+ currentType = quizPlan[i].plannedQuestionType;
2364
+ currentStartIndex = i;
2365
+ }
2366
+ }
2367
+ if (currentConsecutive > maxConsecutive) {
2368
+ maxConsecutive = currentConsecutive;
2369
+ maxType = currentType;
2370
+ maxStartIndex = currentStartIndex;
2371
+ }
2372
+ return {
2373
+ maxConsecutive,
2374
+ type: maxType,
2375
+ startIndex: maxStartIndex
2376
+ };
2377
+ }
2378
+
2379
+ // src/services/TopicDataService.ts
2380
+ var TopicDataService = class {
2381
+ /**
2382
+ * Saves an array of LearningObjective objects to Local Storage, overwriting existing data.
2383
+ * @param data The array of learning objectives to save.
2384
+ */
2385
+ static saveData(data) {
2386
+ try {
2387
+ if (typeof window === "undefined") return;
2388
+ const serializedData = JSON.stringify(data);
2389
+ localStorage.setItem(this.STORAGE_KEY, serializedData);
2390
+ } catch (error) {
2391
+ console.error("Error saving learning objectives to Local Storage:", error);
2392
+ }
2393
+ }
2394
+ /**
2395
+ * Merges a new set of learning objectives with the existing data in Local Storage.
2396
+ * If an LO ID from newData already exists, it will be updated. Otherwise, it will be added.
2397
+ * @param newData The array of new or updated learning objectives.
2398
+ */
2399
+ static mergeData(newData) {
2400
+ const existingData = this.getData();
2401
+ const loMap = new Map(existingData.map((lo) => [lo.loId, lo]));
2402
+ newData.forEach((newLo) => {
2403
+ loMap.set(newLo.loId, newLo);
2404
+ });
2405
+ const mergedData = Array.from(loMap.values());
2406
+ this.saveData(mergedData);
2407
+ }
2408
+ /**
2409
+ * Retrieves the array of LearningObjective objects from Local Storage.
2410
+ * @returns An array of learning objectives, or an empty array if none are found or an error occurs.
2411
+ */
2412
+ static getData() {
2413
+ try {
2414
+ if (typeof window === "undefined") return [];
2415
+ const storedData = localStorage.getItem(this.STORAGE_KEY);
2416
+ return storedData ? JSON.parse(storedData) : [];
2417
+ } catch (error) {
2418
+ console.error("Error retrieving learning objectives from Local Storage:", error);
2419
+ this.clearData();
2420
+ return [];
2421
+ }
2422
+ }
2423
+ /**
2424
+ * Removes all learning objective data from Local Storage.
2425
+ */
2426
+ static clearData() {
2427
+ try {
2428
+ if (typeof window === "undefined") return;
2429
+ localStorage.removeItem(this.STORAGE_KEY);
2430
+ } catch (error) {
2431
+ console.error("Error clearing learning objectives from Local Storage:", error);
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Parses TSV content into an array of LearningObjective objects.
2436
+ * @param tsvContent The raw string content from a .tsv file.
2437
+ * @returns An object containing the successfully parsed data and any errors encountered.
2438
+ */
2439
+ static parseTSV(tsvContent) {
2440
+ const lines = tsvContent.split("\n").filter((line) => line.trim() !== "");
2441
+ if (lines.length < 2) {
2442
+ return { data: [], errors: ["File is empty or contains only a header."] };
2443
+ }
2444
+ const headerLine = lines.shift();
2445
+ const headers = headerLine.split(" ").map((h) => h.trim());
2446
+ if (headers.length !== this.EXPECTED_HEADERS.length || !this.EXPECTED_HEADERS.every((h, i) => h === headers[i])) {
2447
+ const errorMsg = `Invalid TSV header. Expected: "${this.EXPECTED_HEADERS.join(" ")}". Received: "${headers.join(" ")}"`;
2448
+ return { data: [], errors: [errorMsg] };
2449
+ }
2450
+ const data = [];
2451
+ const errors = [];
2452
+ lines.forEach((line, index) => {
2453
+ const values = line.split(" ").map((v) => v.trim());
2454
+ if (values.length !== this.EXPECTED_HEADERS.length) {
2455
+ errors.push(`Line ${index + 2}: Incorrect number of columns. Expected ${this.EXPECTED_HEADERS.length}, but got ${values.length}.`);
2456
+ return;
2457
+ }
2458
+ const [
2459
+ loId,
2460
+ loDescription,
2461
+ subject,
2462
+ category,
2463
+ topic,
2464
+ keywordsStr,
2465
+ grade,
2466
+ stemElementsStr,
2467
+ bloomLevelsStr
2468
+ ] = values;
2469
+ if (!loId || !subject || !category || !topic) {
2470
+ errors.push(`Line ${index + 2}: Missing required fields (LO ID, Subject, Category, or Topic).`);
2471
+ return;
2472
+ }
2473
+ const learningObjective = {
2474
+ loId,
2475
+ loDescription,
2476
+ subject,
2477
+ category,
2478
+ topic,
2479
+ keywords: keywordsStr.split(",").map((k) => k.trim()).filter(Boolean),
2480
+ grade,
2481
+ stemElements: stemElementsStr.split(",").map((s) => s.trim()).filter(Boolean),
2482
+ bloomLevelsGuideline: bloomLevelsStr.split(",").map((b) => b.trim()).filter(Boolean)
2483
+ };
2484
+ data.push(learningObjective);
2485
+ });
2486
+ return { data, errors };
2487
+ }
2488
+ /**
2489
+ * Gets a unique list of all subjects from the stored data.
2490
+ * @returns An array of subject strings.
2491
+ */
2492
+ static getSubjects() {
2493
+ const data = this.getData();
2494
+ const subjects = data.map((item) => item.subject);
2495
+ return [...new Set(subjects)].sort();
2496
+ }
2497
+ /**
2498
+ * Gets a unique list of categories for a given subject.
2499
+ * @param subject The subject to filter by.
2500
+ * @returns An array of category strings.
2501
+ */
2502
+ static getCategoriesBySubject(subject) {
2503
+ const data = this.getData();
2504
+ const categories = data.filter((item) => item.subject === subject).map((item) => item.category);
2505
+ return [...new Set(categories)].sort();
2506
+ }
2507
+ /**
2508
+ * Gets a unique list of topics for a given category.
2509
+ * @param category The category to filter by.
2510
+ * @returns An array of topic strings.
2511
+ */
2512
+ static getTopicsByCategory(category) {
2513
+ const data = this.getData();
2514
+ const topics = data.filter((item) => item.category === category).map((item) => item.topic);
2515
+ return [...new Set(topics)].sort();
2516
+ }
2517
+ /**
2518
+ * Retrieves all LearningObjective details for a given list of topics.
2519
+ * @param topics An array of topic strings to search for.
2520
+ * @returns An array of matching LearningObjective objects.
2521
+ */
2522
+ static getLearningObjectivesByTopics(topics) {
2523
+ const data = this.getData();
2524
+ const topicSet = new Set(topics);
2525
+ return data.filter((item) => topicSet.has(item.topic));
2526
+ }
2527
+ };
2528
+ TopicDataService.STORAGE_KEY = "interactive_quiz_kit_learning_objectives";
2529
+ TopicDataService.EXPECTED_HEADERS = [
2530
+ "LO ID",
2531
+ "LO Description",
2532
+ "Subject",
2533
+ "Category",
2534
+ "Topic",
2535
+ "Keywords",
2536
+ "Grade",
2537
+ "STEM Element(s)",
2538
+ "Bloom\u2019s Level(s) Guideline"
1282
2539
  ];
1283
- var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomContextInput) => {
1284
- let contextScore = 1;
1285
- const selectedContext = contextId ? internalContextOptions.find((c) => c.contextId === contextId) : void 0;
1286
- if (selectedContext) {
1287
- if (["THEO_ABS", "HIST_SCI"].includes(selectedContext.contextId)) contextScore = 1;
1288
- else if (["SPEC_CASE", "NAT_OBS", "DATA_MOD", "INTERDISC", "HYPO_COMP"].includes(selectedContext.contextId)) contextScore = 2;
1289
- else if (["TECH_ENG", "EXP_INV", "REAL_PROB"].includes(selectedContext.contextId)) contextScore = 3;
1290
- } else if (generalCustomContextInput == null ? void 0 : generalCustomContextInput.trim()) {
1291
- contextScore = 2;
2540
+ BaseQuestionGenerationClientInputSchema.extend({
2541
+ codingLanguage: z.enum(["cpp", "javascript", "python", "swift", "csharp"])
2542
+ });
2543
+ var AICodingQuestionOutputSchema = z.object({
2544
+ prompt: z.string().describe("The problem description for the user."),
2545
+ functionSignature: z.string().optional().describe("A suggested function signature for the user to implement."),
2546
+ solutionCode: z.string().describe("A complete, correct model solution in the specified language."),
2547
+ testCases: z.array(z.object({
2548
+ input: z.array(z.any()),
2549
+ expectedOutput: z.any().describe("The expected output for this test case - REQUIRED"),
2550
+ isPublic: z.boolean()
2551
+ })).min(3, { message: "Must provide at least 3 test cases." }),
2552
+ verifiedCodingLanguage: z.enum(["cpp", "javascript", "python", "swift", "csharp"]).optional().describe("The programming language this question actually addresses.")
2553
+ });
2554
+
2555
+ // src/ai/flows/question-gen/generate-coding-question.ts
2556
+ var MAX_RETRY_ATTEMPTS9 = 3;
2557
+ var RETRY_DELAY_MS9 = 3e3;
2558
+ function buildEnhancedPrompt9(clientInput, attemptNumber) {
2559
+ const { quizContext, difficulty, codingLanguage, language, imageUrl } = clientInput;
2560
+ const subject = (quizContext == null ? void 0 : quizContext.originalSubject) || codingLanguage;
2561
+ const attemptInfo = attemptNumber > 1 ? `
2562
+ ## DEBUG INFO - This is attempt #${attemptNumber}
2563
+ Previous attempts failed. Pay strict attention to the JSON schema and all rules.
2564
+
2565
+ ` : "";
2566
+ const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The coding problem must be directly related to processing or interpreting the content of this image.` : "";
2567
+ const contextStrings = [
2568
+ `**Subject:** ${subject}`,
2569
+ (quizContext == null ? void 0 : quizContext.loDescription) && `**Learning Objective:** ${quizContext.loDescription}`,
2570
+ imageContextInstruction,
2571
+ (quizContext == null ? void 0 : quizContext.plannedBloomLevel) && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
2572
+ (quizContext == null ? void 0 : quizContext.targetMisconception) && `**Target Misconception:** The problem should test against this common error: "${quizContext.targetMisconception}"`
2573
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
2574
+ const exampleJson = JSON.stringify({
2575
+ prompt: "Write a function named 'add' that takes two integers and returns their sum.",
2576
+ functionSignature: "function add(a, b) { ... }",
2577
+ solutionCode: "function add(a, b) {\n return a + b;\n}",
2578
+ testCases: [
2579
+ { "input": [1, 2], "expectedOutput": 3, "isPublic": true },
2580
+ { "input": [-1, 1], "expectedOutput": 0, "isPublic": true },
2581
+ { "input": [0, 0], "expectedOutput": 0, "isPublic": false }
2582
+ ],
2583
+ verifiedCodingLanguage: "javascript"
2584
+ }, null, 2);
2585
+ return `${attemptInfo}You are an expert programming problem designer for ${subject}.
2586
+ Generate a single, high-quality Coding question.
2587
+
2588
+ ## Core Rules
2589
+ 1. **Language Purity:** All code ('functionSignature', 'solutionCode') MUST be in **${codingLanguage}**.
2590
+ 2. **Context Adherence:** The problem MUST be directly related to the provided context.
2591
+ 3. **Format Integrity:** You MUST return ONLY a single, valid JSON object.
2592
+
2593
+ ## CRITICAL CONTEXT FOR THIS QUESTION
2594
+ ${contextStrings}
2595
+
2596
+ ## Task: Generate the Question
2597
+ ### Input Parameters
2598
+ - **Topic for Question:** ${(quizContext == null ? void 0 : quizContext.plannedTopic) || "General"}
2599
+ - **Natural Language for Text:** ${language}
2600
+ - **Coding Language:** ${codingLanguage}
2601
+ - **Difficulty Level:** ${difficulty}
2602
+
2603
+ ### Required JSON Output Format
2604
+ Your response must be ONLY the JSON object, matching this exact structure:
2605
+
2606
+ ${exampleJson}
2607
+
2608
+ Now, generate the JSON for the requested question.`;
2609
+ }
2610
+ async function generateCodingQuestion(clientInput, apiKey) {
2611
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
2612
+ const ai = new GoogleGenAI({ apiKey });
2613
+ const model = "gemini-2.5-flash";
2614
+ const config = {
2615
+ temperature: 0.5,
2616
+ responseMimeType: "application/json",
2617
+ thinkingConfig: {
2618
+ thinkingBudget: 5e3
2619
+ }
2620
+ };
2621
+ const attemptResults = [];
2622
+ let lastError = null;
2623
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS9; attempt++) {
2624
+ const startTime = Date.now();
2625
+ const promptText = buildEnhancedPrompt9(clientInput, attempt);
2626
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
2627
+ try {
2628
+ DebugLogger.logPrompt(attempt, promptText, __spreadProps(__spreadValues({}, clientInput), { attemptNumber: attempt, promptHash }));
2629
+ const parts = [{ text: promptText }];
2630
+ if (clientInput.imageUrl) {
2631
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
2632
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
2633
+ parts.unshift(imagePart);
2634
+ }
2635
+ const contents = [{ role: "user", parts }];
2636
+ const aiResult = await ai.models.generateContent({ model, config, contents });
2637
+ const response = aiResult;
2638
+ const rawText = ((_e = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.text) || "";
2639
+ const duration = Date.now() - startTime;
2640
+ DebugLogger.logResponse(attempt, rawText);
2641
+ if (!rawText) throw new Error("AI returned an empty response.");
2642
+ const parsedJson = JSON.parse(rawText);
2643
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
2644
+ const aiGeneratedContent = AICodingQuestionOutputSchema.parse(parsedJson);
2645
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
2646
+ if (aiGeneratedContent.verifiedCodingLanguage && aiGeneratedContent.verifiedCodingLanguage !== clientInput.codingLanguage) {
2647
+ throw new Error(`Language mismatch: Required ${clientInput.codingLanguage}, but AI generated for ${aiGeneratedContent.verifiedCodingLanguage}.`);
2648
+ }
2649
+ const testCases = aiGeneratedContent.testCases.map((tc) => {
2650
+ if (tc.expectedOutput === void 0) {
2651
+ throw new Error("A test case from AI is missing the required 'expectedOutput' field.");
2652
+ }
2653
+ return {
2654
+ id: generateUniqueId("tc_"),
2655
+ input: tc.input,
2656
+ expectedOutput: tc.expectedOutput,
2657
+ isPublic: tc.isPublic
2658
+ };
2659
+ });
2660
+ const completeQuestion = {
2661
+ id: generateUniqueId("coding_"),
2662
+ questionType: "coding",
2663
+ prompt: aiGeneratedContent.prompt,
2664
+ codingLanguage: clientInput.codingLanguage,
2665
+ // FIXED: Correct field name
2666
+ functionSignature: aiGeneratedContent.functionSignature,
2667
+ solutionCode: aiGeneratedContent.solutionCode,
2668
+ testCases,
2669
+ points: 25,
2670
+ topic: (_f = clientInput.quizContext) == null ? void 0 : _f.originalTopic,
2671
+ difficulty: clientInput.difficulty,
2672
+ contextCode: (_g = clientInput.quizContext) == null ? void 0 : _g.plannedContextId,
2673
+ bloomLevel: (_h = clientInput.quizContext) == null ? void 0 : _h.plannedBloomLevel,
2674
+ learningObjective: (_i = clientInput.quizContext) == null ? void 0 : _i.originalLoId,
2675
+ subject: (_j = clientInput.quizContext) == null ? void 0 : _j.originalSubject,
2676
+ category: (_k = clientInput.quizContext) == null ? void 0 : _k.originalCategory,
2677
+ imageUrl: clientInput.imageUrl
2678
+ };
2679
+ const validatedQuestion = CodingQuestionZodSchema.parse(completeQuestion);
2680
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
2681
+ console.log(`
2682
+ \u2705 Coding question generation successful on attempt ${attempt} (${duration}ms)`);
2683
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
2684
+ return { question: validatedQuestion };
2685
+ } catch (error) {
2686
+ lastError = error;
2687
+ const duration = Date.now() - startTime;
2688
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
2689
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS9;
2690
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
2691
+ if (willRetry) {
2692
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS9}ms...`);
2693
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS9));
2694
+ }
2695
+ }
1292
2696
  }
2697
+ DebugLogger.logAttemptSummary(attemptResults);
2698
+ const errorMessage = `Failed to generate Coding question after ${MAX_RETRY_ATTEMPTS9} attempts. Last error: ${lastError == null ? void 0 : lastError.message}`;
2699
+ console.error("\n\u274C Final Result: FAILED");
2700
+ console.error(errorMessage);
2701
+ return { error: errorMessage };
2702
+ }
2703
+
2704
+ // src/ai/flows/generate-questions-from-quiz-plan.ts
2705
+ var MAX_ATTEMPTS = 3;
2706
+ var RETRY_DELAY_MS10 = 2e3;
2707
+ var delay = (ms) => new Promise((res) => setTimeout(res, ms));
2708
+ var calculateCombinedDifficulty = (plannedQ) => {
2709
+ const { plannedBloomLevel, plannedQuestionType, plannedContextId } = plannedQ;
2710
+ let contextScore = 1;
2711
+ if (["SPEC_CASE", "NAT_OBS", "DATA_MOD", "INTERDISC", "HYPO_COMP"].includes(plannedContextId || "")) contextScore = 2;
2712
+ else if (["TECH_ENG", "EXP_INV", "REAL_PROB"].includes(plannedContextId || "")) contextScore = 3;
1293
2713
  let bloomScore = 1;
1294
- if (bloomLevel === "understanding") bloomScore = 2;
1295
- else if (bloomLevel === "applying") bloomScore = 3;
2714
+ if (plannedBloomLevel === "understanding") bloomScore = 2;
2715
+ else if (plannedBloomLevel === "applying") bloomScore = 3;
2716
+ else if (["analyzing", "evaluating", "creating"].includes(plannedBloomLevel)) bloomScore = 4;
1296
2717
  let questionTypeScore = 1;
1297
- switch (qType) {
1298
- case "true_false":
1299
- case "multiple_choice":
1300
- case "short_answer":
1301
- questionTypeScore = 1;
1302
- break;
2718
+ switch (plannedQuestionType) {
1303
2719
  case "matching":
1304
2720
  case "fill_in_the_blanks":
1305
2721
  case "numeric":
@@ -1308,131 +2724,853 @@ var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomCo
1308
2724
  case "sequence":
1309
2725
  case "multiple_response":
1310
2726
  case "drag_and_drop":
2727
+ case "coding":
1311
2728
  questionTypeScore = 3;
1312
2729
  break;
1313
- default:
1314
- questionTypeScore = 1;
1315
2730
  }
1316
2731
  const totalScore = bloomScore + contextScore + questionTypeScore;
1317
2732
  if (totalScore <= 4) return "easy";
1318
- if (totalScore <= 6) return "medium";
2733
+ if (totalScore <= 7) return "medium";
1319
2734
  return "hard";
1320
2735
  };
1321
- var PlannedQuestionSchema2 = z.object({
1322
- plannedTopic: z.string().min(1),
1323
- plannedQuestionType: z.string(),
1324
- // Giữ dạng string để linh hoạt, sẽ kiểm tra trong logic
1325
- plannedBloomLevel: z.enum(["remembering", "understanding", "applying"]),
1326
- plannedContextId: z.string().optional()
1327
- });
1328
- z.object({
1329
- quizPlan: z.array(PlannedQuestionSchema2).min(1),
1330
- language: z.string().optional().default("English").describe("The language for the generated questions."),
1331
- // <-- ĐÃ THÊM
1332
- selectedContextIds: z.array(z.string()).optional(),
1333
- customContextInput: z.string().optional()
1334
- });
1335
- var GenerationErrorSchema = z.object({
1336
- plannedQuestionIndex: z.number(),
1337
- plannedTopic: z.string(),
1338
- plannedQuestionType: z.string(),
1339
- error: z.string()
1340
- });
1341
- z.object({
1342
- generatedQuestions: z.array(z.any()),
1343
- // z.any() là thực tế vì union của tất cả các loại câu hỏi rất phức tạp
1344
- errors: z.array(GenerationErrorSchema).optional()
1345
- });
1346
2736
  async function generateQuestionsFromQuizPlan(clientInput, apiKey) {
1347
2737
  var _a, _b;
1348
- const { quizPlan, selectedContextIds, customContextInput, language } = clientInput;
2738
+ const { quizPlan, language, imageContexts } = clientInput;
1349
2739
  const generatedQuestions = [];
1350
2740
  const errors = [];
2741
+ const allLearningObjectives = TopicDataService.getData();
1351
2742
  for (let i = 0; i < quizPlan.length; i++) {
1352
2743
  const plannedQ = quizPlan[i];
1353
- let generationError = null;
1354
- try {
1355
- let contextDescriptionForAI;
1356
- let contextIdForDifficultyCalc = plannedQ.plannedContextId;
1357
- if (plannedQ.plannedContextId && plannedQ.plannedContextId !== "__custom__") {
1358
- contextDescriptionForAI = (_a = internalContextOptions.find((c) => c.contextId === plannedQ.plannedContextId)) == null ? void 0 : _a.contextDescription;
1359
- } else if (plannedQ.plannedContextId === "__custom__") {
1360
- contextDescriptionForAI = customContextInput == null ? void 0 : customContextInput.trim();
1361
- } else if ((selectedContextIds == null ? void 0 : selectedContextIds[0]) && selectedContextIds[0] !== "__custom__") {
1362
- contextDescriptionForAI = (_b = internalContextOptions.find((c) => c.contextId === selectedContextIds[0])) == null ? void 0 : _b.contextDescription;
1363
- if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = selectedContextIds[0];
1364
- } else if (customContextInput == null ? void 0 : customContextInput.trim()) {
1365
- contextDescriptionForAI = customContextInput.trim();
1366
- if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = "__custom__";
1367
- }
1368
- const difficultyForAI = calculateCombinedDifficulty(
1369
- contextIdForDifficultyCalc,
1370
- plannedQ.plannedBloomLevel,
1371
- plannedQ.plannedQuestionType,
1372
- contextDescriptionForAI
1373
- );
1374
- const baseClientInput = {
1375
- topic: plannedQ.plannedTopic,
1376
- language,
1377
- // <-- TRUYỀN `language` VÀO
1378
- difficulty: difficultyForAI,
1379
- contextDescription: contextDescriptionForAI,
1380
- selectedContextId: plannedQ.plannedContextId || contextIdForDifficultyCalc
1381
- };
1382
- let result = {};
1383
- switch (plannedQ.plannedQuestionType) {
1384
- case "true_false":
1385
- result = await generateTrueFalseQuestion(baseClientInput, apiKey);
1386
- break;
1387
- case "multiple_choice":
1388
- result = await generateMCQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 4 }), apiKey);
1389
- break;
1390
- case "multiple_response":
1391
- result = await generateMRQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 5, minCorrectAnswers: 2, maxCorrectAnswers: 3 }), apiKey);
1392
- break;
1393
- case "short_answer":
1394
- result = await generateShortAnswerQuestion(__spreadProps(__spreadValues({}, baseClientInput), { isCaseSensitive: false }), apiKey);
1395
- break;
1396
- case "numeric":
1397
- result = await generateNumericQuestion(__spreadProps(__spreadValues({}, baseClientInput), { allowDecimals: true }), apiKey);
1398
- break;
1399
- case "fill_in_the_blanks":
1400
- result = await generateFillInTheBlanksQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfBlanks: 2, isCaseSensitive: false }), apiKey);
1401
- break;
1402
- case "sequence":
1403
- result = await generateSequenceQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfItems: 4 }), apiKey);
1404
- break;
1405
- case "matching":
1406
- result = await generateMatchingQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfPairs: 4, shuffleOptions: true }), apiKey);
2744
+ let questionGenerated = false;
2745
+ let lastError = null;
2746
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
2747
+ try {
2748
+ const fullLO = plannedQ.originalLoId ? allLearningObjectives.find((lo) => lo.loId === plannedQ.originalLoId) : null;
2749
+ const quizContext = {
2750
+ plannedTopic: plannedQ.plannedTopic,
2751
+ plannedQuestionType: plannedQ.plannedQuestionType,
2752
+ plannedBloomLevel: plannedQ.plannedBloomLevel,
2753
+ plannedContextId: plannedQ.plannedContextId,
2754
+ targetMisconception: plannedQ.targetMisconception,
2755
+ difficultyReason: plannedQ.difficultyReason,
2756
+ topicSpecificity: plannedQ.topicSpecificity,
2757
+ originalLoId: plannedQ.originalLoId,
2758
+ originalSubject: plannedQ.originalSubject,
2759
+ originalCategory: plannedQ.originalCategory,
2760
+ originalTopic: plannedQ.originalTopic,
2761
+ loDescription: (fullLO == null ? void 0 : fullLO.loDescription) || plannedQ.plannedTopic
2762
+ };
2763
+ const imageUrl = plannedQ.imageId && imageContexts ? (_a = imageContexts.find((ctx) => ctx.id === plannedQ.imageId)) == null ? void 0 : _a.imageUrl : void 0;
2764
+ const baseClientInput = {
2765
+ language,
2766
+ difficulty: calculateCombinedDifficulty(plannedQ),
2767
+ quizContext,
2768
+ imageUrl
2769
+ };
2770
+ let result = {};
2771
+ switch (plannedQ.plannedQuestionType) {
2772
+ case "true_false":
2773
+ result = await generateTrueFalseQuestion(baseClientInput, apiKey);
2774
+ break;
2775
+ case "multiple_choice":
2776
+ result = await generateMCQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 4 }), apiKey);
2777
+ break;
2778
+ case "multiple_response":
2779
+ result = await generateMRQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 5, minCorrectAnswers: 2, maxCorrectAnswers: 3 }), apiKey);
2780
+ break;
2781
+ case "short_answer":
2782
+ result = await generateShortAnswerQuestion(__spreadProps(__spreadValues({}, baseClientInput), { isCaseSensitive: false }), apiKey);
2783
+ break;
2784
+ case "numeric":
2785
+ result = await generateNumericQuestion(__spreadProps(__spreadValues({}, baseClientInput), { allowDecimals: true, tolerance: 0 }), apiKey);
2786
+ break;
2787
+ case "fill_in_the_blanks":
2788
+ result = await generateFillInTheBlanksQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfBlanks: 2, isCaseSensitive: false }), apiKey);
2789
+ break;
2790
+ case "sequence":
2791
+ result = await generateSequenceQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfItems: 4 }), apiKey);
2792
+ break;
2793
+ case "matching":
2794
+ result = await generateMatchingQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfPairs: 4, shuffleOptions: true }), apiKey);
2795
+ break;
2796
+ case "coding": {
2797
+ const subject = ((_b = plannedQ.originalSubject) == null ? void 0 : _b.toLowerCase()) || "";
2798
+ let codingLanguage = "javascript";
2799
+ if (subject.includes("swift")) codingLanguage = "swift";
2800
+ else if (subject.includes("python")) codingLanguage = "python";
2801
+ result = await generateCodingQuestion(__spreadProps(__spreadValues({}, baseClientInput), {
2802
+ codingLanguage,
2803
+ displayLanguage: language || "English"
2804
+ }), apiKey);
2805
+ break;
2806
+ }
2807
+ default:
2808
+ throw new Error(`Question type "${plannedQ.plannedQuestionType}" is not supported for automated generation.`);
2809
+ }
2810
+ if (result.error) {
2811
+ throw new Error(result.error);
2812
+ }
2813
+ if (result.question) {
2814
+ const question = result.question;
2815
+ question.learningObjective = plannedQ.originalLoId;
2816
+ question.subject = plannedQ.originalSubject;
2817
+ question.category = plannedQ.originalCategory;
2818
+ question.topic = plannedQ.originalTopic;
2819
+ question.bloomLevel = plannedQ.plannedBloomLevel;
2820
+ if (question.points === void 0) question.points = 10;
2821
+ generatedQuestions.push(question);
2822
+ questionGenerated = true;
1407
2823
  break;
1408
- default:
1409
- generationError = `Question type "${plannedQ.plannedQuestionType}" is not supported for automated generation.`;
1410
- }
1411
- if (result.question) {
1412
- const question = result.question;
1413
- question.topic = plannedQ.plannedTopic;
1414
- question.bloomLevel = plannedQ.plannedBloomLevel;
1415
- question.difficulty = difficultyForAI;
1416
- question.contextCode = baseClientInput.selectedContextId;
1417
- if (question.points === void 0) question.points = 10;
1418
- generatedQuestions.push(question);
1419
- } else if (!generationError) {
1420
- generationError = `AI did not return a question object for type '${plannedQ.plannedQuestionType}'.`;
2824
+ } else {
2825
+ throw new Error(`AI did not return a question object for type '${plannedQ.plannedQuestionType}'.`);
2826
+ }
2827
+ } catch (e) {
2828
+ lastError = e;
2829
+ console.warn(`Attempt ${attempt} failed for question ${i + 1} (Topic: ${plannedQ.plannedTopic}): ${e.message}`);
2830
+ if (attempt < MAX_ATTEMPTS) {
2831
+ await delay(RETRY_DELAY_MS10);
2832
+ }
1421
2833
  }
1422
- } catch (e) {
1423
- generationError = e.message || `An unknown error occurred.`;
1424
2834
  }
1425
- if (generationError) {
1426
- console.error(`Error generating question at index ${i} (Topic: ${plannedQ.plannedTopic}): ${generationError}`);
2835
+ if (!questionGenerated && lastError) {
1427
2836
  errors.push({
1428
2837
  plannedQuestionIndex: i,
1429
2838
  plannedTopic: plannedQ.plannedTopic,
1430
2839
  plannedQuestionType: plannedQ.plannedQuestionType,
1431
- error: generationError
2840
+ error: lastError.message || "Unknown error after all retries."
1432
2841
  });
1433
2842
  }
1434
2843
  }
1435
2844
  return { generatedQuestions, errors: errors.length > 0 ? errors : void 0 };
1436
2845
  }
2846
+ var QuestionResultForAISchema = z.object({
2847
+ questionId: z.string(),
2848
+ prompt: z.string(),
2849
+ isCorrect: z.boolean(),
2850
+ userAnswer: z.any().optional(),
2851
+ correctAnswer: z.any().optional(),
2852
+ evaluationDetails: z.array(z.custom()).optional().describe("Detailed results for each test case, for coding questions.")
2853
+ });
2854
+ z.object({
2855
+ language: z.string().describe('The language for the generated review (e.g., "Vietnamese", "English").'),
2856
+ questionResults: z.array(QuestionResultForAISchema).min(1)
2857
+ });
2858
+ var AIQuizReviewOutputSchema = z.object({
2859
+ questionReviews: z.array(
2860
+ z.object({
2861
+ questionId: z.string().describe("The ID of the question being reviewed."),
2862
+ explanation: z.string().describe("A detailed, personalized explanation for this question in Markdown format.")
2863
+ })
2864
+ ).describe("An array of reviews, one for each question."),
2865
+ overallSummary: z.string().describe("A comprehensive summary of key concepts in Markdown format."),
2866
+ relatedTopics: z.array(z.string()).describe("A list of suggested topics or keywords for further study.")
2867
+ });
2868
+
2869
+ // src/ai/flows/generate-quiz-review.ts
2870
+ async function generateQuizReview(clientInput, apiKey) {
2871
+ try {
2872
+ const ai = genkit({
2873
+ plugins: [googleAI({ apiKey })],
2874
+ model: gemini20Flash
2875
+ });
2876
+ const resultsString = JSON.stringify(clientInput.questionResults, null, 2);
2877
+ const promptText = `
2878
+ You are an expert educational tutor. Your task is to analyze a student's quiz results and provide a detailed, helpful review in ${clientInput.language}.
2879
+
2880
+ **Here are the student's quiz results:**
2881
+ ${resultsString}
2882
+
2883
+ **Instructions:**
2884
+ 1. **Analyze Each Question:** For every question in the results, create a corresponding object in the "questionReviews" array. Match the "questionId".
2885
+ 2. **Write Personalized Explanations:** In the "explanation" for each question, provide a clear and encouraging explanation.
2886
+ - **For standard questions:** Explain why the correct answer is right and, if the user was wrong, gently explain their misconception.
2887
+ - **CRITICAL - For CODING questions (those with 'evaluationDetails'):**
2888
+ - If the user was correct ('isCorrect': true), congratulate them and briefly praise their solution.
2889
+ - If the user was incorrect, analyze the 'evaluationDetails'. Find the FIRST failed test case. Explain WHY their code failed that specific test case. For example: "Your code works for positive numbers, but it failed on the test case with an empty array. It seems you haven't handled that edge case."
2890
+ - DO NOT just repeat the test case data. Provide a pedagogical explanation.
2891
+ 3. **Create an Overall Summary:** In "overallSummary", synthesize the main ideas from all questions into a cohesive review. Focus on reinforcing the main learning points, especially those related to questions the user answered incorrectly.
2892
+ 4. **Suggest Further Study:** In "relatedTopics", provide 2-3 specific keywords or topics for further study.
2893
+ 5. **Language and Formatting:** Ensure all generated text is in ${clientInput.language} and uses Markdown.
2894
+
2895
+ **JSON OUTPUT FORMAT:**
2896
+ Return a single, valid JSON object with this EXACT format.
2897
+
2898
+ \`\`\`json
2899
+ {
2900
+ "questionReviews": [
2901
+ {
2902
+ "questionId": "some-question-id-1",
2903
+ "explanation": "Your detailed explanation for the first question goes here."
2904
+ }
2905
+ ],
2906
+ "overallSummary": "A comprehensive summary of the key concepts from the quiz.",
2907
+ "relatedTopics": ["Keyword for further study 1", "Related Concept 2"]
2908
+ }
2909
+ \`\`\`
2910
+
2911
+ Return only the valid JSON response.`;
2912
+ const response = await ai.generate(promptText);
2913
+ const rawText = response.text;
2914
+ const jsonText = extractJsonFromMarkdown(rawText);
2915
+ const aiGeneratedContent = JSON.parse(jsonText);
2916
+ const validatedOutput = AIQuizReviewOutputSchema.parse(aiGeneratedContent);
2917
+ return validatedOutput;
2918
+ } catch (error) {
2919
+ console.error("Error generating Quiz Review:", error);
2920
+ if (error instanceof z.ZodError) {
2921
+ throw new Error(`AI returned data in an unexpected format. Validation failed: ${error.message}`);
2922
+ }
2923
+ throw new Error(`Failed to generate Quiz Review: ${error.message}`);
2924
+ }
2925
+ }
2926
+ z.object({
2927
+ language: z.string(),
2928
+ userName: z.string().optional(),
2929
+ performanceByTopic: z.array(z.object({
2930
+ name: z.string(),
2931
+ averageScore: z.number()
2932
+ })).optional(),
2933
+ recentHistory: z.array(z.object({
2934
+ quizTitle: z.string(),
2935
+ percentage: z.number(),
2936
+ topics: z.array(z.object({
2937
+ subject: z.string(),
2938
+ category: z.string(),
2939
+ topic: z.string()
2940
+ }))
2941
+ })).optional(),
2942
+ allAvailableTopics: z.array(z.object({
2943
+ loId: z.string(),
2944
+ subject: z.string(),
2945
+ category: z.string(),
2946
+ topic: z.string()
2947
+ }))
2948
+ });
2949
+ var PracticeSuggestionOutputSchema = z.object({
2950
+ suggestionText: z.string().describe("The personalized message from the AI tutor in Markdown format."),
2951
+ suggestedTopics: z.array(z.object({
2952
+ loId: z.string(),
2953
+ topicName: z.string(),
2954
+ reason: z.enum(["review", "explore"]),
2955
+ suggestedDifficulty: z.enum(["Very Easy", "Easy", "Medium", "Hard", "Expert"])
2956
+ }))
2957
+ });
2958
+ async function generatePracticeSuggestion(clientInput, apiKey) {
2959
+ try {
2960
+ const ai = genkit({
2961
+ plugins: [googleAI({ apiKey })],
2962
+ model: gemini20Flash
2963
+ });
2964
+ const { language, userName, performanceByTopic, recentHistory, allAvailableTopics } = clientInput;
2965
+ const promptText = `
2966
+ You are a friendly and insightful AI Learning Coach. Your goal is to provide a personalized practice suggestion for a student named ${userName || "there"}.
2967
+
2968
+ **Analyze the following student data:**
2969
+
2970
+ 1. **Performance by Topic (Weaknesses):**
2971
+ ${JSON.stringify(performanceByTopic, null, 2)}
2972
+
2973
+ 2. **Recent Practice Sessions (Strengths & Recent Activity):**
2974
+ ${JSON.stringify(recentHistory, null, 2)}
2975
+
2976
+ 3. **All Available Topics in the Curriculum:**
2977
+ ${JSON.stringify(allAvailableTopics.slice(0, 50), null, 2)}... (and more)
2978
+
2979
+ **Your Task:**
2980
+ Based on the data, create a helpful and encouraging practice plan. Follow these steps in your reasoning:
2981
+ 1. **Identify Weaknesses:** Look at "Performance by Topic". Identify 1-2 topics with the lowest 'averageScore'. These are candidates for 'review'.
2982
+ 2. **Identify Strengths/Interests:** Look at "Recent Practice Sessions". Identify 1-2 topics the student has recently practiced and performed well on (e.g., score > 80%).
2983
+ 3. **Suggest Related Topics:** From the "All Available Topics" list, find 1-2 new topics that are in the same 'category' or 'subject' as the student's strengths. These are candidates for 'explore'.
2984
+ 4. **Suggest Difficulty:** For each suggested topic, determine an appropriate difficulty level ('Easy', 'Medium', or 'Hard').
2985
+ - If the reason is 'review', suggest 'Easy' or 'Medium' to help them rebuild their foundation.
2986
+ - If the reason is 'explore', suggest 'Medium' as a starting point for a new challenge.
2987
+ 5. **Synthesize a Plan:** Combine the topics for 'review' and 'explore' into a balanced suggestion.
2988
+ 6. **Write a Personalized Message:** In 'suggestionText', write a friendly message in ${language}. Explain WHY you are suggesting these topics and difficulties.
2989
+
2990
+ **JSON OUTPUT FORMAT:**
2991
+ Return a single, valid JSON object in this exact format. All text must be in ${language}.
2992
+
2993
+ {
2994
+ "suggestionText": "Ch\xE0o ${userName || "b\u1EA1n"}, t\xF4i \u0111\xE3 xem qua k\u1EBFt qu\u1EA3 c\u1EE7a b\u1EA1n! \u0110\u1EC3 c\u1EE7ng c\u1ED1 ki\u1EBFn th\u1EE9c, ch\xFAng ta h\xE3y \xF4n l\u1EA1i ch\u1EE7 \u0111\u1EC1 [T\xEAn ch\u1EE7 \u0111\u1EC1 y\u1EBFu] \u1EDF m\u1EE9c \u0111\u1ED9 'Easy' nh\xE9. V\xEC b\u1EA1n \u0111\xE3 l\xE0m r\u1EA5t t\u1ED1t ph\u1EA7n [T\xEAn ch\u1EE7 \u0111\u1EC1 m\u1EA1nh], h\xE3y th\u1EED s\u1EE9c v\u1EDBi ch\u1EE7 \u0111\u1EC1 li\xEAn quan l\xE0 [T\xEAn ch\u1EE7 \u0111\u1EC1 kh\xE1m ph\xE1] \u1EDF m\u1EE9c \u0111\u1ED9 'Medium' xem sao!",
2995
+ "suggestedTopics": [
2996
+ { "loId": "some-lo-id-1", "topicName": "[T\xEAn ch\u1EE7 \u0111\u1EC1 y\u1EBFu]", "reason": "review", "suggestedDifficulty": "Easy" },
2997
+ { "loId": "some-lo-id-2", "topicName": "[T\xEAn ch\u1EE7 \u0111\u1EC1 kh\xE1m ph\xE1]", "reason": "explore", "suggestedDifficulty": "Medium" }
2998
+ ]
2999
+ }
3000
+
3001
+ Return only the JSON response.`;
3002
+ const response = await ai.generate(promptText);
3003
+ const rawText = response.text;
3004
+ const jsonText = extractJsonFromMarkdown(rawText);
3005
+ const aiGeneratedContent = JSON.parse(jsonText);
3006
+ const validatedOutput = PracticeSuggestionOutputSchema.parse(aiGeneratedContent);
3007
+ return validatedOutput;
3008
+ } catch (error) {
3009
+ console.error("Error generating practice suggestion:", error);
3010
+ if (error instanceof z.ZodError) {
3011
+ throw new Error(`AI returned data in an unexpected format. Validation failed: ${error.message}`);
3012
+ }
3013
+ throw new Error(`Failed to generate practice suggestion: ${error.message}`);
3014
+ }
3015
+ }
3016
+ z.object({
3017
+ language: z.string().default("English"),
3018
+ userName: z.string().optional(),
3019
+ weeklyGoal: z.number().optional(),
3020
+ unlockedAchievements: z.array(z.object({
3021
+ id: z.string(),
3022
+ name: z.string(),
3023
+ description: z.string(),
3024
+ icon: z.string()
3025
+ })),
3026
+ stats: z.any(),
3027
+ // Using z.any() for simplicity, as PracticeStats is complex and defined elsewhere.
3028
+ history: z.array(z.any()),
3029
+ // Using z.any() for simplicity.
3030
+ startDate: z.string().describe("The start date for the analysis period in ISO format (YYYY-MM-DD)."),
3031
+ endDate: z.string().describe("The end date for the analysis period in ISO format (YYYY-MM-DD)."),
3032
+ allAvailableTopics: z.array(z.object({
3033
+ loId: z.string(),
3034
+ subject: z.string(),
3035
+ category: z.string(),
3036
+ topic: z.string()
3037
+ })).describe("A complete list of all available topics for the AI to choose from.")
3038
+ });
3039
+ var RoadmapItemSchema = z.object({
3040
+ day: z.string(),
3041
+ topicName: z.string(),
3042
+ reason: z.string(),
3043
+ suggestedDifficulty: z.enum(["Very Easy", "Easy", "Medium", "Hard", "Expert"]),
3044
+ loId: z.string(),
3045
+ isCompleted: z.boolean()
3046
+ });
3047
+ var WeeklyRoadmapSchema = z.object({
3048
+ generatedAt: z.number(),
3049
+ items: z.array(RoadmapItemSchema)
3050
+ });
3051
+ var AnalysisReportSchema = z.object({
3052
+ generatedAt: z.number(),
3053
+ startDate: z.string(),
3054
+ endDate: z.string(),
3055
+ effortAndConsistencyRemarks: z.string(),
3056
+ strengths: z.array(z.string()),
3057
+ areasForImprovement: z.array(z.string()),
3058
+ gamificationRemarks: z.string()
3059
+ });
3060
+ var LearningAnalysisOutputSchema = z.object({
3061
+ analysisReport: AnalysisReportSchema.optional(),
3062
+ weeklyRoadmap: WeeklyRoadmapSchema.optional()
3063
+ });
3064
+
3065
+ // src/ai/flows/generate-learning-analysis.ts
3066
+ async function generateLearningAnalysis(clientInput, apiKey) {
3067
+ try {
3068
+ const ai = genkit({
3069
+ plugins: [googleAI({ apiKey })],
3070
+ model: gemini20Flash
3071
+ });
3072
+ const {
3073
+ language,
3074
+ userName = "learner",
3075
+ weeklyGoal = 5,
3076
+ unlockedAchievements,
3077
+ stats,
3078
+ history,
3079
+ startDate,
3080
+ endDate,
3081
+ allAvailableTopics
3082
+ } = clientInput;
3083
+ const filteredHistory = history.filter(
3084
+ (session) => session.timestamp >= new Date(startDate).getTime() && session.timestamp <= new Date(endDate).setHours(23, 59, 59, 999)
3085
+ );
3086
+ const promptText = `
3087
+ You are an expert AI Learning Coach. Your task is to analyze a student's learning data and generate a comprehensive JSON object containing an analysis report and a suggested weekly roadmap.
3088
+
3089
+ **Student Data:**
3090
+ - Name: ${userName}
3091
+ - Weekly Goal: ${weeklyGoal} sessions
3092
+ - Unlocked Achievements: ${JSON.stringify(unlockedAchievements.map((a) => a.name))}
3093
+ - Overall Performance Stats (for context): ${JSON.stringify(stats, null, 2)}
3094
+ - Detailed Practice History (for analysis): ${JSON.stringify(filteredHistory, null, 2)}
3095
+ - **Source of Truth - All Available Topics**: ${JSON.stringify(allAvailableTopics, null, 2)}
3096
+
3097
+ **CRITICAL INSTRUCTIONS:**
3098
+ You MUST generate a single JSON object with two main keys: "analysisReport" and "weeklyRoadmap".
3099
+ All topic names and loIds in your "weeklyRoadmap" output MUST be chosen directly from the "All Available Topics" list provided above. DO NOT invent topics or loIds.
3100
+
3101
+ --- LOGIC FLOW ---
3102
+
3103
+ **IF Practice History is NOT EMPTY:**
3104
+ 1. **For "analysisReport":**
3105
+ - Analyze the data strictly within the period from ${startDate} to ${endDate}.
3106
+ - **effortAndConsistencyRemarks**: Write a personalized comment on the student's effort.
3107
+ - **strengths**: Identify 2-3 topics where the student performed best.
3108
+ - **areasForImprovement**: Identify 2-3 topics where the student struggled the most.
3109
+ - **gamificationRemarks**: Write an encouraging message mentioning their achievements.
3110
+ 2. **For "weeklyRoadmap":**
3111
+ - Create a 5-item roadmap for the upcoming week.
3112
+ - Prioritize the "areasForImprovement" you identified. For each, find the corresponding entry in "All Available Topics" and use its "topicName" and "loId".
3113
+ - If more items are needed, select related topics from "All Available Topics".
3114
+
3115
+ **IF Practice History IS EMPTY:**
3116
+ 1. **For "analysisReport":**
3117
+ - **effortAndConsistencyRemarks**: Write a welcoming message for a new learner, encouraging them to start their journey.
3118
+ - **strengths**: Return an empty array [].
3119
+ - **areasForImprovement**: Return an empty array [].
3120
+ - **gamificationRemarks**: Write a general motivational message about starting to learn.
3121
+ 2. **For "weeklyRoadmap":**
3122
+ - Create a 5-item "starter" roadmap.
3123
+ - Select 5 diverse and foundational topics directly from the "All Available Topics" list. Use their exact "topicName" and "loId".
3124
+ - For the "reason", explain that this is a good starting point to explore the subject.
3125
+
3126
+ --- END LOGIC FLOW ---
3127
+
3128
+ **CRITICAL JSON OUTPUT FORMAT:**
3129
+ Return ONLY the JSON object. All descriptive string content (like remarks, strengths, reasons) MUST be in ${language}.
3130
+ The 'suggestedDifficulty' field MUST ALWAYS be one of these exact English strings: 'Very Easy', 'Easy', 'Medium', 'Hard', 'Expert'.
3131
+
3132
+ \`\`\`json
3133
+ {
3134
+ "analysisReport": {
3135
+ "generatedAt": ${Date.now()},
3136
+ "startDate": "${startDate}",
3137
+ "endDate": "${endDate}",
3138
+ "effortAndConsistencyRemarks": "Your detailed remarks...",
3139
+ "strengths": ["Topic A", "Subject B"],
3140
+ "areasForImprovement": ["Topic C", "Topic D"],
3141
+ "gamificationRemarks": "Your encouraging remarks..."
3142
+ },
3143
+ "weeklyRoadmap": {
3144
+ "generatedAt": ${Date.now()},
3145
+ "items": [
3146
+ {
3147
+ "day": "Monday",
3148
+ "topicName": "Topic C",
3149
+ "reason": "To strengthen your understanding of this key area.",
3150
+ "suggestedDifficulty": "Easy",
3151
+ "loId": "lo-id-for-topic-c-from-the-list",
3152
+ "isCompleted": false
3153
+ }
3154
+ ]
3155
+ }
3156
+ }
3157
+ \`\`\`
3158
+ `;
3159
+ const response = await ai.generate(promptText);
3160
+ const rawText = response.text;
3161
+ const jsonText = extractJsonFromMarkdown(rawText);
3162
+ const aiGeneratedContent = JSON.parse(jsonText);
3163
+ const validatedOutput = LearningAnalysisOutputSchema.parse(aiGeneratedContent);
3164
+ return validatedOutput;
3165
+ } catch (error) {
3166
+ console.error("Error generating learning analysis:", error);
3167
+ if (error instanceof z.ZodError) {
3168
+ throw new Error(`AI returned data in an unexpected format. Validation failed: ${error.message}`);
3169
+ }
3170
+ const errorMessage = String(error.message || "");
3171
+ if (errorMessage.includes("503") || errorMessage.toLowerCase().includes("service unavailable") || errorMessage.toLowerCase().includes("overloaded")) {
3172
+ throw new Error("D\u1ECBch v\u1EE5 AI hi\u1EC7n \u0111ang qu\xE1 t\u1EA3i. Vui l\xF2ng th\u1EED l\u1EA1i sau \xEDt ph\xFAt.");
3173
+ }
3174
+ throw new Error(`Failed to generate learning analysis: ${errorMessage}`);
3175
+ }
3176
+ }
3177
+ z.object({
3178
+ language: z.string(),
3179
+ userName: z.string().optional(),
3180
+ currentStreak: z.number().optional(),
3181
+ weakestTopic: z.string().optional()
3182
+ });
3183
+ var GenerateMotivationalQuoteOutputSchema = z.object({
3184
+ text: z.string(),
3185
+ author: z.string()
3186
+ });
3187
+
3188
+ // src/ai/flows/generate-motivational-quote.ts
3189
+ async function generateMotivationalQuote(clientInput, apiKey) {
3190
+ try {
3191
+ const ai = genkit({
3192
+ plugins: [googleAI({ apiKey })],
3193
+ model: gemini20Flash
3194
+ });
3195
+ const { language, userName, currentStreak, weakestTopic } = clientInput;
3196
+ let contextPrompt = "";
3197
+ if (userName) {
3198
+ contextPrompt += `The student's name is ${userName}. Address them directly.`;
3199
+ }
3200
+ if (currentStreak && currentStreak > 1) {
3201
+ contextPrompt += ` They are on a ${currentStreak}-day practice streak. Acknowledge this achievement.`;
3202
+ }
3203
+ if (weakestTopic) {
3204
+ contextPrompt += ` They seem to be struggling with the topic "${weakestTopic}". Encourage them to persevere on this topic.`;
3205
+ }
3206
+ const promptText = `
3207
+ You are an AI Learning Coach. Your task is to generate a single, short, and powerful motivational quote for a student.
3208
+
3209
+ **Context about the student:**
3210
+ ${contextPrompt || "No specific context provided, generate a general motivational quote about learning."}
3211
+
3212
+ **Instructions:**
3213
+ 1. Create a quote that is concise (under 20 words).
3214
+ 2. The quote must be encouraging and relevant to learning or perseverance.
3215
+ 3. If context is provided, subtly weave it into the quote.
3216
+ 4. The quote must be in the following language: ${language}.
3217
+
3218
+ **JSON OUTPUT FORMAT:**
3219
+ Return the response as a single, valid JSON object with this EXACT format:
3220
+ {
3221
+ "text": "Your generated motivational quote here.",
3222
+ "author": "AI Tutor"
3223
+ }
3224
+
3225
+ Return only the JSON response.`;
3226
+ try {
3227
+ const response = await ai.generate(promptText);
3228
+ const rawText = response.text;
3229
+ const jsonText = extractJsonFromMarkdown(rawText);
3230
+ const aiGeneratedContent = JSON.parse(jsonText);
3231
+ const validatedOutput = GenerateMotivationalQuoteOutputSchema.parse(aiGeneratedContent);
3232
+ return validatedOutput;
3233
+ } catch (apiError) {
3234
+ console.warn(`Could not generate AI motivational quote due to an API error: ${apiError.message}. Falling back to static quotes.`);
3235
+ return null;
3236
+ }
3237
+ } catch (error) {
3238
+ console.error("An unexpected error occurred in the generateMotivationalQuote flow:", error);
3239
+ return null;
3240
+ }
3241
+ }
3242
+ z.object({
3243
+ language: z.string(),
3244
+ learningObjectivesContent: z.string().min(50, { message: "Learning objectives content must be substantial." }),
3245
+ overallSubject: z.string().min(1, { message: "Overall subject is required for context." })
3246
+ });
3247
+ var PlanKnowledgeCardsOutputSchema = z.object({
3248
+ concepts: z.array(z.string()).min(1, { message: "AI should identify at least one concept." })
3249
+ });
3250
+
3251
+ // src/ai/flows/plan-knowledge-cards.ts
3252
+ async function planKnowledgeCards(clientInput, apiKey) {
3253
+ try {
3254
+ const ai = genkit({
3255
+ plugins: [googleAI({ apiKey })],
3256
+ model: gemini20Flash
3257
+ });
3258
+ const { language, learningObjectivesContent, overallSubject } = clientInput;
3259
+ const promptText = `
3260
+ You are an expert curriculum designer specializing in the subject of "${overallSubject}".
3261
+ Your task is to analyze the provided learning objectives and identify the most critical concepts, keywords, and technical terms that a student must know.
3262
+
3263
+ **Provided Learning Objectives Document:**
3264
+ ---
3265
+ ${learningObjectivesContent}
3266
+ ---
3267
+
3268
+ **Instructions:**
3269
+ 1. Read the entire document carefully to understand the scope of the subject matter.
3270
+ 2. Identify between 15 and 20 of the most fundamental and important concepts.
3271
+ 3. Focus on nouns, technical terms, and core principles (e.g., "Variable", "Optional Chaining", "Protocol", "Delegate Pattern").
3272
+ 4. Do not select broad, generic phrases. The concepts should be specific enough to be explained on a single flashcard.
3273
+ 5. List these concepts in ${language}.
3274
+
3275
+ **JSON OUTPUT FORMAT:**
3276
+ Return a single, valid JSON object with this EXACT format:
3277
+ {
3278
+ "concepts": [
3279
+ "First Concept",
3280
+ "Second Concept",
3281
+ "Third Concept",
3282
+ ...
3283
+ ]
3284
+ }
3285
+
3286
+ Return only the JSON response.`;
3287
+ const response = await ai.generate(promptText);
3288
+ const rawText = response.text;
3289
+ const jsonText = extractJsonFromMarkdown(rawText);
3290
+ const aiGeneratedContent = JSON.parse(jsonText);
3291
+ const validatedOutput = PlanKnowledgeCardsOutputSchema.parse(aiGeneratedContent);
3292
+ return validatedOutput;
3293
+ } catch (error) {
3294
+ console.error("Error planning knowledge cards:", error);
3295
+ if (error instanceof z.ZodError) {
3296
+ throw new Error(`AI returned data in an unexpected format for planning. Validation failed: ${error.message}`);
3297
+ }
3298
+ throw new Error(`Failed to plan knowledge cards: ${error.message}`);
3299
+ }
3300
+ }
3301
+ z.object({
3302
+ language: z.string(),
3303
+ concept: z.string().min(1, { message: "Concept cannot be empty." }),
3304
+ overallSubject: z.string().min(1, { message: "Overall subject is required for context." }),
3305
+ fullContext: z.string().optional().describe("The full learning objectives document for context reference.")
3306
+ });
3307
+ var GenerateSingleKnowledgeCardOutputSchema = z.object({
3308
+ concept: z.string(),
3309
+ definition: z.string(),
3310
+ example: z.string()
3311
+ });
3312
+
3313
+ // src/ai/flows/generate-single-knowledge-card.ts
3314
+ async function generateSingleKnowledgeCard(clientInput, apiKey) {
3315
+ const ai = genkit({
3316
+ plugins: [googleAI({ apiKey })],
3317
+ model: gemini20Flash
3318
+ });
3319
+ try {
3320
+ const { language, concept, overallSubject, fullContext } = clientInput;
3321
+ const promptText = `
3322
+ You are an expert educator for "${overallSubject}". Create content for a single knowledge card.
3323
+ Target Concept: ${concept}
3324
+ Instructions:
3325
+ 1. Write a concise "definition" (under 40 words).
3326
+ 2. Provide a practical "example" (code or real-world scenario) relevant to "${overallSubject}".
3327
+ 3. All content must be in ${language}.
3328
+ 4. The 'concept' field in your output must exactly match the Target Concept.`;
3329
+ const response = await ai.generate({
3330
+ prompt: promptText,
3331
+ output: {
3332
+ schema: GenerateSingleKnowledgeCardOutputSchema
3333
+ }
3334
+ });
3335
+ const validatedOutput = response.output;
3336
+ if (!validatedOutput) {
3337
+ throw new Error("AI did not return a valid structured output.");
3338
+ }
3339
+ return validatedOutput;
3340
+ } catch (error) {
3341
+ console.error(`Error generating knowledge card for concept "${clientInput.concept}":`, error);
3342
+ if (error instanceof z.ZodError) {
3343
+ throw new Error(`AI output validation failed for card "${clientInput.concept}". Error: ${error.message}`);
3344
+ }
3345
+ throw new Error(`Failed to generate knowledge card for "${clientInput.concept}": ${error.message}`);
3346
+ }
3347
+ }
3348
+ var LearningObjectiveContextSchema = z.object({
3349
+ loId: z.string(),
3350
+ subject: z.string(),
3351
+ category: z.string(),
3352
+ topic: z.string(),
3353
+ loDescription: z.string()
3354
+ });
3355
+ z.object({
3356
+ language: z.string().default("English"),
3357
+ documentContent: z.string().min(100, { message: "Document content must be substantial enough for analysis." }),
3358
+ learningObjectives: z.array(LearningObjectiveContextSchema).min(1, { message: "At least one learning objective is required for mapping." })
3359
+ });
3360
+ var MappedLOSchema = z.object({
3361
+ loId: z.string().describe("The exact loId from the provided learning objectives list that matches the document content."),
3362
+ confidence: z.number().min(0).max(100).describe("A confidence score (0-100) of how well the document maps to this LO."),
3363
+ reasoning: z.string().describe("A brief explanation for why this mapping is relevant.")
3364
+ });
3365
+ var AssessAndMapDocumentOutputSchema = z.object({
3366
+ relevanceScore: z.number().min(0).max(100).describe("An overall score (0-100) indicating how relevant the document is to the provided learning objectives as a whole."),
3367
+ isFreestyleRecommended: z.boolean().describe("A simple recommendation. True if the document is not relevant enough to be part of the structured curriculum."),
3368
+ mappedLOs: z.array(MappedLOSchema).describe("A list of specific learning objectives the document is mapped to. Empty if not relevant.")
3369
+ });
3370
+
3371
+ // src/ai/flows/assess-and-map-document.ts
3372
+ async function assessAndMapDocument(clientInput, apiKey) {
3373
+ try {
3374
+ const ai = genkit({
3375
+ plugins: [googleAI({ apiKey })],
3376
+ model: gemini20Flash
3377
+ });
3378
+ const { language, documentContent, learningObjectives } = clientInput;
3379
+ const relevanceThreshold = 40;
3380
+ const promptText = `
3381
+ You are an expert curriculum analyst. Your task is to analyze a given document and determine how relevant it is to a provided list of learning objectives (LOs).
3382
+
3383
+ **INPUT DATA:**
3384
+
3385
+ 1. **DOCUMENT CONTENT (first 1000 characters):**
3386
+ ---
3387
+ ${documentContent.substring(0, 1e3)}...
3388
+ ---
3389
+
3390
+ 2. **LIST OF AVAILABLE LEARNING OBJECTIVES:**
3391
+ ---
3392
+ ${JSON.stringify(learningObjectives, null, 2)}
3393
+ ---
3394
+
3395
+ **YOUR ANALYSIS TASKS:**
3396
+
3397
+ 1. **Overall Relevance Assessment:** Read the document content and compare it against the entire list of LOs. Assign an overall "relevanceScore" from 0 (completely unrelated) to 100 (perfectly aligned with one or more LOs).
3398
+
3399
+ 2. **Specific Mapping:** Identify which specific LOs from the list are directly addressed by the document. For each match you find, provide:
3400
+ - The exact "loId" of the matched LO.
3401
+ - A "confidence" score (0-100) for that specific match.
3402
+ - A brief "reasoning" in ${language} explaining why the document content maps to that LO.
3403
+
3404
+ 3. **Recommendation:** Based on your overall relevanceScore, provide a boolean recommendation "isFreestyleRecommended". This should be \`true\` if the relevanceScore is below ${relevanceThreshold}, and \`false\` otherwise.
3405
+
3406
+ **CRITICAL JSON OUTPUT FORMAT:**
3407
+ Return a single, valid JSON object in this EXACT format. Do not include any other text or markdown.
3408
+
3409
+ \`\`\`json
3410
+ {
3411
+ "relevanceScore": 85,
3412
+ "isFreestyleRecommended": false,
3413
+ "mappedLOs": [
3414
+ {
3415
+ "loId": "SWIFT_FUNC_01",
3416
+ "confidence": 95,
3417
+ "reasoning": "The document provides a detailed explanation of function syntax and default parameters, which directly aligns with this learning objective."
3418
+ }
3419
+ ]
3420
+ }
3421
+ \`\`\`
3422
+
3423
+ If the document is not relevant at all, the "mappedLOs" array should be empty, and the "relevanceScore" should be low.
3424
+ `;
3425
+ const response = await ai.generate(promptText);
3426
+ const rawText = response.text;
3427
+ const jsonText = extractJsonFromMarkdown(rawText);
3428
+ const aiGeneratedContent = JSON.parse(jsonText);
3429
+ const validatedOutput = AssessAndMapDocumentOutputSchema.parse(aiGeneratedContent);
3430
+ return validatedOutput;
3431
+ } catch (error) {
3432
+ console.error("Error in assessAndMapDocument flow:", error);
3433
+ if (error instanceof z.ZodError) {
3434
+ throw new Error(`AI output validation failed: ${error.message}`);
3435
+ }
3436
+ throw new Error(`Failed to assess document: ${error.message}`);
3437
+ }
3438
+ }
3439
+ z.object({
3440
+ language: z.string().optional().default("English"),
3441
+ documentContent: z.string().min(100, { message: "Document content must be substantial enough to generate a quiz." }),
3442
+ numQuestions: z.number().int().min(1).max(20).optional().default(10),
3443
+ questionTypes: z.array(z.enum(["multiple_choice", "true_false", "short_answer"])).optional()
3444
+ });
3445
+ var AnyGeneratedQuestionSchema = z.union([
3446
+ MultipleChoiceQuestionZodSchema,
3447
+ TrueFalseQuestionZodSchema,
3448
+ ShortAnswerQuestionZodSchema
3449
+ ]);
3450
+ var GenerateQuizFromTextOutputSchema = z.object({
3451
+ generatedQuestions: z.array(AnyGeneratedQuestionSchema)
3452
+ });
3453
+
3454
+ // src/ai/flows/generate-quiz-from-text.ts
3455
+ var AnyGeneratedQuestionSchema2 = GenerateQuizFromTextOutputSchema.shape.generatedQuestions.element;
3456
+ async function generateQuizFromText(clientInput, apiKey) {
3457
+ try {
3458
+ const ai = genkit({
3459
+ plugins: [googleAI({ apiKey })],
3460
+ model: gemini20Flash
3461
+ });
3462
+ const { language, documentContent, numQuestions, questionTypes } = clientInput;
3463
+ const allowedTypes = questionTypes || ["multiple_choice", "true_false"];
3464
+ const promptText = `
3465
+ You are an expert educator. Your task is to create a quiz with exactly ${numQuestions} questions based SOLELY on the provided document content.
3466
+
3467
+ **DOCUMENT CONTENT:**
3468
+ ---
3469
+ ${documentContent}
3470
+ ---
3471
+
3472
+ **QUIZ REQUIREMENTS:**
3473
+ 1. **Total Questions:** Generate exactly ${numQuestions} questions.
3474
+ 2. **Language:** All content (prompts, options, explanations) must be in ${language}.
3475
+ 3. **Content Source:** All questions MUST be derived directly from the provided DOCUMENT CONTENT. Do not use any external knowledge.
3476
+ 4. **Question Types:** Use a mix of the following question types: ${allowedTypes.join(", ")}. Prioritize 'multiple_choice'.
3477
+ 5. **Key Concepts:** Focus on the most important facts, definitions, and concepts within the text.
3478
+ 6. **Explanations:** Provide a brief, clear explanation for each question, referencing the source text.
3479
+
3480
+ **OUTPUT FORMAT:**
3481
+ Return the response as a single JSON object with a key "generatedQuestions" containing an array of exactly ${numQuestions} question objects. Do NOT include any other text or markdown formatting outside of the JSON object.
3482
+
3483
+ **Example of the JSON structure:**
3484
+ \`\`\`json
3485
+ {
3486
+ "generatedQuestions": [
3487
+ {
3488
+ "questionType": "multiple_choice",
3489
+ "prompt": "Based on the text, what is the primary function of a mitochondria?",
3490
+ "options": [
3491
+ { "tempId": "A", "text": "Cellular respiration" },
3492
+ { "tempId": "B", "text": "Photosynthesis" },
3493
+ { "tempId": "C", "text": "Protein synthesis" }
3494
+ ],
3495
+ "correctTempOptionId": "A",
3496
+ "explanation": "The document states that mitochondria are the powerhouses of the cell, responsible for cellular respiration.",
3497
+ "points": 10,
3498
+ "difficulty": "medium",
3499
+ "topic": "Cell Biology"
3500
+ },
3501
+ {
3502
+ "questionType": "true_false",
3503
+ "prompt": "The document indicates that the cell wall is found in both plant and animal cells.",
3504
+ "correctAnswer": false,
3505
+ "explanation": "The text specifies that the cell wall is a feature of plant cells, not animal cells.",
3506
+ "points": 10,
3507
+ "difficulty": "easy",
3508
+ "topic": "Cell Biology"
3509
+ }
3510
+ ]
3511
+ }
3512
+ \`\`\`
3513
+
3514
+ Now, generate the JSON response.`;
3515
+ const response = await ai.generate(promptText);
3516
+ const rawText = response.text;
3517
+ const jsonText = extractJsonFromMarkdown(rawText);
3518
+ const aiGeneratedContent = JSON.parse(jsonText);
3519
+ if (!aiGeneratedContent.generatedQuestions || !Array.isArray(aiGeneratedContent.generatedQuestions)) {
3520
+ throw new Error("AI did not return a valid 'generatedQuestions' array.");
3521
+ }
3522
+ const finalQuestions = [];
3523
+ for (const rawQuestion of aiGeneratedContent.generatedQuestions) {
3524
+ try {
3525
+ const questionId = generateUniqueId(`${rawQuestion.questionType}_`);
3526
+ let finalQuestion = null;
3527
+ switch (rawQuestion.questionType) {
3528
+ case "multiple_choice": {
3529
+ const tempOptions = rawQuestion.options || [];
3530
+ const finalOptions = tempOptions.map((opt) => ({
3531
+ id: generateUniqueId("opt_"),
3532
+ text: opt.text,
3533
+ tempId: opt.tempId
3534
+ }));
3535
+ const correctTempId = rawQuestion.correctTempOptionId;
3536
+ const correctFinalOption = finalOptions.find((opt) => opt.tempId === correctTempId);
3537
+ if (!correctFinalOption) {
3538
+ console.warn(`Skipping MCQ due to invalid correctTempOptionId: ${correctTempId}`);
3539
+ continue;
3540
+ }
3541
+ finalQuestion = __spreadProps(__spreadValues({}, rawQuestion), {
3542
+ id: questionId,
3543
+ options: finalOptions.map((_a) => {
3544
+ var _b = _a, { tempId } = _b, rest = __objRest(_b, ["tempId"]);
3545
+ return rest;
3546
+ }),
3547
+ correctAnswerId: correctFinalOption.id
3548
+ });
3549
+ break;
3550
+ }
3551
+ case "true_false":
3552
+ case "short_answer": {
3553
+ finalQuestion = __spreadProps(__spreadValues({}, rawQuestion), { id: questionId });
3554
+ break;
3555
+ }
3556
+ default:
3557
+ console.warn(`Unsupported question type generated by AI: ${rawQuestion.questionType}. Skipping.`);
3558
+ continue;
3559
+ }
3560
+ const validatedQuestion = AnyGeneratedQuestionSchema2.parse(finalQuestion);
3561
+ finalQuestions.push(validatedQuestion);
3562
+ } catch (error) {
3563
+ console.error("Error processing a single generated question:", error, "Question data:", rawQuestion);
3564
+ }
3565
+ }
3566
+ return { generatedQuestions: finalQuestions };
3567
+ } catch (error) {
3568
+ console.error("Error generating quiz from text:", error);
3569
+ if (error instanceof z.ZodError) {
3570
+ throw new Error(`AI output validation failed: ${error.message}`);
3571
+ }
3572
+ throw new Error(`Failed to generate quiz from text: ${error.message}`);
3573
+ }
3574
+ }
1437
3575
 
1438
- export { generateFillInTheBlanksQuestion, generateMCQQuestion, generateMRQQuestion, generateMatchingQuestion, generateNumericQuestion, generateQuestionsFromQuizPlan, generateQuizPlan, generateSequenceQuestion, generateShortAnswerQuestion, generateTrueFalseQuestion };
3576
+ export { assessAndMapDocument, generateFillInTheBlanksQuestion, generateLearningAnalysis, generateMCQQuestion, generateMRQQuestion, generateMatchingQuestion, generateMotivationalQuote, generateNumericQuestion, generatePracticeSuggestion, generateQuestionsFromQuizPlan, generateQuizFromText, generateQuizPlan, generateQuizReview, generateSequenceQuestion, generateShortAnswerQuestion, generateSingleKnowledgeCard, generateTrueFalseQuestion, planKnowledgeCards };