@thanh01.pmt/interactive-quiz-kit 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.cjs +521 -520
- package/dist/ai.mjs +521 -521
- package/dist/authoring.cjs +513 -512
- package/dist/authoring.mjs +513 -513
- package/dist/react-ui.cjs +5489 -5721
- package/dist/react-ui.mjs +5490 -5702
- package/package.json +1 -1
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-
|
|
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-
|
|
200
|
-
BaseQuestionGenerationClientInputSchema.extend({
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
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: "
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
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
|
|
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
|
|
257
|
-
2. **
|
|
258
|
-
3. **
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
302
|
+
const aiGeneratedContent = AITrueFalseOutputFieldsSchema.parse(parsedJson);
|
|
313
303
|
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
314
|
-
|
|
315
|
-
|
|
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("
|
|
346
|
-
questionType: "
|
|
315
|
+
id: generateUniqueId("tf_ai_"),
|
|
316
|
+
questionType: "true_false",
|
|
347
317
|
prompt: aiGeneratedContent.prompt,
|
|
348
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
388
|
-
shuffleOptions: z.boolean().optional().default(true)
|
|
355
|
+
numberOfOptions: z.number().int().min(2).max(6).optional().default(4)
|
|
389
356
|
});
|
|
390
|
-
var
|
|
391
|
-
prompt: z.string().describe("The
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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-
|
|
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,
|
|
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
|
|
381
|
+
Previous attempts failed...
|
|
413
382
|
|
|
414
383
|
` : "";
|
|
415
|
-
const imageContextInstruction = imageUrl ? `**Image Context:** You MUST analyze the provided image. The
|
|
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:**
|
|
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:
|
|
425
|
-
|
|
426
|
-
{
|
|
427
|
-
{
|
|
428
|
-
{
|
|
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
|
-
|
|
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:
|
|
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
|
|
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. **
|
|
442
|
-
3. **
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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)
|
|
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 =
|
|
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
|
|
511
|
-
aiGeneratedContent.
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
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("
|
|
520
|
-
questionType: "
|
|
495
|
+
id: generateUniqueId("mcq_ai_"),
|
|
496
|
+
questionType: "multiple_choice",
|
|
521
497
|
prompt: aiGeneratedContent.prompt,
|
|
522
|
-
prompts: finalPrompts,
|
|
523
498
|
options: finalOptions,
|
|
524
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
565
|
-
prompt: z.string()
|
|
566
|
-
options: z.array(
|
|
567
|
-
|
|
568
|
-
|
|
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-
|
|
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:
|
|
572
|
+
prompt: "Which of the following are considered programming paradigms?",
|
|
602
573
|
options: [
|
|
603
|
-
{ tempId: "A", text: "
|
|
604
|
-
{ tempId: "B", text: "
|
|
605
|
-
{ tempId: "C", text: "
|
|
606
|
-
{ tempId: "D", text: "
|
|
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
|
-
|
|
609
|
-
explanation:
|
|
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: "
|
|
612
|
-
topic:
|
|
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
|
|
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}**.
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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("
|
|
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("
|
|
703
|
-
questionType: "
|
|
692
|
+
id: generateUniqueId("mrq_ai_"),
|
|
693
|
+
questionType: "multiple_response",
|
|
704
694
|
prompt: aiGeneratedContent.prompt,
|
|
705
695
|
options: finalOptions,
|
|
706
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
748
|
-
prompt: z.string(),
|
|
749
|
-
|
|
750
|
-
|
|
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()
|
|
743
|
+
verifiedCategory: z.string().optional()
|
|
744
|
+
// Thêm để xác thực
|
|
756
745
|
});
|
|
757
746
|
|
|
758
|
-
// src/ai/flows/question-gen/generate-
|
|
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,
|
|
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
|
|
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
|
|
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}
|
|
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:**
|
|
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: "
|
|
780
|
-
|
|
781
|
-
|
|
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: "
|
|
791
|
-
topic: "
|
|
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
|
|
795
|
-
Your
|
|
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
|
|
799
|
-
2. **
|
|
800
|
-
3. **
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
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("
|
|
900
|
-
questionType: "
|
|
844
|
+
id: generateUniqueId("saq_ai_"),
|
|
845
|
+
questionType: "short_answer",
|
|
901
846
|
prompt: aiGeneratedContent.prompt,
|
|
902
|
-
|
|
903
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
1137
|
-
var
|
|
1138
|
-
function
|
|
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 <=
|
|
1338
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS7; attempt++) {
|
|
1208
1339
|
const startTime = Date.now();
|
|
1209
|
-
const promptText =
|
|
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 <
|
|
1405
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS7;
|
|
1275
1406
|
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
1276
1407
|
if (willRetry) {
|
|
1277
|
-
console.log(`\u23F3 Retrying in ${
|
|
1278
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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 ${
|
|
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
|
-
|
|
1420
|
+
numberOfPairs: z.number().int().min(2).max(8).optional().default(4),
|
|
1421
|
+
shuffleOptions: z.boolean().optional().default(true)
|
|
1290
1422
|
});
|
|
1291
|
-
var
|
|
1292
|
-
prompt: z.string().describe("The
|
|
1293
|
-
|
|
1294
|
-
|
|
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-
|
|
1304
|
-
var
|
|
1305
|
-
var
|
|
1306
|
-
function
|
|
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.
|
|
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
|
|
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:**
|
|
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: "
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
|
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
|
|
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. **
|
|
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
|
|
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
|
|
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.
|
|
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 <=
|
|
1508
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS8; attempt++) {
|
|
1370
1509
|
const startTime = Date.now();
|
|
1371
|
-
const promptText =
|
|
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 =
|
|
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("
|
|
1401
|
-
questionType: "
|
|
1552
|
+
id: generateUniqueId("match_ai_"),
|
|
1553
|
+
questionType: "matching",
|
|
1402
1554
|
prompt: aiGeneratedContent.prompt,
|
|
1403
|
-
|
|
1404
|
-
|
|
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 =
|
|
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
|
|
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 <
|
|
1580
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS8;
|
|
1427
1581
|
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
1428
1582
|
if (willRetry) {
|
|
1429
|
-
console.log(`\u23F3 Retrying in ${
|
|
1430
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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
|
|
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
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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-
|
|
1453
|
-
var
|
|
1454
|
-
var
|
|
1455
|
-
function
|
|
1456
|
-
const { quizContext,
|
|
1457
|
-
const
|
|
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.
|
|
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
|
|
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
|
-
`**
|
|
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
|
-
|
|
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: "
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
|
1483
|
-
|
|
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
|
|
1486
|
-
1. **
|
|
1487
|
-
2. **
|
|
1488
|
-
3. **
|
|
1489
|
-
4. **
|
|
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
|
|
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.
|
|
1672
|
+
temperature: 0.5,
|
|
1514
1673
|
responseMimeType: "application/json",
|
|
1515
1674
|
thinkingConfig: {
|
|
1516
|
-
thinkingBudget:
|
|
1675
|
+
thinkingBudget: 5e3
|
|
1517
1676
|
}
|
|
1518
1677
|
};
|
|
1519
1678
|
const attemptResults = [];
|
|
1520
1679
|
let lastError = null;
|
|
1521
|
-
for (let attempt = 1; attempt <=
|
|
1680
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS9; attempt++) {
|
|
1522
1681
|
const startTime = Date.now();
|
|
1523
|
-
const promptText =
|
|
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 =
|
|
1701
|
+
const aiGeneratedContent = AICodingQuestionOutputSchema.parse(parsedJson);
|
|
1543
1702
|
DebugLogger.logValidation(attempt, "Zod Schema Validated", aiGeneratedContent);
|
|
1544
|
-
if (
|
|
1545
|
-
throw new Error(
|
|
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("
|
|
1556
|
-
questionType: "
|
|
1713
|
+
id: generateUniqueId("coding_"),
|
|
1714
|
+
questionType: "coding",
|
|
1557
1715
|
prompt: aiGeneratedContent.prompt,
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
1730
|
+
CodingQuestionZodSchema.parse(completeQuestion);
|
|
1571
1731
|
attemptResults.push({ success: true, duration, promptLength: promptText.length, responseLength: rawText.length, promptHash });
|
|
1572
1732
|
console.log(`
|
|
1573
|
-
\u2705
|
|
1733
|
+
\u2705 Coding question generation successful on attempt ${attempt} (${duration}ms)`);
|
|
1574
1734
|
if (attempt > 1) DebugLogger.logAttemptSummary(attemptResults);
|
|
1575
|
-
return { question:
|
|
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 <
|
|
1740
|
+
const willRetry = attempt < MAX_RETRY_ATTEMPTS9;
|
|
1581
1741
|
DebugLogger.logRetryInfo(attempt, error, willRetry);
|
|
1582
1742
|
if (willRetry) {
|
|
1583
|
-
console.log(`\u23F3 Retrying in ${
|
|
1584
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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
|
|
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 };
|