@thanh01.pmt/interactive-quiz-kit 1.0.27 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.cjs CHANGED
@@ -5,7 +5,7 @@ var zod = require('zod');
5
5
  var genkit = require('genkit');
6
6
  var googleai = require('@genkit-ai/googleai');
7
7
 
8
- // src/ai/flows/question-gen/generate-fitb-question.ts
8
+ // src/ai/flows/question-gen/generate-true-false-question.ts
9
9
 
10
10
  // src/utils/idGenerators.ts
11
11
  function generateUniqueId(prefix = "id_") {
@@ -198,20 +198,12 @@ var CodingQuestionZodSchema = BaseQuestionZodSchema.extend({
198
198
  })).min(1)
199
199
  });
200
200
 
201
- // src/ai/flows/question-gen/generate-fitb-question-types.ts
202
- BaseQuestionGenerationClientInputSchema.extend({
203
- numberOfBlanks: zod.z.number().int().min(1).max(5).optional().default(1),
204
- isCaseSensitive: zod.z.boolean().optional().default(false)
205
- });
206
- var AIFillInTheBlanksOutputFieldsSchema = zod.z.object({
207
- prompt: zod.z.string().describe("The instructional text for the user, e.g., 'Fill in the blanks to complete the sentence.'"),
208
- // Yêu cầu AI trả về cấu trúc segments trực tiếp
209
- segments: zod.z.array(zod.z.object({
210
- type: zod.z.enum(["text", "blank"]),
211
- content: zod.z.string().optional().describe("The text content for a 'text' segment."),
212
- acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1).optional().describe("An array of correct answers for a 'blank' segment.")
213
- })).min(1).describe("An array of text and blank segments representing the question."),
214
- explanation: zod.z.string().optional(),
201
+ // src/ai/flows/question-gen/generate-true-false-question-types.ts
202
+ BaseQuestionGenerationClientInputSchema.extend({});
203
+ var AITrueFalseOutputFieldsSchema = zod.z.object({
204
+ prompt: zod.z.string().describe("The statement that the user will evaluate as true or false."),
205
+ correctAnswer: zod.z.boolean(),
206
+ explanation: zod.z.string().optional().describe("An explanation of why the statement is true or false, especially important if false."),
215
207
  points: zod.z.number().optional().default(10),
216
208
  difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
217
209
  topic: zod.z.string().optional(),
@@ -219,57 +211,55 @@ var AIFillInTheBlanksOutputFieldsSchema = zod.z.object({
219
211
  // Thêm để xác thực
220
212
  });
221
213
 
222
- // src/ai/flows/question-gen/generate-fitb-question.ts
214
+ // src/ai/flows/question-gen/generate-true-false-question.ts
223
215
  var MAX_RETRY_ATTEMPTS = 3;
224
216
  var RETRY_DELAY_MS = 3e3;
225
217
  function buildEnhancedPrompt(clientInput, attemptNumber) {
226
- const { quizContext, language, difficulty, numberOfBlanks, imageUrl } = clientInput;
218
+ const { quizContext, language, difficulty, imageUrl } = clientInput;
227
219
  const category = quizContext?.originalCategory || "the specified technical category";
228
220
  const attemptInfo = attemptNumber > 1 ? `
229
221
  ## DEBUG INFO - This is attempt #${attemptNumber}
230
- 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'.
222
+ Previous attempts failed. Ensure the JSON is valid and 'correctAnswer' is a boolean.
231
223
 
232
224
  ` : "";
233
- 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.` : "";
225
+ 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.` : "";
226
+ const misconceptionGuidance = quizContext?.targetMisconception ? `**Target Misconception:** The statement you create MUST be FALSE and based on this common mistake: "${quizContext.targetMisconception}"` : "";
234
227
  const contextStrings = [
235
228
  `**Required Category:** ${category}`,
236
229
  quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
237
230
  imageContextInstruction,
238
231
  quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
239
- quizContext?.targetMisconception && `**Target Misconception:** Design the blank to test this specific point: "${quizContext.targetMisconception}"`
232
+ misconceptionGuidance,
233
+ quizContext?.difficultyReason && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
240
234
  ].filter(Boolean).map((s) => `- ${s}`).join("\n");
241
235
  const exampleJson = JSON.stringify({
242
- prompt: "Complete the following Swift code snippet.",
243
- segments: [
244
- { "type": "text", "content": "To declare a new function in Swift, you use the `" },
245
- { "type": "blank", "acceptedAnswers": ["func"] },
246
- { "type": "text", "content": "` keyword." }
247
- ],
248
- explanation: "The 'func' keyword is used to declare a function in the Swift programming language.",
236
+ prompt: "In Swift, you must explicitly unwrap an Optional value before you can use its stored value.",
237
+ correctAnswer: true,
238
+ 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 '!'.",
249
239
  points: 10,
250
240
  difficulty: "easy",
251
- topic: "Swift Function Declaration",
241
+ topic: "Swift Optionals",
252
242
  verifiedCategory: category
253
243
  }, null, 2);
254
244
  return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
255
- Your mission is to create a high-quality, technically accurate Fill-in-the-Blanks Question.
245
+ Your mission is to create a high-quality, technically accurate True/False Question.
256
246
 
257
247
  ## Core Rules (Non-negotiable)
258
- 1. **Category Purity:** The question MUST be exclusively about **${category}**.
259
- 2. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
260
- 3. **Logical Segments:** For 'blank' segments, you MUST provide 'acceptedAnswers'. For 'text' segments, you MUST provide 'content'. Do not mix them.
248
+ 1. **Category Purity:** The statement ('prompt') MUST be exclusively about **${category}**.
249
+ 2. **Clarity:** The statement must be definitively true or false, with no ambiguity.
250
+ 3. **Misconception Priority:** If a Target Misconception is provided, the statement MUST be FALSE and reflect that misconception. This is a critical rule.
251
+ 4. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
261
252
 
262
253
  ## CRITICAL CONTEXT FOR THIS QUESTION
263
254
  ${contextStrings}
264
255
 
265
256
  ## Task: Generate the Question
266
- Based on all the rules and context above, generate a single Fill-in-the-Blanks Question.
257
+ Based on all the rules and context above, generate a single True/False Question.
267
258
 
268
259
  ### Input Parameters
269
260
  - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
270
261
  - **Language for Text:** ${language}
271
262
  - **Difficulty Level:** ${difficulty}
272
- - **Number of Blanks:** Generate exactly ${numberOfBlanks} segment(s) with type 'blank'.
273
263
 
274
264
  ### Required JSON Output Format
275
265
  Your response must be ONLY the JSON object, matching this exact structure:
@@ -278,11 +268,11 @@ ${exampleJson}
278
268
 
279
269
  Now, generate the JSON for the requested question.`;
280
270
  }
281
- async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
271
+ async function generateTrueFalseQuestion(clientInput, apiKey) {
282
272
  const ai = new genai.GoogleGenAI({ apiKey });
283
273
  const model = "gemini-2.5-flash";
284
274
  const config = {
285
- temperature: 0.8,
275
+ temperature: 0.6,
286
276
  responseMimeType: "application/json",
287
277
  thinkingConfig: {
288
278
  thinkingBudget: 4e3
@@ -311,20 +301,11 @@ async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
311
301
  if (!rawText) throw new Error("AI returned an empty response.");
312
302
  const parsedJson = JSON.parse(rawText);
313
303
  DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
314
- const aiGeneratedContent = AIFillInTheBlanksOutputFieldsSchema.parse(parsedJson);
304
+ const aiGeneratedContent = AITrueFalseOutputFieldsSchema.parse(parsedJson);
315
305
  DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
316
- const blankCount = aiGeneratedContent.segments.filter((s) => s.type === "blank").length;
317
- if (blankCount !== clientInput.numberOfBlanks) {
318
- throw new Error(`AI generated ${blankCount} blanks, but ${clientInput.numberOfBlanks} were required.`);
306
+ if (clientInput.quizContext?.targetMisconception && aiGeneratedContent.correctAnswer === true) {
307
+ throw new Error("AI failed to follow the Misconception Priority rule. The answer should have been false.");
319
308
  }
320
- aiGeneratedContent.segments.forEach((segment, index) => {
321
- if (segment.type === "blank" && (!segment.acceptedAnswers || segment.acceptedAnswers.length === 0)) {
322
- throw new Error(`Segment ${index} is a 'blank' but is missing 'acceptedAnswers'.`);
323
- }
324
- if (segment.type === "text" && typeof segment.content !== "string") {
325
- throw new Error(`Segment ${index} is 'text' but is missing 'content'.`);
326
- }
327
- });
328
309
  if (clientInput.quizContext?.originalCategory) {
329
310
  const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
330
311
  const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
@@ -332,24 +313,11 @@ async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
332
313
  throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
333
314
  }
334
315
  }
335
- const finalSegments = [];
336
- const finalAnswers = [];
337
- aiGeneratedContent.segments.forEach((segment) => {
338
- if (segment.type === "text") {
339
- finalSegments.push({ type: "text", content: segment.content });
340
- } else if (segment.type === "blank" && segment.acceptedAnswers) {
341
- const blankId = generateUniqueId("blank_");
342
- finalSegments.push({ type: "blank", id: blankId });
343
- finalAnswers.push({ blankId, acceptedValues: segment.acceptedAnswers });
344
- }
345
- });
346
316
  const completeQuestion = {
347
- id: generateUniqueId("fitb_ai_"),
348
- questionType: "fill_in_the_blanks",
317
+ id: generateUniqueId("tf_ai_"),
318
+ questionType: "true_false",
349
319
  prompt: aiGeneratedContent.prompt,
350
- segments: finalSegments,
351
- answers: finalAnswers,
352
- isCaseSensitive: clientInput.isCaseSensitive,
320
+ correctAnswer: aiGeneratedContent.correctAnswer,
353
321
  explanation: aiGeneratedContent.explanation,
354
322
  points: aiGeneratedContent.points,
355
323
  topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
@@ -361,10 +329,10 @@ async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
361
329
  category: clientInput.quizContext?.originalCategory,
362
330
  imageUrl: clientInput.imageUrl
363
331
  };
364
- const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
332
+ const validatedQuestion = TrueFalseQuestionZodSchema.parse(completeQuestion);
365
333
  attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
366
334
  console.log(`
367
- \u2705 FITB generation successful on attempt ${attempt} (${duration}ms)`);
335
+ \u2705 True/False generation successful on attempt ${attempt} (${duration}ms)`);
368
336
  if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
369
337
  return { question: validatedQuestion };
370
338
  } catch (error) {
@@ -380,89 +348,93 @@ async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
380
348
  }
381
349
  }
382
350
  DebugLogger.logAttemptSummary(attemptResults);
383
- const errorMessage = `Failed to generate FITB question after ${MAX_RETRY_ATTEMPTS} attempts. Last error: ${lastError?.message}`;
351
+ const errorMessage = `Failed to generate True/False question after ${MAX_RETRY_ATTEMPTS} attempts. Last error: ${lastError?.message}`;
384
352
  console.error("\n\u274C Final Result: FAILED");
385
353
  console.error(errorMessage);
386
354
  return { error: errorMessage };
387
355
  }
388
356
  BaseQuestionGenerationClientInputSchema.extend({
389
- numberOfPairs: zod.z.number().int().min(2).max(8).optional().default(4),
390
- shuffleOptions: zod.z.boolean().optional().default(true)
357
+ numberOfOptions: zod.z.number().int().min(2).max(6).optional().default(4)
391
358
  });
392
- var AIMatchingOutputFieldsSchema = zod.z.object({
393
- prompt: zod.z.string().describe("The instructional text for the user, e.g., 'Match the concept to its definition.'"),
394
- correctPairs: zod.z.array(zod.z.object({
395
- promptText: zod.z.string().min(1).describe("The text for the left-hand side item (the prompt)."),
396
- optionText: zod.z.string().min(1).describe("The text for the right-hand side item (the matching option).")
397
- })).min(2),
398
- explanation: zod.z.string().optional(),
359
+ var AIMCQOutputFieldsSchema = zod.z.object({
360
+ prompt: zod.z.string().describe("The question statement itself."),
361
+ options: zod.z.array(
362
+ zod.z.object({
363
+ tempId: zod.z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B', '1', '2')."),
364
+ text: zod.z.string().describe("The text content of this answer option.")
365
+ })
366
+ ).min(2).max(6),
367
+ correctTempOptionId: zod.z.string().describe("The temporary ID of the correct option from the generated options array."),
368
+ explanation: zod.z.string().optional().describe("A brief explanation of why the answer is correct."),
399
369
  points: zod.z.number().optional().default(10),
400
370
  difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
401
371
  topic: zod.z.string().optional(),
402
- verifiedCategory: zod.z.string().optional()
403
- // Thêm để xác thực
372
+ verifiedCategory: zod.z.string().optional().describe("The category this question actually addresses.")
404
373
  });
405
374
 
406
- // src/ai/flows/question-gen/generate-matching-question.ts
375
+ // src/ai/flows/question-gen/generate-mcq-question.ts
407
376
  var MAX_RETRY_ATTEMPTS2 = 3;
408
377
  var RETRY_DELAY_MS2 = 3e3;
409
378
  function buildEnhancedPrompt2(clientInput, attemptNumber) {
410
- const { quizContext, language, difficulty, numberOfPairs, imageUrl } = clientInput;
379
+ const { quizContext, language, difficulty, numberOfOptions, imageUrl } = clientInput;
411
380
  const category = quizContext?.originalCategory || "the specified technical category";
412
381
  const attemptInfo = attemptNumber > 1 ? `
413
382
  ## DEBUG INFO - This is attempt #${attemptNumber}
414
- Previous attempts failed. Please ensure the 'correctPairs' array has exactly the required number of items and the JSON is valid.
383
+ Previous attempts failed...
415
384
 
416
385
  ` : "";
417
- const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The matching pairs must be directly related to the content of this image.` : "";
386
+ 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.` : "";
418
387
  const contextStrings = [
419
388
  `**Required Category:** ${category}`,
420
389
  quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
421
390
  imageContextInstruction,
422
391
  quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
423
- quizContext?.targetMisconception && `**Target Misconception:** Design a pair that specifically tests this confusion: "${quizContext.targetMisconception}"`
392
+ quizContext?.targetMisconception && `**Target Misconception:** Use this to create plausible incorrect answers: "${quizContext.targetMisconception}"`,
393
+ quizContext?.difficultyReason && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
424
394
  ].filter(Boolean).map((s) => `- ${s}`).join("\n");
425
395
  const exampleJson = JSON.stringify({
426
- prompt: "Match each Swift collection type to its primary characteristic.",
427
- correctPairs: [
428
- { "promptText": "Array", "optionText": "An ordered, random-access collection." },
429
- { "promptText": "Set", "optionText": "An unordered collection of unique elements." },
430
- { "promptText": "Dictionary", "optionText": "An unordered collection of key-value associations." }
396
+ prompt: `In ${category}, what is the primary purpose of the 'guard' statement?`,
397
+ options: [
398
+ { tempId: "A", text: "To execute a block of code repeatedly." },
399
+ { tempId: "B", text: "To define a new custom data type." },
400
+ { tempId: "C", text: "To exit a scope early if a condition is not met." },
401
+ { tempId: "D", text: "To handle errors thrown by a function." }
431
402
  ],
432
- explanation: "These are the fundamental characteristics of Swift's main collection types.",
403
+ correctTempOptionId: "C",
404
+ 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.`,
433
405
  points: 10,
434
406
  difficulty: "easy",
435
- topic: "Swift Collection Types",
407
+ topic: `Control Flow in ${category}`,
436
408
  verifiedCategory: category
437
409
  }, null, 2);
438
- return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
439
- Your mission is to create a high-quality, technically accurate Matching Question.
410
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in the programming language: ${category}.
411
+ Your sole mission is to create a high-quality, technically accurate Multiple Choice Question. You must adhere to the following rules at all times.
440
412
 
441
413
  ## Core Rules (Non-negotiable)
442
- 1. **Category Purity:** The question MUST be exclusively about **${category}**.
443
- 2. **Logical Pairs:** The items to be matched must have a clear, one-to-one relationship.
444
- 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
414
+ 1. **Category Purity:** The question, options, and explanation MUST be exclusively about **${category}**. Do NOT mention or use syntax from other languages.
415
+ 2. **Context Adherence:** The question's content must directly align with all provided context.
416
+ 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.
445
417
 
446
418
  ## CRITICAL CONTEXT FOR THIS QUESTION
447
419
  ${contextStrings}
448
420
 
449
421
  ## Task: Generate the Question
450
- Based on all the rules and context above, generate a single Matching Question.
422
+ Based on all the rules and context above, generate a single Multiple Choice Question.
451
423
 
452
424
  ### Input Parameters
453
425
  - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
454
426
  - **Language for Text:** ${language}
455
427
  - **Difficulty Level:** ${difficulty}
456
- - **Number of Pairs:** Generate exactly ${numberOfPairs} correct pairs in the 'correctPairs' array.
428
+ - **Number of Options:** ${numberOfOptions}
457
429
 
458
430
  ### Required JSON Output Format
459
- Your response must be ONLY the JSON object, matching this exact structure:
431
+ Your response must be ONLY the JSON object, matching this exact structure and field names.
460
432
 
461
433
  ${exampleJson}
462
434
 
463
435
  Now, generate the JSON for the requested question.`;
464
436
  }
465
- async function generateMatchingQuestion(clientInput, apiKey) {
437
+ async function generateMCQQuestion(clientInput, apiKey) {
466
438
  const ai = new genai.GoogleGenAI({ apiKey });
467
439
  const model = "gemini-2.5-flash";
468
440
  const config = {
@@ -487,19 +459,22 @@ async function generateMatchingQuestion(clientInput, apiKey) {
487
459
  parts.unshift(imagePart);
488
460
  }
489
461
  const contents = [{ role: "user", parts }];
490
- const aiResult = await ai.models.generateContent({ model, config, contents });
462
+ const aiResult = await ai.models.generateContent({
463
+ model,
464
+ config,
465
+ contents
466
+ });
491
467
  const response = aiResult;
492
468
  const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
493
469
  const duration = Date.now() - startTime;
494
470
  DebugLogger.logResponse(attempt, rawText);
495
- if (!rawText) throw new Error("AI returned an empty response.");
471
+ if (!rawText) {
472
+ throw new Error("AI returned an empty response.");
473
+ }
496
474
  const parsedJson = JSON.parse(rawText);
497
475
  DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
498
- const aiGeneratedContent = AIMatchingOutputFieldsSchema.parse(parsedJson);
476
+ const aiGeneratedContent = AIMCQOutputFieldsSchema.parse(parsedJson);
499
477
  DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
500
- if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
501
- throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were required.`);
502
- }
503
478
  if (clientInput.quizContext?.originalCategory) {
504
479
  const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
505
480
  const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
@@ -507,24 +482,23 @@ async function generateMatchingQuestion(clientInput, apiKey) {
507
482
  throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
508
483
  }
509
484
  }
510
- const finalPrompts = [];
511
485
  const finalOptions = [];
512
- const finalCorrectAnswerMap = [];
513
- aiGeneratedContent.correctPairs.forEach((pair) => {
514
- const promptId = generateUniqueId("m_p_");
515
- const optionId = generateUniqueId("m_o_");
516
- finalPrompts.push({ id: promptId, content: pair.promptText });
517
- finalOptions.push({ id: optionId, content: pair.optionText });
518
- finalCorrectAnswerMap.push({ promptId, optionId });
486
+ const tempIdToFinalIdMap = {};
487
+ aiGeneratedContent.options.forEach((aiOption) => {
488
+ const finalId = generateUniqueId("opt_");
489
+ finalOptions.push({ id: finalId, text: aiOption.text });
490
+ tempIdToFinalIdMap[aiOption.tempId] = finalId;
519
491
  });
492
+ const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
493
+ if (!finalCorrectAnswerId) {
494
+ throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid`);
495
+ }
520
496
  const completeQuestion = {
521
- id: generateUniqueId("match_ai_"),
522
- questionType: "matching",
497
+ id: generateUniqueId("mcq_ai_"),
498
+ questionType: "multiple_choice",
523
499
  prompt: aiGeneratedContent.prompt,
524
- prompts: finalPrompts,
525
500
  options: finalOptions,
526
- correctAnswerMap: finalCorrectAnswerMap,
527
- shuffleOptions: clientInput.shuffleOptions,
501
+ correctAnswerId: finalCorrectAnswerId,
528
502
  explanation: aiGeneratedContent.explanation,
529
503
  points: aiGeneratedContent.points,
530
504
  topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
@@ -536,10 +510,10 @@ async function generateMatchingQuestion(clientInput, apiKey) {
536
510
  category: clientInput.quizContext?.originalCategory,
537
511
  imageUrl: clientInput.imageUrl
538
512
  };
539
- const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
513
+ const validatedQuestion = MultipleChoiceQuestionZodSchema.parse(completeQuestion);
540
514
  attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
541
515
  console.log(`
542
- \u2705 Matching generation successful on attempt ${attempt} (${duration}ms)`);
516
+ \u2705 MCQ generation successful on attempt ${attempt} (${duration}ms)`);
543
517
  if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
544
518
  return { question: validatedQuestion };
545
519
  } catch (error) {
@@ -555,84 +529,83 @@ async function generateMatchingQuestion(clientInput, apiKey) {
555
529
  }
556
530
  }
557
531
  DebugLogger.logAttemptSummary(attemptResults);
558
- const errorMessage = `Failed to generate Matching question after ${MAX_RETRY_ATTEMPTS2} attempts. Last error: ${lastError?.message}`;
532
+ const errorMessage = `Failed to generate MCQ question after ${MAX_RETRY_ATTEMPTS2} attempts. Last error: ${lastError?.message}`;
559
533
  console.error("\n\u274C Final Result: FAILED");
560
534
  console.error(errorMessage);
561
535
  return { error: errorMessage };
562
536
  }
563
537
  BaseQuestionGenerationClientInputSchema.extend({
564
- numberOfOptions: zod.z.number().int().min(2).max(6).optional().default(4)
538
+ numberOfOptions: zod.z.number().int().min(2).max(8).optional().default(5),
539
+ minCorrectAnswers: zod.z.number().int().min(1).optional().default(2),
540
+ maxCorrectAnswers: zod.z.number().int().min(1).optional().default(3)
565
541
  });
566
- var AIMCQOutputFieldsSchema = zod.z.object({
567
- prompt: zod.z.string().describe("The question statement itself."),
568
- options: zod.z.array(
569
- zod.z.object({
570
- tempId: zod.z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B', '1', '2')."),
571
- text: zod.z.string().describe("The text content of this answer option.")
572
- })
573
- ).min(2).max(6),
574
- correctTempOptionId: zod.z.string().describe("The temporary ID of the correct option from the generated options array."),
575
- explanation: zod.z.string().optional().describe("A brief explanation of why the answer is correct."),
542
+ var AIMRQOutputFieldsSchema = zod.z.object({
543
+ prompt: zod.z.string(),
544
+ options: zod.z.array(zod.z.object({ tempId: zod.z.string(), text: zod.z.string() })).min(2).max(8),
545
+ correctTempOptionIds: zod.z.array(zod.z.string()).min(1),
546
+ explanation: zod.z.string().optional(),
576
547
  points: zod.z.number().optional().default(10),
577
548
  difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
578
549
  topic: zod.z.string().optional(),
579
550
  verifiedCategory: zod.z.string().optional().describe("The category this question actually addresses.")
580
551
  });
581
552
 
582
- // src/ai/flows/question-gen/generate-mcq-question.ts
553
+ // src/ai/flows/question-gen/generate-mrq-question.ts
583
554
  var MAX_RETRY_ATTEMPTS3 = 3;
584
555
  var RETRY_DELAY_MS3 = 3e3;
585
556
  function buildEnhancedPrompt3(clientInput, attemptNumber) {
586
- const { quizContext, language, difficulty, numberOfOptions, imageUrl } = clientInput;
557
+ const { quizContext, language, difficulty, numberOfOptions, minCorrectAnswers, maxCorrectAnswers, imageUrl } = clientInput;
587
558
  const category = quizContext?.originalCategory || "the specified technical category";
588
559
  const attemptInfo = attemptNumber > 1 ? `
589
560
  ## DEBUG INFO - This is attempt #${attemptNumber}
590
- Previous attempts failed...
561
+ Previous attempts failed due to validation errors. Pay close attention to the number of correct answers and the JSON schema.
591
562
 
592
563
  ` : "";
593
564
  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.` : "";
594
565
  const contextStrings = [
595
- `**Required Category:** ${category}`,
566
+ `**Required Category:** ${category} (This is the ONLY language to be used)`,
596
567
  quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
597
568
  imageContextInstruction,
598
569
  quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
599
- quizContext?.targetMisconception && `**Target Misconception:** Use this to create plausible incorrect answers: "${quizContext.targetMisconception}"`,
570
+ quizContext?.targetMisconception && `**Target Misconception:** Use this to create plausible incorrect answers (distractors). The misconception is: "${quizContext.targetMisconception}"`,
600
571
  quizContext?.difficultyReason && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
601
572
  ].filter(Boolean).map((s) => `- ${s}`).join("\n");
602
573
  const exampleJson = JSON.stringify({
603
- prompt: `In ${category}, what is the primary purpose of the 'guard' statement?`,
574
+ prompt: "Which of the following are considered programming paradigms?",
604
575
  options: [
605
- { tempId: "A", text: "To execute a block of code repeatedly." },
606
- { tempId: "B", text: "To define a new custom data type." },
607
- { tempId: "C", text: "To exit a scope early if a condition is not met." },
608
- { tempId: "D", text: "To handle errors thrown by a function." }
576
+ { "tempId": "A", "text": "Object-Oriented" },
577
+ { "tempId": "B", "text": "Assembly" },
578
+ { "tempId": "C", "text": "Functional" },
579
+ { "tempId": "D", "text": "Procedural" },
580
+ { "tempId": "E", "text": "Middleware" }
609
581
  ],
610
- correctTempOptionId: "C",
611
- 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.`,
582
+ correctTempOptionIds: ["A", "C", "D"],
583
+ 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.",
612
584
  points: 10,
613
- difficulty: "easy",
614
- topic: `Control Flow in ${category}`,
585
+ difficulty: "medium",
586
+ topic: "Programming Paradigms",
615
587
  verifiedCategory: category
616
588
  }, null, 2);
617
589
  return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in the programming language: ${category}.
618
- Your sole mission is to create a high-quality, technically accurate Multiple Choice Question. You must adhere to the following rules at all times.
590
+ Your sole mission is to create a high-quality, technically accurate Multiple Response Question. You must adhere to the following rules at all times.
619
591
 
620
592
  ## Core Rules (Non-negotiable)
621
- 1. **Category Purity:** The question, options, and explanation MUST be exclusively about **${category}**. Do NOT mention or use syntax from other languages.
593
+ 1. **Category Purity:** The question, options, and explanation MUST be exclusively about **${category}**.
622
594
  2. **Context Adherence:** The question's content must directly align with all provided context.
623
- 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.
595
+ 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.
624
596
 
625
597
  ## CRITICAL CONTEXT FOR THIS QUESTION
626
598
  ${contextStrings}
627
599
 
628
600
  ## Task: Generate the Question
629
- Based on all the rules and context above, generate a single Multiple Choice Question.
601
+ Based on all the rules and context above, generate a single Multiple Response Question.
630
602
 
631
603
  ### Input Parameters
632
604
  - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
633
605
  - **Language for Text:** ${language}
634
606
  - **Difficulty Level:** ${difficulty}
635
- - **Number of Options:** ${numberOfOptions}
607
+ - **Number of Options:** Generate exactly ${numberOfOptions} options.
608
+ - **Number of Correct Answers:** The 'correctTempOptionIds' array MUST contain between ${minCorrectAnswers} and ${maxCorrectAnswers} valid IDs from the options you generate.
636
609
 
637
610
  ### Required JSON Output Format
638
611
  Your response must be ONLY the JSON object, matching this exact structure and field names.
@@ -641,7 +614,13 @@ ${exampleJson}
641
614
 
642
615
  Now, generate the JSON for the requested question.`;
643
616
  }
644
- async function generateMCQQuestion(clientInput, apiKey) {
617
+ async function generateMRQQuestion(clientInput, apiKey) {
618
+ if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
619
+ return { error: `Invalid input: minCorrectAnswers (${clientInput.minCorrectAnswers}) cannot be greater than maxCorrectAnswers (${clientInput.maxCorrectAnswers}).` };
620
+ }
621
+ if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
622
+ return { error: `Invalid input: maxCorrectAnswers (${clientInput.maxCorrectAnswers}) must be less than the total numberOfOptions (${clientInput.numberOfOptions}).` };
623
+ }
645
624
  const ai = new genai.GoogleGenAI({ apiKey });
646
625
  const model = "gemini-2.5-flash";
647
626
  const config = {
@@ -680,8 +659,15 @@ async function generateMCQQuestion(clientInput, apiKey) {
680
659
  }
681
660
  const parsedJson = JSON.parse(rawText);
682
661
  DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
683
- const aiGeneratedContent = AIMCQOutputFieldsSchema.parse(parsedJson);
662
+ const aiGeneratedContent = AIMRQOutputFieldsSchema.parse(parsedJson);
684
663
  DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
664
+ if (aiGeneratedContent.options.length !== clientInput.numberOfOptions) {
665
+ throw new Error(`AI generated ${aiGeneratedContent.options.length} options, but ${clientInput.numberOfOptions} were required.`);
666
+ }
667
+ const correctCount = aiGeneratedContent.correctTempOptionIds.length;
668
+ if (correctCount < clientInput.minCorrectAnswers || correctCount > clientInput.maxCorrectAnswers) {
669
+ throw new Error(`AI provided ${correctCount} correct answers, which is outside the required range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
670
+ }
685
671
  if (clientInput.quizContext?.originalCategory) {
686
672
  const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
687
673
  const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
@@ -691,25 +677,29 @@ async function generateMCQQuestion(clientInput, apiKey) {
691
677
  }
692
678
  const finalOptions = [];
693
679
  const tempIdToFinalIdMap = {};
680
+ const allTempIds = /* @__PURE__ */ new Set();
694
681
  aiGeneratedContent.options.forEach((aiOption) => {
695
- const finalId = generateUniqueId("opt_");
682
+ const finalId = generateUniqueId("opt_mr_");
696
683
  finalOptions.push({ id: finalId, text: aiOption.text });
697
684
  tempIdToFinalIdMap[aiOption.tempId] = finalId;
685
+ allTempIds.add(aiOption.tempId);
686
+ });
687
+ const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
688
+ if (!allTempIds.has(tempId)) {
689
+ throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') which does not exist in the generated options.`);
690
+ }
691
+ return tempIdToFinalIdMap[tempId];
698
692
  });
699
- const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
700
- if (!finalCorrectAnswerId) {
701
- throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid`);
702
- }
703
693
  const completeQuestion = {
704
- id: generateUniqueId("mcq_ai_"),
705
- questionType: "multiple_choice",
694
+ id: generateUniqueId("mrq_ai_"),
695
+ questionType: "multiple_response",
706
696
  prompt: aiGeneratedContent.prompt,
707
697
  options: finalOptions,
708
- correctAnswerId: finalCorrectAnswerId,
698
+ correctAnswerIds: finalCorrectAnswerIds,
709
699
  explanation: aiGeneratedContent.explanation,
710
700
  points: aiGeneratedContent.points,
711
701
  topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
712
- difficulty: clientInput.difficulty,
702
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
713
703
  contextCode: clientInput.quizContext?.plannedContextId,
714
704
  bloomLevel: clientInput.quizContext?.plannedBloomLevel,
715
705
  learningObjective: clientInput.quizContext?.originalLoId,
@@ -717,10 +707,10 @@ async function generateMCQQuestion(clientInput, apiKey) {
717
707
  category: clientInput.quizContext?.originalCategory,
718
708
  imageUrl: clientInput.imageUrl
719
709
  };
720
- const validatedQuestion = MultipleChoiceQuestionZodSchema.parse(completeQuestion);
710
+ const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
721
711
  attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
722
712
  console.log(`
723
- \u2705 MCQ generation successful on attempt ${attempt} (${duration}ms)`);
713
+ \u2705 MRQ generation successful on attempt ${attempt} (${duration}ms)`);
724
714
  if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
725
715
  return { question: validatedQuestion };
726
716
  } catch (error) {
@@ -736,102 +726,85 @@ async function generateMCQQuestion(clientInput, apiKey) {
736
726
  }
737
727
  }
738
728
  DebugLogger.logAttemptSummary(attemptResults);
739
- const errorMessage = `Failed to generate MCQ question after ${MAX_RETRY_ATTEMPTS3} attempts. Last error: ${lastError?.message}`;
729
+ const errorMessage = `Failed to generate MRQ question after ${MAX_RETRY_ATTEMPTS3} attempts. Last error: ${lastError?.message}`;
740
730
  console.error("\n\u274C Final Result: FAILED");
741
731
  console.error(errorMessage);
742
732
  return { error: errorMessage };
743
733
  }
744
734
  BaseQuestionGenerationClientInputSchema.extend({
745
- numberOfOptions: zod.z.number().int().min(2).max(8).optional().default(5),
746
- minCorrectAnswers: zod.z.number().int().min(1).optional().default(2),
747
- maxCorrectAnswers: zod.z.number().int().min(1).optional().default(3)
735
+ isCaseSensitive: zod.z.boolean().optional().default(false)
748
736
  });
749
- var AIMRQOutputFieldsSchema = zod.z.object({
750
- prompt: zod.z.string(),
751
- options: zod.z.array(zod.z.object({ tempId: zod.z.string(), text: zod.z.string() })).min(2).max(8),
752
- correctTempOptionIds: zod.z.array(zod.z.string()).min(1),
737
+ var AIShortAnswerOutputFieldsSchema = zod.z.object({
738
+ prompt: zod.z.string().describe("The question text that prompts the user for a short answer."),
739
+ acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1).describe("An array of one or more acceptable short answers. Include common variations if applicable."),
740
+ // isCaseSensitive không cần thiết ở đây, chúng ta sẽ quản lý nó ở phía client
753
741
  explanation: zod.z.string().optional(),
754
742
  points: zod.z.number().optional().default(10),
755
743
  difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
756
744
  topic: zod.z.string().optional(),
757
- verifiedCategory: zod.z.string().optional().describe("The category this question actually addresses.")
745
+ verifiedCategory: zod.z.string().optional()
746
+ // Thêm để xác thực
758
747
  });
759
748
 
760
- // src/ai/flows/question-gen/generate-mrq-question.ts
749
+ // src/ai/flows/question-gen/generate-short-answer-question.ts
761
750
  var MAX_RETRY_ATTEMPTS4 = 3;
762
751
  var RETRY_DELAY_MS4 = 3e3;
763
752
  function buildEnhancedPrompt4(clientInput, attemptNumber) {
764
- const { quizContext, language, difficulty, numberOfOptions, minCorrectAnswers, maxCorrectAnswers, imageUrl } = clientInput;
753
+ const { quizContext, language, difficulty, imageUrl } = clientInput;
765
754
  const category = quizContext?.originalCategory || "the specified technical category";
766
755
  const attemptInfo = attemptNumber > 1 ? `
767
756
  ## DEBUG INFO - This is attempt #${attemptNumber}
768
- Previous attempts failed due to validation errors. Pay close attention to the number of correct answers and the JSON schema.
757
+ Previous attempts failed. Ensure 'acceptedAnswers' is a non-empty array of strings and the JSON is valid.
769
758
 
770
759
  ` : "";
771
- 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.` : "";
760
+ 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.` : "";
772
761
  const contextStrings = [
773
- `**Required Category:** ${category} (This is the ONLY language to be used)`,
762
+ `**Required Category:** ${category}`,
774
763
  quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
775
764
  imageContextInstruction,
776
765
  quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
777
- quizContext?.targetMisconception && `**Target Misconception:** Use this to create plausible incorrect answers (distractors). The misconception is: "${quizContext.targetMisconception}"`,
778
- quizContext?.difficultyReason && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
766
+ quizContext?.targetMisconception && `**Target Misconception:** The question should require an answer that corrects this specific misconception: "${quizContext.targetMisconception}"`
779
767
  ].filter(Boolean).map((s) => `- ${s}`).join("\n");
780
768
  const exampleJson = JSON.stringify({
781
- prompt: "Which of the following are considered programming paradigms?",
782
- options: [
783
- { "tempId": "A", "text": "Object-Oriented" },
784
- { "tempId": "B", "text": "Assembly" },
785
- { "tempId": "C", "text": "Functional" },
786
- { "tempId": "D", "text": "Procedural" },
787
- { "tempId": "E", "text": "Middleware" }
788
- ],
789
- correctTempOptionIds: ["A", "C", "D"],
790
- 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.",
769
+ prompt: "In Swift, what keyword is used to declare a constant?",
770
+ acceptedAnswers: ["let"],
771
+ 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.",
791
772
  points: 10,
792
- difficulty: "medium",
793
- topic: "Programming Paradigms",
773
+ difficulty: "easy",
774
+ topic: "Swift Constants",
794
775
  verifiedCategory: category
795
776
  }, null, 2);
796
- return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in the programming language: ${category}.
797
- Your sole mission is to create a high-quality, technically accurate Multiple Response Question. You must adhere to the following rules at all times.
777
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
778
+ Your mission is to create a high-quality, technically accurate Short Answer Question.
798
779
 
799
780
  ## Core Rules (Non-negotiable)
800
- 1. **Category Purity:** The question, options, and explanation MUST be exclusively about **${category}**.
801
- 2. **Context Adherence:** The question's content must directly align with all provided context.
802
- 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.
781
+ 1. **Category Purity:** The question MUST be exclusively about **${category}**.
782
+ 2. **Objective Answer:** The question must have a short, factual, and objective answer. Avoid questions that are subjective or require long explanations.
783
+ 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
803
784
 
804
785
  ## CRITICAL CONTEXT FOR THIS QUESTION
805
786
  ${contextStrings}
806
787
 
807
788
  ## Task: Generate the Question
808
- Based on all the rules and context above, generate a single Multiple Response Question.
789
+ Based on all the rules and context above, generate a single Short Answer Question.
809
790
 
810
791
  ### Input Parameters
811
792
  - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
812
793
  - **Language for Text:** ${language}
813
794
  - **Difficulty Level:** ${difficulty}
814
- - **Number of Options:** Generate exactly ${numberOfOptions} options.
815
- - **Number of Correct Answers:** The 'correctTempOptionIds' array MUST contain between ${minCorrectAnswers} and ${maxCorrectAnswers} valid IDs from the options you generate.
816
795
 
817
796
  ### Required JSON Output Format
818
- Your response must be ONLY the JSON object, matching this exact structure and field names.
797
+ Your response must be ONLY the JSON object, matching this exact structure:
819
798
 
820
799
  ${exampleJson}
821
800
 
822
801
  Now, generate the JSON for the requested question.`;
823
802
  }
824
- async function generateMRQQuestion(clientInput, apiKey) {
825
- if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
826
- return { error: `Invalid input: minCorrectAnswers (${clientInput.minCorrectAnswers}) cannot be greater than maxCorrectAnswers (${clientInput.maxCorrectAnswers}).` };
827
- }
828
- if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
829
- return { error: `Invalid input: maxCorrectAnswers (${clientInput.maxCorrectAnswers}) must be less than the total numberOfOptions (${clientInput.numberOfOptions}).` };
830
- }
803
+ async function generateShortAnswerQuestion(clientInput, apiKey) {
831
804
  const ai = new genai.GoogleGenAI({ apiKey });
832
805
  const model = "gemini-2.5-flash";
833
806
  const config = {
834
- temperature: 0.8,
807
+ temperature: 0.5,
835
808
  responseMimeType: "application/json",
836
809
  thinkingConfig: {
837
810
  thinkingBudget: 4e3
@@ -852,29 +825,16 @@ async function generateMRQQuestion(clientInput, apiKey) {
852
825
  parts.unshift(imagePart);
853
826
  }
854
827
  const contents = [{ role: "user", parts }];
855
- const aiResult = await ai.models.generateContent({
856
- model,
857
- config,
858
- contents
859
- });
828
+ const aiResult = await ai.models.generateContent({ model, config, contents });
860
829
  const response = aiResult;
861
830
  const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
862
831
  const duration = Date.now() - startTime;
863
832
  DebugLogger.logResponse(attempt, rawText);
864
- if (!rawText) {
865
- throw new Error("AI returned an empty response.");
866
- }
833
+ if (!rawText) throw new Error("AI returned an empty response.");
867
834
  const parsedJson = JSON.parse(rawText);
868
835
  DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
869
- const aiGeneratedContent = AIMRQOutputFieldsSchema.parse(parsedJson);
836
+ const aiGeneratedContent = AIShortAnswerOutputFieldsSchema.parse(parsedJson);
870
837
  DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
871
- if (aiGeneratedContent.options.length !== clientInput.numberOfOptions) {
872
- throw new Error(`AI generated ${aiGeneratedContent.options.length} options, but ${clientInput.numberOfOptions} were required.`);
873
- }
874
- const correctCount = aiGeneratedContent.correctTempOptionIds.length;
875
- if (correctCount < clientInput.minCorrectAnswers || correctCount > clientInput.maxCorrectAnswers) {
876
- throw new Error(`AI provided ${correctCount} correct answers, which is outside the required range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
877
- }
878
838
  if (clientInput.quizContext?.originalCategory) {
879
839
  const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
880
840
  const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
@@ -882,31 +842,16 @@ async function generateMRQQuestion(clientInput, apiKey) {
882
842
  throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
883
843
  }
884
844
  }
885
- const finalOptions = [];
886
- const tempIdToFinalIdMap = {};
887
- const allTempIds = /* @__PURE__ */ new Set();
888
- aiGeneratedContent.options.forEach((aiOption) => {
889
- const finalId = generateUniqueId("opt_mr_");
890
- finalOptions.push({ id: finalId, text: aiOption.text });
891
- tempIdToFinalIdMap[aiOption.tempId] = finalId;
892
- allTempIds.add(aiOption.tempId);
893
- });
894
- const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
895
- if (!allTempIds.has(tempId)) {
896
- throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') which does not exist in the generated options.`);
897
- }
898
- return tempIdToFinalIdMap[tempId];
899
- });
900
845
  const completeQuestion = {
901
- id: generateUniqueId("mrq_ai_"),
902
- questionType: "multiple_response",
846
+ id: generateUniqueId("saq_ai_"),
847
+ questionType: "short_answer",
903
848
  prompt: aiGeneratedContent.prompt,
904
- options: finalOptions,
905
- correctAnswerIds: finalCorrectAnswerIds,
849
+ acceptedAnswers: aiGeneratedContent.acceptedAnswers,
850
+ isCaseSensitive: clientInput.isCaseSensitive,
906
851
  explanation: aiGeneratedContent.explanation,
907
852
  points: aiGeneratedContent.points,
908
853
  topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
909
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
854
+ difficulty: clientInput.difficulty,
910
855
  contextCode: clientInput.quizContext?.plannedContextId,
911
856
  bloomLevel: clientInput.quizContext?.plannedBloomLevel,
912
857
  learningObjective: clientInput.quizContext?.originalLoId,
@@ -914,10 +859,10 @@ async function generateMRQQuestion(clientInput, apiKey) {
914
859
  category: clientInput.quizContext?.originalCategory,
915
860
  imageUrl: clientInput.imageUrl
916
861
  };
917
- const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
862
+ const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
918
863
  attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
919
864
  console.log(`
920
- \u2705 MRQ generation successful on attempt ${attempt} (${duration}ms)`);
865
+ \u2705 Short Answer generation successful on attempt ${attempt} (${duration}ms)`);
921
866
  if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
922
867
  return { question: validatedQuestion };
923
868
  } catch (error) {
@@ -933,7 +878,7 @@ async function generateMRQQuestion(clientInput, apiKey) {
933
878
  }
934
879
  }
935
880
  DebugLogger.logAttemptSummary(attemptResults);
936
- const errorMessage = `Failed to generate MRQ question after ${MAX_RETRY_ATTEMPTS4} attempts. Last error: ${lastError?.message}`;
881
+ const errorMessage = `Failed to generate Short Answer question after ${MAX_RETRY_ATTEMPTS4} attempts. Last error: ${lastError?.message}`;
937
882
  console.error("\n\u274C Final Result: FAILED");
938
883
  console.error(errorMessage);
939
884
  return { error: errorMessage };
@@ -1118,6 +1063,192 @@ async function generateNumericQuestion(clientInput, apiKey) {
1118
1063
  console.error(errorMessage);
1119
1064
  return { error: errorMessage };
1120
1065
  }
1066
+ BaseQuestionGenerationClientInputSchema.extend({
1067
+ numberOfBlanks: zod.z.number().int().min(1).max(5).optional().default(1),
1068
+ isCaseSensitive: zod.z.boolean().optional().default(false)
1069
+ });
1070
+ var AIFillInTheBlanksOutputFieldsSchema = zod.z.object({
1071
+ prompt: zod.z.string().describe("The instructional text for the user, e.g., 'Fill in the blanks to complete the sentence.'"),
1072
+ // Yêu cầu AI trả về cấu trúc segments trực tiếp
1073
+ segments: zod.z.array(zod.z.object({
1074
+ type: zod.z.enum(["text", "blank"]),
1075
+ content: zod.z.string().optional().describe("The text content for a 'text' segment."),
1076
+ acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1).optional().describe("An array of correct answers for a 'blank' segment.")
1077
+ })).min(1).describe("An array of text and blank segments representing the question."),
1078
+ explanation: zod.z.string().optional(),
1079
+ points: zod.z.number().optional().default(10),
1080
+ difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
1081
+ topic: zod.z.string().optional(),
1082
+ verifiedCategory: zod.z.string().optional()
1083
+ // Thêm để xác thực
1084
+ });
1085
+
1086
+ // src/ai/flows/question-gen/generate-fitb-question.ts
1087
+ var MAX_RETRY_ATTEMPTS6 = 3;
1088
+ var RETRY_DELAY_MS6 = 3e3;
1089
+ function buildEnhancedPrompt6(clientInput, attemptNumber) {
1090
+ const { quizContext, language, difficulty, numberOfBlanks, imageUrl } = clientInput;
1091
+ const category = quizContext?.originalCategory || "the specified technical category";
1092
+ const attemptInfo = attemptNumber > 1 ? `
1093
+ ## DEBUG INFO - This is attempt #${attemptNumber}
1094
+ 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'.
1095
+
1096
+ ` : "";
1097
+ 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.` : "";
1098
+ const contextStrings = [
1099
+ `**Required Category:** ${category}`,
1100
+ quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
1101
+ imageContextInstruction,
1102
+ quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
1103
+ quizContext?.targetMisconception && `**Target Misconception:** Design the blank to test this specific point: "${quizContext.targetMisconception}"`
1104
+ ].filter(Boolean).map((s) => `- ${s}`).join("\n");
1105
+ const exampleJson = JSON.stringify({
1106
+ prompt: "Complete the following Swift code snippet.",
1107
+ segments: [
1108
+ { "type": "text", "content": "To declare a new function in Swift, you use the `" },
1109
+ { "type": "blank", "acceptedAnswers": ["func"] },
1110
+ { "type": "text", "content": "` keyword." }
1111
+ ],
1112
+ explanation: "The 'func' keyword is used to declare a function in the Swift programming language.",
1113
+ points: 10,
1114
+ difficulty: "easy",
1115
+ topic: "Swift Function Declaration",
1116
+ verifiedCategory: category
1117
+ }, null, 2);
1118
+ return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
1119
+ Your mission is to create a high-quality, technically accurate Fill-in-the-Blanks Question.
1120
+
1121
+ ## Core Rules (Non-negotiable)
1122
+ 1. **Category Purity:** The question MUST be exclusively about **${category}**.
1123
+ 2. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
1124
+ 3. **Logical Segments:** For 'blank' segments, you MUST provide 'acceptedAnswers'. For 'text' segments, you MUST provide 'content'. Do not mix them.
1125
+
1126
+ ## CRITICAL CONTEXT FOR THIS QUESTION
1127
+ ${contextStrings}
1128
+
1129
+ ## Task: Generate the Question
1130
+ Based on all the rules and context above, generate a single Fill-in-the-Blanks Question.
1131
+
1132
+ ### Input Parameters
1133
+ - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
1134
+ - **Language for Text:** ${language}
1135
+ - **Difficulty Level:** ${difficulty}
1136
+ - **Number of Blanks:** Generate exactly ${numberOfBlanks} segment(s) with type 'blank'.
1137
+
1138
+ ### Required JSON Output Format
1139
+ Your response must be ONLY the JSON object, matching this exact structure:
1140
+
1141
+ ${exampleJson}
1142
+
1143
+ Now, generate the JSON for the requested question.`;
1144
+ }
1145
+ async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
1146
+ const ai = new genai.GoogleGenAI({ apiKey });
1147
+ const model = "gemini-2.5-flash";
1148
+ const config = {
1149
+ temperature: 0.8,
1150
+ responseMimeType: "application/json",
1151
+ thinkingConfig: {
1152
+ thinkingBudget: 4e3
1153
+ }
1154
+ };
1155
+ const attemptResults = [];
1156
+ let lastError = null;
1157
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS6; attempt++) {
1158
+ const startTime = Date.now();
1159
+ const promptText = buildEnhancedPrompt6(clientInput, attempt);
1160
+ const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1161
+ try {
1162
+ DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
1163
+ const parts = [{ text: promptText }];
1164
+ if (clientInput.imageUrl) {
1165
+ const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
1166
+ const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
1167
+ parts.unshift(imagePart);
1168
+ }
1169
+ const contents = [{ role: "user", parts }];
1170
+ const aiResult = await ai.models.generateContent({ model, config, contents });
1171
+ const response = aiResult;
1172
+ const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
1173
+ const duration = Date.now() - startTime;
1174
+ DebugLogger.logResponse(attempt, rawText);
1175
+ if (!rawText) throw new Error("AI returned an empty response.");
1176
+ const parsedJson = JSON.parse(rawText);
1177
+ DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
1178
+ const aiGeneratedContent = AIFillInTheBlanksOutputFieldsSchema.parse(parsedJson);
1179
+ DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
1180
+ const blankCount = aiGeneratedContent.segments.filter((s) => s.type === "blank").length;
1181
+ if (blankCount !== clientInput.numberOfBlanks) {
1182
+ throw new Error(`AI generated ${blankCount} blanks, but ${clientInput.numberOfBlanks} were required.`);
1183
+ }
1184
+ aiGeneratedContent.segments.forEach((segment, index) => {
1185
+ if (segment.type === "blank" && (!segment.acceptedAnswers || segment.acceptedAnswers.length === 0)) {
1186
+ throw new Error(`Segment ${index} is a 'blank' but is missing 'acceptedAnswers'.`);
1187
+ }
1188
+ if (segment.type === "text" && typeof segment.content !== "string") {
1189
+ throw new Error(`Segment ${index} is 'text' but is missing 'content'.`);
1190
+ }
1191
+ });
1192
+ if (clientInput.quizContext?.originalCategory) {
1193
+ const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
1194
+ const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
1195
+ if (verifiedCategory && verifiedCategory !== requiredCategory) {
1196
+ throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
1197
+ }
1198
+ }
1199
+ const finalSegments = [];
1200
+ const finalAnswers = [];
1201
+ aiGeneratedContent.segments.forEach((segment) => {
1202
+ if (segment.type === "text") {
1203
+ finalSegments.push({ type: "text", content: segment.content });
1204
+ } else if (segment.type === "blank" && segment.acceptedAnswers) {
1205
+ const blankId = generateUniqueId("blank_");
1206
+ finalSegments.push({ type: "blank", id: blankId });
1207
+ finalAnswers.push({ blankId, acceptedValues: segment.acceptedAnswers });
1208
+ }
1209
+ });
1210
+ const completeQuestion = {
1211
+ id: generateUniqueId("fitb_ai_"),
1212
+ questionType: "fill_in_the_blanks",
1213
+ prompt: aiGeneratedContent.prompt,
1214
+ segments: finalSegments,
1215
+ answers: finalAnswers,
1216
+ isCaseSensitive: clientInput.isCaseSensitive,
1217
+ explanation: aiGeneratedContent.explanation,
1218
+ points: aiGeneratedContent.points,
1219
+ topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
1220
+ difficulty: clientInput.difficulty,
1221
+ contextCode: clientInput.quizContext?.plannedContextId,
1222
+ bloomLevel: clientInput.quizContext?.plannedBloomLevel,
1223
+ learningObjective: clientInput.quizContext?.originalLoId,
1224
+ subject: clientInput.quizContext?.originalSubject,
1225
+ category: clientInput.quizContext?.originalCategory,
1226
+ imageUrl: clientInput.imageUrl
1227
+ };
1228
+ const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
1229
+ attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
1230
+ console.log(`
1231
+ \u2705 FITB generation successful on attempt ${attempt} (${duration}ms)`);
1232
+ if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
1233
+ return { question: validatedQuestion };
1234
+ } catch (error) {
1235
+ lastError = error;
1236
+ const duration = Date.now() - startTime;
1237
+ attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1238
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS6;
1239
+ DebugLogger.logRetryInfo(attempt, error, willRetry);
1240
+ if (willRetry) {
1241
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS6}ms...`);
1242
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS6));
1243
+ }
1244
+ }
1245
+ }
1246
+ DebugLogger.logAttemptSummary(attemptResults);
1247
+ const errorMessage = `Failed to generate FITB question after ${MAX_RETRY_ATTEMPTS6} attempts. Last error: ${lastError?.message}`;
1248
+ console.error("\n\u274C Final Result: FAILED");
1249
+ console.error(errorMessage);
1250
+ return { error: errorMessage };
1251
+ }
1121
1252
  BaseQuestionGenerationClientInputSchema.extend({
1122
1253
  numberOfItems: zod.z.number().int().min(2).max(10).optional().default(4)
1123
1254
  });
@@ -1135,9 +1266,9 @@ var AISequenceOutputFieldsSchema = zod.z.object({
1135
1266
  });
1136
1267
 
1137
1268
  // src/ai/flows/question-gen/generate-sequence-question.ts
1138
- var MAX_RETRY_ATTEMPTS6 = 3;
1139
- var RETRY_DELAY_MS6 = 3e3;
1140
- function buildEnhancedPrompt6(clientInput, attemptNumber) {
1269
+ var MAX_RETRY_ATTEMPTS7 = 3;
1270
+ var RETRY_DELAY_MS7 = 3e3;
1271
+ function buildEnhancedPrompt7(clientInput, attemptNumber) {
1141
1272
  const { quizContext, language, difficulty, numberOfItems, imageUrl } = clientInput;
1142
1273
  const category = quizContext?.originalCategory || "the specified technical category";
1143
1274
  const attemptInfo = attemptNumber > 1 ? `
@@ -1206,9 +1337,9 @@ async function generateSequenceQuestion(clientInput, apiKey) {
1206
1337
  };
1207
1338
  const attemptResults = [];
1208
1339
  let lastError = null;
1209
- for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS6; attempt++) {
1340
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS7; attempt++) {
1210
1341
  const startTime = Date.now();
1211
- const promptText = buildEnhancedPrompt6(clientInput, attempt);
1342
+ const promptText = buildEnhancedPrompt7(clientInput, attempt);
1212
1343
  const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1213
1344
  try {
1214
1345
  DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
@@ -1273,27 +1404,30 @@ async function generateSequenceQuestion(clientInput, apiKey) {
1273
1404
  lastError = error;
1274
1405
  const duration = Date.now() - startTime;
1275
1406
  attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1276
- const willRetry = attempt < MAX_RETRY_ATTEMPTS6;
1407
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS7;
1277
1408
  DebugLogger.logRetryInfo(attempt, error, willRetry);
1278
1409
  if (willRetry) {
1279
- console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS6}ms...`);
1280
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS6));
1410
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS7}ms...`);
1411
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS7));
1281
1412
  }
1282
1413
  }
1283
1414
  }
1284
1415
  DebugLogger.logAttemptSummary(attemptResults);
1285
- const errorMessage = `Failed to generate Sequence question after ${MAX_RETRY_ATTEMPTS6} attempts. Last error: ${lastError?.message}`;
1416
+ const errorMessage = `Failed to generate Sequence question after ${MAX_RETRY_ATTEMPTS7} attempts. Last error: ${lastError?.message}`;
1286
1417
  console.error("\n\u274C Final Result: FAILED");
1287
1418
  console.error(errorMessage);
1288
1419
  return { error: errorMessage };
1289
1420
  }
1290
1421
  BaseQuestionGenerationClientInputSchema.extend({
1291
- isCaseSensitive: zod.z.boolean().optional().default(false)
1422
+ numberOfPairs: zod.z.number().int().min(2).max(8).optional().default(4),
1423
+ shuffleOptions: zod.z.boolean().optional().default(true)
1292
1424
  });
1293
- var AIShortAnswerOutputFieldsSchema = zod.z.object({
1294
- prompt: zod.z.string().describe("The question text that prompts the user for a short answer."),
1295
- acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1).describe("An array of one or more acceptable short answers. Include common variations if applicable."),
1296
- // isCaseSensitive không cần thiết đây, chúng ta sẽ quản lý nó ở phía client
1425
+ var AIMatchingOutputFieldsSchema = zod.z.object({
1426
+ prompt: zod.z.string().describe("The instructional text for the user, e.g., 'Match the concept to its definition.'"),
1427
+ correctPairs: zod.z.array(zod.z.object({
1428
+ promptText: zod.z.string().min(1).describe("The text for the left-hand side item (the prompt)."),
1429
+ optionText: zod.z.string().min(1).describe("The text for the right-hand side item (the matching option).")
1430
+ })).min(2),
1297
1431
  explanation: zod.z.string().optional(),
1298
1432
  points: zod.z.number().optional().default(10),
1299
1433
  difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
@@ -1302,52 +1436,57 @@ var AIShortAnswerOutputFieldsSchema = zod.z.object({
1302
1436
  // Thêm để xác thực
1303
1437
  });
1304
1438
 
1305
- // src/ai/flows/question-gen/generate-short-answer-question.ts
1306
- var MAX_RETRY_ATTEMPTS7 = 3;
1307
- var RETRY_DELAY_MS7 = 3e3;
1308
- function buildEnhancedPrompt7(clientInput, attemptNumber) {
1309
- const { quizContext, language, difficulty, imageUrl } = clientInput;
1439
+ // src/ai/flows/question-gen/generate-matching-question.ts
1440
+ var MAX_RETRY_ATTEMPTS8 = 3;
1441
+ var RETRY_DELAY_MS8 = 3e3;
1442
+ function buildEnhancedPrompt8(clientInput, attemptNumber) {
1443
+ const { quizContext, language, difficulty, numberOfPairs, imageUrl } = clientInput;
1310
1444
  const category = quizContext?.originalCategory || "the specified technical category";
1311
1445
  const attemptInfo = attemptNumber > 1 ? `
1312
1446
  ## DEBUG INFO - This is attempt #${attemptNumber}
1313
- Previous attempts failed. Ensure 'acceptedAnswers' is a non-empty array of strings and the JSON is valid.
1447
+ Previous attempts failed. Please ensure the 'correctPairs' array has exactly the required number of items and the JSON is valid.
1314
1448
 
1315
1449
  ` : "";
1316
- 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.` : "";
1450
+ const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The matching pairs must be directly related to the content of this image.` : "";
1317
1451
  const contextStrings = [
1318
1452
  `**Required Category:** ${category}`,
1319
1453
  quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
1320
1454
  imageContextInstruction,
1321
1455
  quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
1322
- quizContext?.targetMisconception && `**Target Misconception:** The question should require an answer that corrects this specific misconception: "${quizContext.targetMisconception}"`
1456
+ quizContext?.targetMisconception && `**Target Misconception:** Design a pair that specifically tests this confusion: "${quizContext.targetMisconception}"`
1323
1457
  ].filter(Boolean).map((s) => `- ${s}`).join("\n");
1324
1458
  const exampleJson = JSON.stringify({
1325
- prompt: "In Swift, what keyword is used to declare a constant?",
1326
- acceptedAnswers: ["let"],
1327
- 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.",
1459
+ prompt: "Match each Swift collection type to its primary characteristic.",
1460
+ correctPairs: [
1461
+ { "promptText": "Array", "optionText": "An ordered, random-access collection." },
1462
+ { "promptText": "Set", "optionText": "An unordered collection of unique elements." },
1463
+ { "promptText": "Dictionary", "optionText": "An unordered collection of key-value associations." }
1464
+ ],
1465
+ explanation: "These are the fundamental characteristics of Swift's main collection types.",
1328
1466
  points: 10,
1329
1467
  difficulty: "easy",
1330
- topic: "Swift Constants",
1468
+ topic: "Swift Collection Types",
1331
1469
  verifiedCategory: category
1332
1470
  }, null, 2);
1333
1471
  return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
1334
- Your mission is to create a high-quality, technically accurate Short Answer Question.
1472
+ Your mission is to create a high-quality, technically accurate Matching Question.
1335
1473
 
1336
1474
  ## Core Rules (Non-negotiable)
1337
1475
  1. **Category Purity:** The question MUST be exclusively about **${category}**.
1338
- 2. **Objective Answer:** The question must have a short, factual, and objective answer. Avoid questions that are subjective or require long explanations.
1339
- 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
1476
+ 2. **Logical Pairs:** The items to be matched must have a clear, one-to-one relationship.
1477
+ 3. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object that strictly follows the provided schema.
1340
1478
 
1341
1479
  ## CRITICAL CONTEXT FOR THIS QUESTION
1342
1480
  ${contextStrings}
1343
1481
 
1344
1482
  ## Task: Generate the Question
1345
- Based on all the rules and context above, generate a single Short Answer Question.
1483
+ Based on all the rules and context above, generate a single Matching Question.
1346
1484
 
1347
1485
  ### Input Parameters
1348
1486
  - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
1349
1487
  - **Language for Text:** ${language}
1350
1488
  - **Difficulty Level:** ${difficulty}
1489
+ - **Number of Pairs:** Generate exactly ${numberOfPairs} correct pairs in the 'correctPairs' array.
1351
1490
 
1352
1491
  ### Required JSON Output Format
1353
1492
  Your response must be ONLY the JSON object, matching this exact structure:
@@ -1356,11 +1495,11 @@ ${exampleJson}
1356
1495
 
1357
1496
  Now, generate the JSON for the requested question.`;
1358
1497
  }
1359
- async function generateShortAnswerQuestion(clientInput, apiKey) {
1498
+ async function generateMatchingQuestion(clientInput, apiKey) {
1360
1499
  const ai = new genai.GoogleGenAI({ apiKey });
1361
1500
  const model = "gemini-2.5-flash";
1362
1501
  const config = {
1363
- temperature: 0.5,
1502
+ temperature: 0.8,
1364
1503
  responseMimeType: "application/json",
1365
1504
  thinkingConfig: {
1366
1505
  thinkingBudget: 4e3
@@ -1368,9 +1507,9 @@ async function generateShortAnswerQuestion(clientInput, apiKey) {
1368
1507
  };
1369
1508
  const attemptResults = [];
1370
1509
  let lastError = null;
1371
- for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS7; attempt++) {
1510
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS8; attempt++) {
1372
1511
  const startTime = Date.now();
1373
- const promptText = buildEnhancedPrompt7(clientInput, attempt);
1512
+ const promptText = buildEnhancedPrompt8(clientInput, attempt);
1374
1513
  const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1375
1514
  try {
1376
1515
  DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
@@ -1389,8 +1528,11 @@ async function generateShortAnswerQuestion(clientInput, apiKey) {
1389
1528
  if (!rawText) throw new Error("AI returned an empty response.");
1390
1529
  const parsedJson = JSON.parse(rawText);
1391
1530
  DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
1392
- const aiGeneratedContent = AIShortAnswerOutputFieldsSchema.parse(parsedJson);
1531
+ const aiGeneratedContent = AIMatchingOutputFieldsSchema.parse(parsedJson);
1393
1532
  DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
1533
+ if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
1534
+ throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were required.`);
1535
+ }
1394
1536
  if (clientInput.quizContext?.originalCategory) {
1395
1537
  const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
1396
1538
  const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
@@ -1398,12 +1540,24 @@ async function generateShortAnswerQuestion(clientInput, apiKey) {
1398
1540
  throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
1399
1541
  }
1400
1542
  }
1543
+ const finalPrompts = [];
1544
+ const finalOptions = [];
1545
+ const finalCorrectAnswerMap = [];
1546
+ aiGeneratedContent.correctPairs.forEach((pair) => {
1547
+ const promptId = generateUniqueId("m_p_");
1548
+ const optionId = generateUniqueId("m_o_");
1549
+ finalPrompts.push({ id: promptId, content: pair.promptText });
1550
+ finalOptions.push({ id: optionId, content: pair.optionText });
1551
+ finalCorrectAnswerMap.push({ promptId, optionId });
1552
+ });
1401
1553
  const completeQuestion = {
1402
- id: generateUniqueId("saq_ai_"),
1403
- questionType: "short_answer",
1554
+ id: generateUniqueId("match_ai_"),
1555
+ questionType: "matching",
1404
1556
  prompt: aiGeneratedContent.prompt,
1405
- acceptedAnswers: aiGeneratedContent.acceptedAnswers,
1406
- isCaseSensitive: clientInput.isCaseSensitive,
1557
+ prompts: finalPrompts,
1558
+ options: finalOptions,
1559
+ correctAnswerMap: finalCorrectAnswerMap,
1560
+ shuffleOptions: clientInput.shuffleOptions,
1407
1561
  explanation: aiGeneratedContent.explanation,
1408
1562
  points: aiGeneratedContent.points,
1409
1563
  topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
@@ -1415,90 +1569,95 @@ async function generateShortAnswerQuestion(clientInput, apiKey) {
1415
1569
  category: clientInput.quizContext?.originalCategory,
1416
1570
  imageUrl: clientInput.imageUrl
1417
1571
  };
1418
- const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
1572
+ const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
1419
1573
  attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
1420
1574
  console.log(`
1421
- \u2705 Short Answer generation successful on attempt ${attempt} (${duration}ms)`);
1575
+ \u2705 Matching generation successful on attempt ${attempt} (${duration}ms)`);
1422
1576
  if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
1423
1577
  return { question: validatedQuestion };
1424
1578
  } catch (error) {
1425
1579
  lastError = error;
1426
1580
  const duration = Date.now() - startTime;
1427
1581
  attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1428
- const willRetry = attempt < MAX_RETRY_ATTEMPTS7;
1582
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS8;
1429
1583
  DebugLogger.logRetryInfo(attempt, error, willRetry);
1430
1584
  if (willRetry) {
1431
- console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS7}ms...`);
1432
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS7));
1585
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS8}ms...`);
1586
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS8));
1433
1587
  }
1434
1588
  }
1435
1589
  }
1436
1590
  DebugLogger.logAttemptSummary(attemptResults);
1437
- const errorMessage = `Failed to generate Short Answer question after ${MAX_RETRY_ATTEMPTS7} attempts. Last error: ${lastError?.message}`;
1591
+ const errorMessage = `Failed to generate Matching question after ${MAX_RETRY_ATTEMPTS8} attempts. Last error: ${lastError?.message}`;
1438
1592
  console.error("\n\u274C Final Result: FAILED");
1439
1593
  console.error(errorMessage);
1440
1594
  return { error: errorMessage };
1441
1595
  }
1442
- BaseQuestionGenerationClientInputSchema.extend({});
1443
- var AITrueFalseOutputFieldsSchema = zod.z.object({
1444
- prompt: zod.z.string().describe("The statement that the user will evaluate as true or false."),
1445
- correctAnswer: zod.z.boolean(),
1446
- explanation: zod.z.string().optional().describe("An explanation of why the statement is true or false, especially important if false."),
1447
- points: zod.z.number().optional().default(10),
1448
- difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
1449
- topic: zod.z.string().optional(),
1450
- verifiedCategory: zod.z.string().optional()
1451
- // Thêm để xác thực
1596
+ BaseQuestionGenerationClientInputSchema.extend({
1597
+ codingLanguage: zod.z.enum(["cpp", "javascript", "python", "swift", "csharp"])
1598
+ });
1599
+ var AICodingQuestionOutputSchema = zod.z.object({
1600
+ prompt: zod.z.string().describe("The problem description for the user."),
1601
+ functionSignature: zod.z.string().optional().describe("A suggested function signature for the user to implement."),
1602
+ solutionCode: zod.z.string().describe("A complete, correct model solution in the specified language."),
1603
+ testCases: zod.z.array(zod.z.object({
1604
+ input: zod.z.array(zod.z.any()),
1605
+ // FIX: Use .refine to make it explicitly non-optional for TypeScript's inference
1606
+ expectedOutput: zod.z.any().refine((val) => val !== void 0, {
1607
+ message: "expectedOutput is required and cannot be undefined."
1608
+ }),
1609
+ isPublic: zod.z.boolean()
1610
+ })).min(3, { message: "Must provide at least 3 test cases." }),
1611
+ verifiedCodingLanguage: zod.z.enum(["cpp", "javascript", "python", "swift", "csharp"]).optional().describe("The programming language this question actually addresses.")
1452
1612
  });
1453
1613
 
1454
- // src/ai/flows/question-gen/generate-true-false-question.ts
1455
- var MAX_RETRY_ATTEMPTS8 = 3;
1456
- var RETRY_DELAY_MS8 = 3e3;
1457
- function buildEnhancedPrompt8(clientInput, attemptNumber) {
1458
- const { quizContext, language, difficulty, imageUrl } = clientInput;
1459
- const category = quizContext?.originalCategory || "the specified technical category";
1614
+ // src/ai/flows/question-gen/generate-coding-question.ts
1615
+ var MAX_RETRY_ATTEMPTS9 = 3;
1616
+ var RETRY_DELAY_MS9 = 3e3;
1617
+ function buildEnhancedPrompt9(clientInput, attemptNumber) {
1618
+ const { quizContext, difficulty, codingLanguage, language, imageUrl } = clientInput;
1619
+ const subject = quizContext?.originalSubject || codingLanguage;
1460
1620
  const attemptInfo = attemptNumber > 1 ? `
1461
1621
  ## DEBUG INFO - This is attempt #${attemptNumber}
1462
- Previous attempts failed. Ensure the JSON is valid and 'correctAnswer' is a boolean.
1622
+ Previous attempts failed. Pay strict attention to the JSON schema and all rules. Ensure every object in the 'testCases' array has a non-null 'expectedOutput' field.
1463
1623
 
1464
1624
  ` : "";
1465
- 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.` : "";
1466
- const misconceptionGuidance = quizContext?.targetMisconception ? `**Target Misconception:** The statement you create MUST be FALSE and based on this common mistake: "${quizContext.targetMisconception}"` : "";
1625
+ 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.` : "";
1467
1626
  const contextStrings = [
1468
- `**Required Category:** ${category}`,
1627
+ `**Subject:** ${subject}`,
1469
1628
  quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
1470
1629
  imageContextInstruction,
1471
1630
  quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
1472
- misconceptionGuidance,
1473
- quizContext?.difficultyReason && `**Pedagogical Reason:** ${quizContext.difficultyReason}`
1631
+ quizContext?.targetMisconception && `**Target Misconception:** The problem should test against this common error: "${quizContext.targetMisconception}"`
1474
1632
  ].filter(Boolean).map((s) => `- ${s}`).join("\n");
1475
1633
  const exampleJson = JSON.stringify({
1476
- prompt: "In Swift, you must explicitly unwrap an Optional value before you can use its stored value.",
1477
- correctAnswer: true,
1478
- 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 '!'.",
1479
- points: 10,
1480
- difficulty: "easy",
1481
- topic: "Swift Optionals",
1482
- verifiedCategory: category
1634
+ prompt: "Write a function named 'add' that takes two integers and returns their sum.",
1635
+ functionSignature: "function add(a, b) { ... }",
1636
+ solutionCode: "function add(a, b) {\n return a + b;\n}",
1637
+ testCases: [
1638
+ { "input": [1, 2], "expectedOutput": 3, "isPublic": true },
1639
+ { "input": [-1, 1], "expectedOutput": 0, "isPublic": true },
1640
+ { "input": [0, 0], "expectedOutput": 0, "isPublic": false }
1641
+ ],
1642
+ verifiedCodingLanguage: "javascript"
1483
1643
  }, null, 2);
1484
- return `${attemptInfo}You are an expert Question Author for advanced technical education, specializing in: ${category}.
1485
- Your mission is to create a high-quality, technically accurate True/False Question.
1644
+ return `${attemptInfo}You are an expert programming problem designer for ${subject}.
1645
+ Generate a single, high-quality Coding question.
1486
1646
 
1487
- ## Core Rules (Non-negotiable)
1488
- 1. **Category Purity:** The statement ('prompt') MUST be exclusively about **${category}**.
1489
- 2. **Clarity:** The statement must be definitively true or false, with no ambiguity.
1490
- 3. **Misconception Priority:** If a Target Misconception is provided, the statement MUST be FALSE and reflect that misconception. This is a critical rule.
1491
- 4. **Schema Integrity:** The response MUST be ONLY a single, valid JSON object.
1647
+ ## Core Rules
1648
+ 1. **Language Purity:** All code ('functionSignature', 'solutionCode') MUST be in **${codingLanguage}**.
1649
+ 2. **Context Adherence:** The problem MUST be directly related to the provided context.
1650
+ 3. **Format Integrity:** You MUST return ONLY a single, valid JSON object.
1651
+ 4. **Test Case Integrity:** Every test case object in the 'testCases' array MUST have a non-null and defined 'expectedOutput' field. This is a critical rule.
1492
1652
 
1493
1653
  ## CRITICAL CONTEXT FOR THIS QUESTION
1494
1654
  ${contextStrings}
1495
1655
 
1496
1656
  ## Task: Generate the Question
1497
- Based on all the rules and context above, generate a single True/False Question.
1498
-
1499
1657
  ### Input Parameters
1500
1658
  - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
1501
- - **Language for Text:** ${language}
1659
+ - **Natural Language for Text:** ${language}
1660
+ - **Coding Language:** ${codingLanguage}
1502
1661
  - **Difficulty Level:** ${difficulty}
1503
1662
 
1504
1663
  ### Required JSON Output Format
@@ -1508,21 +1667,21 @@ ${exampleJson}
1508
1667
 
1509
1668
  Now, generate the JSON for the requested question.`;
1510
1669
  }
1511
- async function generateTrueFalseQuestion(clientInput, apiKey) {
1670
+ async function generateCodingQuestion(clientInput, apiKey) {
1512
1671
  const ai = new genai.GoogleGenAI({ apiKey });
1513
1672
  const model = "gemini-2.5-flash";
1514
1673
  const config = {
1515
- temperature: 0.6,
1674
+ temperature: 0.5,
1516
1675
  responseMimeType: "application/json",
1517
1676
  thinkingConfig: {
1518
- thinkingBudget: 4e3
1677
+ thinkingBudget: 5e3
1519
1678
  }
1520
1679
  };
1521
1680
  const attemptResults = [];
1522
1681
  let lastError = null;
1523
- for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS8; attempt++) {
1682
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS9; attempt++) {
1524
1683
  const startTime = Date.now();
1525
- const promptText = buildEnhancedPrompt8(clientInput, attempt);
1684
+ const promptText = buildEnhancedPrompt9(clientInput, attempt);
1526
1685
  const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
1527
1686
  try {
1528
1687
  DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
@@ -1541,26 +1700,27 @@ async function generateTrueFalseQuestion(clientInput, apiKey) {
1541
1700
  if (!rawText) throw new Error("AI returned an empty response.");
1542
1701
  const parsedJson = JSON.parse(rawText);
1543
1702
  DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
1544
- const aiGeneratedContent = AITrueFalseOutputFieldsSchema.parse(parsedJson);
1703
+ const aiGeneratedContent = AICodingQuestionOutputSchema.parse(parsedJson);
1545
1704
  DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
1546
- if (clientInput.quizContext?.targetMisconception && aiGeneratedContent.correctAnswer === true) {
1547
- throw new Error("AI failed to follow the Misconception Priority rule. The answer should have been false.");
1548
- }
1549
- if (clientInput.quizContext?.originalCategory) {
1550
- const verifiedCategory = aiGeneratedContent.verifiedCategory?.toLowerCase();
1551
- const requiredCategory = clientInput.quizContext.originalCategory.toLowerCase();
1552
- if (verifiedCategory && verifiedCategory !== requiredCategory) {
1553
- throw new Error(`Category mismatch: Required ${requiredCategory}, got ${verifiedCategory}`);
1554
- }
1705
+ if (aiGeneratedContent.verifiedCodingLanguage && aiGeneratedContent.verifiedCodingLanguage !== clientInput.codingLanguage) {
1706
+ throw new Error(`Language mismatch: Required ${clientInput.codingLanguage}, but AI generated for ${aiGeneratedContent.verifiedCodingLanguage}.`);
1555
1707
  }
1708
+ const testCases = aiGeneratedContent.testCases.map((tc) => ({
1709
+ id: generateUniqueId("tc_"),
1710
+ input: tc.input,
1711
+ expectedOutput: tc.expectedOutput,
1712
+ isPublic: tc.isPublic
1713
+ }));
1556
1714
  const completeQuestion = {
1557
- id: generateUniqueId("tf_ai_"),
1558
- questionType: "true_false",
1715
+ id: generateUniqueId("coding_"),
1716
+ questionType: "coding",
1559
1717
  prompt: aiGeneratedContent.prompt,
1560
- correctAnswer: aiGeneratedContent.correctAnswer,
1561
- explanation: aiGeneratedContent.explanation,
1562
- points: aiGeneratedContent.points,
1563
- topic: aiGeneratedContent.topic || clientInput.quizContext?.originalTopic,
1718
+ codingLanguage: clientInput.codingLanguage,
1719
+ functionSignature: aiGeneratedContent.functionSignature,
1720
+ solutionCode: aiGeneratedContent.solutionCode,
1721
+ testCases,
1722
+ points: 25,
1723
+ topic: clientInput.quizContext?.originalTopic,
1564
1724
  difficulty: clientInput.difficulty,
1565
1725
  contextCode: clientInput.quizContext?.plannedContextId,
1566
1726
  bloomLevel: clientInput.quizContext?.plannedBloomLevel,
@@ -1569,26 +1729,26 @@ async function generateTrueFalseQuestion(clientInput, apiKey) {
1569
1729
  category: clientInput.quizContext?.originalCategory,
1570
1730
  imageUrl: clientInput.imageUrl
1571
1731
  };
1572
- const validatedQuestion = TrueFalseQuestionZodSchema.parse(completeQuestion);
1732
+ CodingQuestionZodSchema.parse(completeQuestion);
1573
1733
  attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
1574
1734
  console.log(`
1575
- \u2705 True/False generation successful on attempt ${attempt} (${duration}ms)`);
1735
+ \u2705 Coding question generation successful on attempt ${attempt} (${duration}ms)`);
1576
1736
  if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
1577
- return { question: validatedQuestion };
1737
+ return { question: completeQuestion };
1578
1738
  } catch (error) {
1579
1739
  lastError = error;
1580
1740
  const duration = Date.now() - startTime;
1581
1741
  attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
1582
- const willRetry = attempt < MAX_RETRY_ATTEMPTS8;
1742
+ const willRetry = attempt < MAX_RETRY_ATTEMPTS9;
1583
1743
  DebugLogger.logRetryInfo(attempt, error, willRetry);
1584
1744
  if (willRetry) {
1585
- console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS8}ms...`);
1586
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS8));
1745
+ console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS9}ms...`);
1746
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS9));
1587
1747
  }
1588
1748
  }
1589
1749
  }
1590
1750
  DebugLogger.logAttemptSummary(attemptResults);
1591
- const errorMessage = `Failed to generate True/False question after ${MAX_RETRY_ATTEMPTS8} attempts. Last error: ${lastError?.message}`;
1751
+ const errorMessage = `Failed to generate Coding question after ${MAX_RETRY_ATTEMPTS9} attempts. Last error: ${lastError?.message}`;
1592
1752
  console.error("\n\u274C Final Result: FAILED");
1593
1753
  console.error(errorMessage);
1594
1754
  return { error: errorMessage };
@@ -2496,166 +2656,6 @@ TopicDataService.EXPECTED_HEADERS = [
2496
2656
  "STEM Element(s)",
2497
2657
  "Bloom\u2019s Level(s) Guideline"
2498
2658
  ];
2499
- BaseQuestionGenerationClientInputSchema.extend({
2500
- codingLanguage: zod.z.enum(["cpp", "javascript", "python", "swift", "csharp"])
2501
- });
2502
- var AICodingQuestionOutputSchema = zod.z.object({
2503
- prompt: zod.z.string().describe("The problem description for the user."),
2504
- functionSignature: zod.z.string().optional().describe("A suggested function signature for the user to implement."),
2505
- solutionCode: zod.z.string().describe("A complete, correct model solution in the specified language."),
2506
- testCases: zod.z.array(zod.z.object({
2507
- input: zod.z.array(zod.z.any()),
2508
- // FIX: Use .refine to make it explicitly non-optional for TypeScript's inference
2509
- expectedOutput: zod.z.any().refine((val) => val !== void 0, {
2510
- message: "expectedOutput is required and cannot be undefined."
2511
- }),
2512
- isPublic: zod.z.boolean()
2513
- })).min(3, { message: "Must provide at least 3 test cases." }),
2514
- verifiedCodingLanguage: zod.z.enum(["cpp", "javascript", "python", "swift", "csharp"]).optional().describe("The programming language this question actually addresses.")
2515
- });
2516
-
2517
- // src/ai/flows/question-gen/generate-coding-question.ts
2518
- var MAX_RETRY_ATTEMPTS9 = 3;
2519
- var RETRY_DELAY_MS9 = 3e3;
2520
- function buildEnhancedPrompt9(clientInput, attemptNumber) {
2521
- const { quizContext, difficulty, codingLanguage, language, imageUrl } = clientInput;
2522
- const subject = quizContext?.originalSubject || codingLanguage;
2523
- const attemptInfo = attemptNumber > 1 ? `
2524
- ## DEBUG INFO - This is attempt #${attemptNumber}
2525
- Previous attempts failed. Pay strict attention to the JSON schema and all rules. Ensure every object in the 'testCases' array has a non-null 'expectedOutput' field.
2526
-
2527
- ` : "";
2528
- 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.` : "";
2529
- const contextStrings = [
2530
- `**Subject:** ${subject}`,
2531
- quizContext?.loDescription && `**Learning Objective:** ${quizContext.loDescription}`,
2532
- imageContextInstruction,
2533
- quizContext?.plannedBloomLevel && `**Cognitive Level (Bloom's):** ${quizContext.plannedBloomLevel}`,
2534
- quizContext?.targetMisconception && `**Target Misconception:** The problem should test against this common error: "${quizContext.targetMisconception}"`
2535
- ].filter(Boolean).map((s) => `- ${s}`).join("\n");
2536
- const exampleJson = JSON.stringify({
2537
- prompt: "Write a function named 'add' that takes two integers and returns their sum.",
2538
- functionSignature: "function add(a, b) { ... }",
2539
- solutionCode: "function add(a, b) {\n return a + b;\n}",
2540
- testCases: [
2541
- { "input": [1, 2], "expectedOutput": 3, "isPublic": true },
2542
- { "input": [-1, 1], "expectedOutput": 0, "isPublic": true },
2543
- { "input": [0, 0], "expectedOutput": 0, "isPublic": false }
2544
- ],
2545
- verifiedCodingLanguage: "javascript"
2546
- }, null, 2);
2547
- return `${attemptInfo}You are an expert programming problem designer for ${subject}.
2548
- Generate a single, high-quality Coding question.
2549
-
2550
- ## Core Rules
2551
- 1. **Language Purity:** All code ('functionSignature', 'solutionCode') MUST be in **${codingLanguage}**.
2552
- 2. **Context Adherence:** The problem MUST be directly related to the provided context.
2553
- 3. **Format Integrity:** You MUST return ONLY a single, valid JSON object.
2554
- 4. **Test Case Integrity:** Every test case object in the 'testCases' array MUST have a non-null and defined 'expectedOutput' field. This is a critical rule.
2555
-
2556
- ## CRITICAL CONTEXT FOR THIS QUESTION
2557
- ${contextStrings}
2558
-
2559
- ## Task: Generate the Question
2560
- ### Input Parameters
2561
- - **Topic for Question:** ${quizContext?.plannedTopic || "General"}
2562
- - **Natural Language for Text:** ${language}
2563
- - **Coding Language:** ${codingLanguage}
2564
- - **Difficulty Level:** ${difficulty}
2565
-
2566
- ### Required JSON Output Format
2567
- Your response must be ONLY the JSON object, matching this exact structure:
2568
-
2569
- ${exampleJson}
2570
-
2571
- Now, generate the JSON for the requested question.`;
2572
- }
2573
- async function generateCodingQuestion(clientInput, apiKey) {
2574
- const ai = new genai.GoogleGenAI({ apiKey });
2575
- const model = "gemini-2.5-flash";
2576
- const config = {
2577
- temperature: 0.5,
2578
- responseMimeType: "application/json",
2579
- thinkingConfig: {
2580
- thinkingBudget: 5e3
2581
- }
2582
- };
2583
- const attemptResults = [];
2584
- let lastError = null;
2585
- for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS9; attempt++) {
2586
- const startTime = Date.now();
2587
- const promptText = buildEnhancedPrompt9(clientInput, attempt);
2588
- const promptHash = Buffer.from(promptText).toString("base64").slice(0, 10);
2589
- try {
2590
- DebugLogger.logPrompt(attempt, promptText, { ...clientInput, attemptNumber: attempt, promptHash });
2591
- const parts = [{ text: promptText }];
2592
- if (clientInput.imageUrl) {
2593
- const mimeType = clientInput.imageUrl.endsWith(".png") ? "image/png" : "image/jpeg";
2594
- const imagePart = await urlToGenerativePart(clientInput.imageUrl, mimeType);
2595
- parts.unshift(imagePart);
2596
- }
2597
- const contents = [{ role: "user", parts }];
2598
- const aiResult = await ai.models.generateContent({ model, config, contents });
2599
- const response = aiResult;
2600
- const rawText = response.candidates?.[0]?.content?.parts?.[0]?.text || "";
2601
- const duration = Date.now() - startTime;
2602
- DebugLogger.logResponse(attempt, rawText);
2603
- if (!rawText) throw new Error("AI returned an empty response.");
2604
- const parsedJson = JSON.parse(rawText);
2605
- DebugLogger.logValidation(attempt, "JSON Parsed Successfully", parsedJson);
2606
- const aiGeneratedContent = AICodingQuestionOutputSchema.parse(parsedJson);
2607
- DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
2608
- if (aiGeneratedContent.verifiedCodingLanguage && aiGeneratedContent.verifiedCodingLanguage !== clientInput.codingLanguage) {
2609
- throw new Error(`Language mismatch: Required ${clientInput.codingLanguage}, but AI generated for ${aiGeneratedContent.verifiedCodingLanguage}.`);
2610
- }
2611
- const testCases = aiGeneratedContent.testCases.map((tc) => ({
2612
- id: generateUniqueId("tc_"),
2613
- input: tc.input,
2614
- expectedOutput: tc.expectedOutput,
2615
- isPublic: tc.isPublic
2616
- }));
2617
- const completeQuestion = {
2618
- id: generateUniqueId("coding_"),
2619
- questionType: "coding",
2620
- prompt: aiGeneratedContent.prompt,
2621
- codingLanguage: clientInput.codingLanguage,
2622
- functionSignature: aiGeneratedContent.functionSignature,
2623
- solutionCode: aiGeneratedContent.solutionCode,
2624
- testCases,
2625
- points: 25,
2626
- topic: clientInput.quizContext?.originalTopic,
2627
- difficulty: clientInput.difficulty,
2628
- contextCode: clientInput.quizContext?.plannedContextId,
2629
- bloomLevel: clientInput.quizContext?.plannedBloomLevel,
2630
- learningObjective: clientInput.quizContext?.originalLoId,
2631
- subject: clientInput.quizContext?.originalSubject,
2632
- category: clientInput.quizContext?.originalCategory,
2633
- imageUrl: clientInput.imageUrl
2634
- };
2635
- CodingQuestionZodSchema.parse(completeQuestion);
2636
- attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
2637
- console.log(`
2638
- \u2705 Coding question generation successful on attempt ${attempt} (${duration}ms)`);
2639
- if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
2640
- return { question: completeQuestion };
2641
- } catch (error) {
2642
- lastError = error;
2643
- const duration = Date.now() - startTime;
2644
- attemptResults.push({ success: false, duration, error: error.message, promptLength: promptText.length, promptHash });
2645
- const willRetry = attempt < MAX_RETRY_ATTEMPTS9;
2646
- DebugLogger.logRetryInfo(attempt, error, willRetry);
2647
- if (willRetry) {
2648
- console.log(`\u23F3 Retrying in ${RETRY_DELAY_MS9}ms...`);
2649
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS9));
2650
- }
2651
- }
2652
- }
2653
- DebugLogger.logAttemptSummary(attemptResults);
2654
- const errorMessage = `Failed to generate Coding question after ${MAX_RETRY_ATTEMPTS9} attempts. Last error: ${lastError?.message}`;
2655
- console.error("\n\u274C Final Result: FAILED");
2656
- console.error(errorMessage);
2657
- return { error: errorMessage };
2658
- }
2659
2659
 
2660
2660
  // src/ai/flows/generate-questions-from-quiz-plan.ts
2661
2661
  var MAX_ATTEMPTS = 3;
@@ -3528,6 +3528,7 @@ Now, generate the JSON response.`;
3528
3528
  }
3529
3529
 
3530
3530
  exports.assessAndMapDocument = assessAndMapDocument;
3531
+ exports.generateCodingQuestion = generateCodingQuestion;
3531
3532
  exports.generateFillInTheBlanksQuestion = generateFillInTheBlanksQuestion;
3532
3533
  exports.generateLearningAnalysis = generateLearningAnalysis;
3533
3534
  exports.generateMCQQuestion = generateMCQQuestion;