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