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