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