@thanh01.pmt/interactive-quiz-kit 1.0.21 → 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 +3263 -1117
- package/dist/ai.js +3256 -1118
- package/dist/authoring.cjs +6717 -4885
- package/dist/authoring.js +6606 -4789
- package/dist/index.cjs +1898 -1087
- package/dist/index.js +1894 -1088
- package/dist/player.cjs +1609 -623
- package/dist/player.js +1554 -570
- package/dist/react-ui.cjs +13310 -7005
- package/dist/react-ui.js +12762 -6511
- package/package.json +53 -25
package/dist/ai.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var genai = require('@google/genai');
|
|
3
4
|
var zod = require('zod');
|
|
4
5
|
var genkit = require('genkit');
|
|
5
6
|
var googleai = require('@genkit-ai/googleai');
|
|
@@ -23,282 +24,520 @@ var __spreadValues = (a, b) => {
|
|
|
23
24
|
return a;
|
|
24
25
|
};
|
|
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
|
+
};
|
|
26
39
|
|
|
27
40
|
// src/utils/idGenerators.ts
|
|
28
41
|
function generateUniqueId(prefix = "id_") {
|
|
29
42
|
return prefix + Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
30
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
|
+
};
|
|
31
102
|
|
|
32
|
-
// src/
|
|
33
|
-
function
|
|
34
|
-
const
|
|
35
|
-
|
|
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
|
+
};
|
|
36
116
|
}
|
|
37
|
-
zod.z.object({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.")
|
|
46
130
|
});
|
|
47
|
-
zod.z.object({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
).min(1),
|
|
56
|
-
|
|
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(),
|
|
57
141
|
explanation: zod.z.string().optional(),
|
|
58
|
-
points: zod.z.number().optional().default(10),
|
|
59
142
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
60
|
-
topic: zod.z.string().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()
|
|
61
152
|
});
|
|
62
|
-
var
|
|
63
|
-
|
|
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({
|
|
64
184
|
questionType: zod.z.literal("fill_in_the_blanks"),
|
|
65
|
-
prompt: zod.z.string().min(1),
|
|
66
185
|
segments: zod.z.array(zod.z.object({
|
|
67
186
|
type: zod.z.enum(["text", "blank"]),
|
|
68
187
|
content: zod.z.string().optional(),
|
|
69
|
-
// Only for 'text' type
|
|
70
188
|
id: zod.z.string().optional()
|
|
71
|
-
// Only for 'blank' type
|
|
72
189
|
})).min(1),
|
|
73
190
|
answers: zod.z.array(zod.z.object({
|
|
74
191
|
blankId: zod.z.string(),
|
|
75
192
|
acceptedValues: zod.z.array(zod.z.string().min(1)).min(1)
|
|
76
193
|
})).min(1),
|
|
77
|
-
isCaseSensitive: zod.z.boolean().optional()
|
|
78
|
-
points: zod.z.number().min(0).optional(),
|
|
79
|
-
explanation: zod.z.string().optional()
|
|
80
|
-
// ... other fields
|
|
194
|
+
isCaseSensitive: zod.z.boolean().optional()
|
|
81
195
|
}).refine((data) => {
|
|
82
|
-
const segmentBlankIds = new Set(data.segments.filter((s) => s.type === "blank").map((s) => s.id));
|
|
196
|
+
const segmentBlankIds = new Set(data.segments.filter((s) => s.type === "blank" && s.id).map((s) => s.id));
|
|
83
197
|
const answerBlankIds = new Set(data.answers.map((a) => a.blankId));
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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"]
|
|
92
207
|
});
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
95
229
|
});
|
|
96
|
-
async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
|
|
97
|
-
var _a;
|
|
98
|
-
try {
|
|
99
|
-
const ai = genkit.genkit({
|
|
100
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
101
|
-
model: googleai.gemini20Flash
|
|
102
|
-
});
|
|
103
|
-
const promptText = `You are an expert quiz question writer.
|
|
104
|
-
Generate a single Fill-In-The-Blanks question in ${clientInput.language} with approximately ${clientInput.numberOfBlanks} blank(s).
|
|
105
230
|
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
|
|
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
|
+
});
|
|
120
251
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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'.
|
|
127
261
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
133
286
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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'.`);
|
|
160
354
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
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}`);
|
|
166
364
|
}
|
|
167
|
-
lastIndex = placeholderRegex.lastIndex;
|
|
168
|
-
}
|
|
169
|
-
if (lastIndex < aiGeneratedContent.sentenceWithPlaceholders.length) {
|
|
170
|
-
segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex) });
|
|
171
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
|
+
});
|
|
172
377
|
const completeQuestion = {
|
|
173
378
|
id: generateUniqueId("fitb_ai_"),
|
|
174
379
|
questionType: "fill_in_the_blanks",
|
|
175
380
|
prompt: aiGeneratedContent.prompt,
|
|
176
|
-
segments,
|
|
177
|
-
answers,
|
|
178
|
-
isCaseSensitive:
|
|
381
|
+
segments: finalSegments,
|
|
382
|
+
answers: finalAnswers,
|
|
383
|
+
isCaseSensitive: clientInput.isCaseSensitive,
|
|
179
384
|
explanation: aiGeneratedContent.explanation,
|
|
180
385
|
points: aiGeneratedContent.points,
|
|
181
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
182
|
-
difficulty:
|
|
183
|
-
contextCode:
|
|
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
|
|
184
394
|
};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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));
|
|
191
410
|
}
|
|
192
|
-
} else {
|
|
193
|
-
throw new Error("AI did not return content for the Fill-In-The-Blanks question.");
|
|
194
411
|
}
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.error("Error generating Fill-In-The-Blanks question:", error);
|
|
197
|
-
throw new Error(`Failed to generate Fill-In-The-Blanks question: ${error.message}`);
|
|
198
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 };
|
|
199
418
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
zod.z.object({
|
|
205
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
206
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
207
|
-
// <-- ĐÃ THÊM
|
|
208
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
209
|
-
numberOfPairs: zod.z.number().int().min(2).max(8).optional().default(4).describe("Number of pairs to match (2-8)."),
|
|
210
|
-
shuffleOptions: zod.z.boolean().optional().default(true).describe("Whether the options should be shuffled."),
|
|
211
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question."),
|
|
212
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context.")
|
|
419
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
420
|
+
numberOfPairs: zod.z.number().int().min(2).max(8).optional().default(4),
|
|
421
|
+
shuffleOptions: zod.z.boolean().optional().default(true)
|
|
213
422
|
});
|
|
214
|
-
zod.z.object({
|
|
215
|
-
prompt: zod.z.string().describe("The
|
|
216
|
-
correctPairs: zod.z.array(
|
|
217
|
-
zod.z.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
})
|
|
221
|
-
).min(2),
|
|
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),
|
|
222
429
|
explanation: zod.z.string().optional(),
|
|
223
430
|
points: zod.z.number().optional().default(10),
|
|
224
431
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
225
|
-
topic: zod.z.string().optional()
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
id: zod.z.string(),
|
|
229
|
-
questionType: zod.z.literal("matching"),
|
|
230
|
-
prompt: zod.z.string().min(1),
|
|
231
|
-
prompts: zod.z.array(zod.z.object({ id: zod.z.string(), content: zod.z.string().min(1) })).min(2),
|
|
232
|
-
options: zod.z.array(zod.z.object({ id: zod.z.string(), content: zod.z.string().min(1) })).min(2),
|
|
233
|
-
correctAnswerMap: zod.z.array(zod.z.object({ promptId: zod.z.string(), optionId: zod.z.string() })).min(2),
|
|
234
|
-
shuffleOptions: zod.z.boolean().optional(),
|
|
235
|
-
points: zod.z.number().min(0).optional(),
|
|
236
|
-
explanation: zod.z.string().optional()
|
|
237
|
-
// ... other fields
|
|
238
|
-
}).refine((data) => {
|
|
239
|
-
const promptIds = new Set(data.prompts.map((p) => p.id));
|
|
240
|
-
const optionIds = new Set(data.options.map((o) => o.id));
|
|
241
|
-
return data.correctAnswerMap.every(
|
|
242
|
-
(map) => promptIds.has(map.promptId) && optionIds.has(map.optionId)
|
|
243
|
-
);
|
|
244
|
-
}, {
|
|
245
|
-
message: "All IDs in correctAnswerMap must exist in the prompts and options arrays.",
|
|
246
|
-
path: ["correctAnswerMap"]
|
|
247
|
-
}).refine((data) => {
|
|
248
|
-
const mappedPromptIds = new Set(data.correctAnswerMap.map((m) => m.promptId));
|
|
249
|
-
return mappedPromptIds.size === data.prompts.length && data.correctAnswerMap.length === data.prompts.length;
|
|
250
|
-
}, {
|
|
251
|
-
message: "Each prompt must be mapped exactly once in the correctAnswerMap.",
|
|
252
|
-
path: ["correctAnswerMap"]
|
|
253
|
-
});
|
|
254
|
-
zod.z.object({
|
|
255
|
-
question: MatchingQuestionZodSchema.optional().describe("The generated Matching question.")
|
|
432
|
+
topic: zod.z.string().optional(),
|
|
433
|
+
verifiedCategory: zod.z.string().optional()
|
|
434
|
+
// Thêm để xác thực
|
|
256
435
|
});
|
|
257
|
-
async function generateMatchingQuestion(clientInput, apiKey) {
|
|
258
|
-
try {
|
|
259
|
-
const ai = genkit.genkit({
|
|
260
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
261
|
-
model: googleai.gemini20Flash
|
|
262
|
-
});
|
|
263
|
-
const promptText = `You are an expert quiz question writer.
|
|
264
|
-
Generate a single Matching question in ${clientInput.language} with exactly ${clientInput.numberOfPairs} correct pairs.
|
|
265
436
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
"explanation": "These are the capital cities for the respective countries.",
|
|
276
|
-
"points": 10,
|
|
277
|
-
"difficulty": "medium",
|
|
278
|
-
"topic": "World Geography"
|
|
279
|
-
}
|
|
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.
|
|
280
446
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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.
|
|
287
471
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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.
|
|
292
476
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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);
|
|
300
532
|
if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
|
|
301
|
-
throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were
|
|
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
|
+
}
|
|
302
541
|
}
|
|
303
542
|
const finalPrompts = [];
|
|
304
543
|
const finalOptions = [];
|
|
@@ -320,38 +559,43 @@ Return only the JSON response.`;
|
|
|
320
559
|
shuffleOptions: clientInput.shuffleOptions,
|
|
321
560
|
explanation: aiGeneratedContent.explanation,
|
|
322
561
|
points: aiGeneratedContent.points,
|
|
323
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
324
|
-
difficulty:
|
|
325
|
-
contextCode:
|
|
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
|
|
326
570
|
};
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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));
|
|
333
586
|
}
|
|
334
|
-
} else {
|
|
335
|
-
throw new Error("AI did not return content for the Matching question.");
|
|
336
587
|
}
|
|
337
|
-
} catch (error) {
|
|
338
|
-
console.error("Error generating Matching question:", error);
|
|
339
|
-
throw new Error(`Failed to generate Matching question: ${error.message}`);
|
|
340
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 };
|
|
341
594
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
zod.z.object({
|
|
346
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
347
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
348
|
-
// <-- ĐÃ THÊM
|
|
349
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
350
|
-
numberOfOptions: zod.z.number().int().min(2).max(6).optional().default(4).describe("Number of answer options to generate (2-6)."),
|
|
351
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
352
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
595
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
596
|
+
numberOfOptions: zod.z.number().int().min(2).max(6).optional().default(4)
|
|
353
597
|
});
|
|
354
|
-
zod.z.object({
|
|
598
|
+
var AIMCQOutputFieldsSchema = zod.z.object({
|
|
355
599
|
prompt: zod.z.string().describe("The question statement itself."),
|
|
356
600
|
options: zod.z.array(
|
|
357
601
|
zod.z.object({
|
|
@@ -361,99 +605,123 @@ zod.z.object({
|
|
|
361
605
|
).min(2).max(6),
|
|
362
606
|
correctTempOptionId: zod.z.string().describe("The temporary ID of the correct option from the generated options array."),
|
|
363
607
|
explanation: zod.z.string().optional().describe("A brief explanation of why the answer is correct."),
|
|
364
|
-
points: zod.z.number().optional().default(10)
|
|
365
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
|
|
366
|
-
topic: zod.z.string().optional().describe("Refined topic.")
|
|
367
|
-
});
|
|
368
|
-
var MultipleChoiceQuestionZodSchema = zod.z.object({
|
|
369
|
-
id: zod.z.string(),
|
|
370
|
-
questionType: zod.z.literal("multiple_choice"),
|
|
371
|
-
prompt: zod.z.string().min(1),
|
|
372
|
-
options: zod.z.array(zod.z.object({ id: zod.z.string(), text: zod.z.string().min(1) })).min(2).max(10),
|
|
373
|
-
correctAnswerId: zod.z.string(),
|
|
374
|
-
points: zod.z.number().min(0).optional(),
|
|
375
|
-
explanation: zod.z.string().optional(),
|
|
376
|
-
learningObjective: zod.z.string().optional(),
|
|
377
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
378
|
-
bloomLevel: zod.z.string().optional(),
|
|
608
|
+
points: zod.z.number().optional().default(10),
|
|
379
609
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
course: zod.z.string().optional(),
|
|
383
|
-
category: zod.z.string().optional(),
|
|
384
|
-
topic: zod.z.string().optional()
|
|
385
|
-
}).refine((data) => {
|
|
386
|
-
return data.options.some((option) => option.id === data.correctAnswerId);
|
|
387
|
-
}, {
|
|
388
|
-
message: "correctAnswerId must match one of the option IDs",
|
|
389
|
-
path: ["correctAnswerId"]
|
|
390
|
-
}).refine((data) => {
|
|
391
|
-
const optionIds = data.options.map((opt) => opt.id);
|
|
392
|
-
return optionIds.length === new Set(optionIds).size;
|
|
393
|
-
}, {
|
|
394
|
-
message: "All option IDs must be unique",
|
|
395
|
-
path: ["options"]
|
|
396
|
-
});
|
|
397
|
-
zod.z.object({
|
|
398
|
-
question: MultipleChoiceQuestionZodSchema.optional().describe("The generated Multiple Choice question.")
|
|
610
|
+
topic: zod.z.string().optional(),
|
|
611
|
+
verifiedCategory: zod.z.string().optional().describe("The category this question actually addresses.")
|
|
399
612
|
});
|
|
400
|
-
async function generateMCQQuestion(clientInput, apiKey) {
|
|
401
|
-
try {
|
|
402
|
-
const ai = genkit.genkit({
|
|
403
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
404
|
-
model: googleai.gemini20Flash
|
|
405
|
-
});
|
|
406
|
-
const promptText = `You are an expert quiz question writer.
|
|
407
|
-
Generate a single Multiple Choice question in ${clientInput.language} based on the following inputs.
|
|
408
613
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
"correctTempOptionId": "C",
|
|
419
|
-
"explanation": "Brief explanation",
|
|
420
|
-
"points": 10,
|
|
421
|
-
"difficulty": "medium",
|
|
422
|
-
"topic": "refined topic"
|
|
423
|
-
}
|
|
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...
|
|
424
623
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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.
|
|
431
651
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
Number of Options: ${clientInput.numberOfOptions}
|
|
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.
|
|
437
656
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
454
685
|
}
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
}
|
|
457
725
|
const finalOptions = [];
|
|
458
726
|
const tempIdToFinalIdMap = {};
|
|
459
727
|
aiGeneratedContent.options.forEach((aiOption) => {
|
|
@@ -463,7 +731,7 @@ Return only the JSON response.`;
|
|
|
463
731
|
});
|
|
464
732
|
const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
|
|
465
733
|
if (!finalCorrectAnswerId) {
|
|
466
|
-
throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid
|
|
734
|
+
throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid`);
|
|
467
735
|
}
|
|
468
736
|
const completeQuestion = {
|
|
469
737
|
id: generateUniqueId("mcq_ai_"),
|
|
@@ -473,173 +741,196 @@ Return only the JSON response.`;
|
|
|
473
741
|
correctAnswerId: finalCorrectAnswerId,
|
|
474
742
|
explanation: aiGeneratedContent.explanation,
|
|
475
743
|
points: aiGeneratedContent.points,
|
|
476
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
477
|
-
difficulty:
|
|
478
|
-
contextCode:
|
|
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
|
|
479
752
|
};
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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));
|
|
486
768
|
}
|
|
487
|
-
} else {
|
|
488
|
-
throw new Error("AI did not return content for the MCQ question.");
|
|
489
769
|
}
|
|
490
|
-
} catch (error) {
|
|
491
|
-
console.error("Error generating MCQ question:", error);
|
|
492
|
-
throw new Error(`Failed to generate MCQ question: ${error.message}`);
|
|
493
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 };
|
|
494
776
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
zod.z.object({
|
|
500
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
501
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
502
|
-
// <-- ĐÃ THÊM
|
|
503
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
504
|
-
numberOfOptions: zod.z.number().int().min(2).max(8).optional().default(5).describe("Number of answer options to generate (2-8)."),
|
|
505
|
-
minCorrectAnswers: zod.z.number().int().min(1).optional().default(2).describe("Minimum number of correct answers among the options."),
|
|
506
|
-
maxCorrectAnswers: zod.z.number().int().min(1).optional().default(3).describe("Maximum number of correct answers (must be <= numberOfOptions)."),
|
|
507
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
508
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
509
|
-
});
|
|
510
|
-
zod.z.object({
|
|
511
|
-
prompt: zod.z.string().describe("The question statement itself."),
|
|
512
|
-
options: zod.z.array(
|
|
513
|
-
zod.z.object({
|
|
514
|
-
tempId: zod.z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B')."),
|
|
515
|
-
text: zod.z.string().describe("The text content of this answer option.")
|
|
516
|
-
})
|
|
517
|
-
).min(2).max(8),
|
|
518
|
-
correctTempOptionIds: zod.z.array(zod.z.string()).min(1).describe("An array of temporary IDs of the correct options."),
|
|
519
|
-
explanation: zod.z.string().optional().describe("A brief explanation of why the answers are correct."),
|
|
520
|
-
points: zod.z.number().optional().default(10).describe("Points for a correct answer."),
|
|
521
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
|
|
522
|
-
topic: zod.z.string().optional().describe("Refined topic.")
|
|
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)
|
|
523
781
|
});
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
options: zod.z.array(zod.z.object({ id: zod.z.string(), text: zod.z.string().min(1) })).min(2).max(10),
|
|
529
|
-
correctAnswerIds: zod.z.array(zod.z.string()).min(1),
|
|
530
|
-
points: zod.z.number().min(0).optional(),
|
|
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),
|
|
531
786
|
explanation: zod.z.string().optional(),
|
|
532
|
-
|
|
533
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
534
|
-
bloomLevel: zod.z.string().optional(),
|
|
787
|
+
points: zod.z.number().optional().default(10),
|
|
535
788
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
course: zod.z.string().optional(),
|
|
539
|
-
category: zod.z.string().optional(),
|
|
540
|
-
topic: zod.z.string().optional()
|
|
541
|
-
}).refine((data) => {
|
|
542
|
-
const optionIds = new Set(data.options.map((option) => option.id));
|
|
543
|
-
return data.correctAnswerIds.every((correctId) => optionIds.has(correctId));
|
|
544
|
-
}, {
|
|
545
|
-
message: "All correctAnswerIds must match one of the option IDs",
|
|
546
|
-
path: ["correctAnswerIds"]
|
|
547
|
-
}).refine((data) => {
|
|
548
|
-
const optionIds = data.options.map((opt) => opt.id);
|
|
549
|
-
return optionIds.length === new Set(optionIds).size;
|
|
550
|
-
}, {
|
|
551
|
-
message: "All option IDs must be unique",
|
|
552
|
-
path: ["options"]
|
|
553
|
-
});
|
|
554
|
-
zod.z.object({
|
|
555
|
-
question: MultipleResponseQuestionZodSchema.optional().describe("The generated Multiple Response question.")
|
|
789
|
+
topic: zod.z.string().optional(),
|
|
790
|
+
verifiedCategory: zod.z.string().optional().describe("The category this question actually addresses.")
|
|
556
791
|
});
|
|
557
|
-
async function generateMRQQuestion(clientInput, apiKey) {
|
|
558
|
-
try {
|
|
559
|
-
if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
|
|
560
|
-
throw new Error("Minimum correct answers cannot exceed maximum correct answers.");
|
|
561
|
-
}
|
|
562
|
-
if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
|
|
563
|
-
throw new Error("Maximum correct answers must be less than the total number of options.");
|
|
564
|
-
}
|
|
565
|
-
const ai = genkit.genkit({
|
|
566
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
567
|
-
model: googleai.gemini20Flash
|
|
568
|
-
});
|
|
569
|
-
const promptText = `You are an expert quiz question writer specializing in Multiple Response questions.
|
|
570
|
-
Generate a single Multiple Response question in ${clientInput.language} based on the following inputs.
|
|
571
792
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
],
|
|
582
|
-
"correctTempOptionIds": ["A", "C"],
|
|
583
|
-
"explanation": "Brief explanation of all correct answers.",
|
|
584
|
-
"points": 10,
|
|
585
|
-
"difficulty": "medium",
|
|
586
|
-
"topic": "refined topic"
|
|
587
|
-
}
|
|
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.
|
|
588
802
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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.
|
|
604
831
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
620
872
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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();
|
|
625
922
|
aiGeneratedContent.options.forEach((aiOption) => {
|
|
626
923
|
const finalId = generateUniqueId("opt_mr_");
|
|
627
924
|
finalOptions.push({ id: finalId, text: aiOption.text });
|
|
628
925
|
tempIdToFinalIdMap[aiOption.tempId] = finalId;
|
|
926
|
+
allTempIds.add(aiOption.tempId);
|
|
629
927
|
});
|
|
630
928
|
const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') that does not map to any generated option.`);
|
|
929
|
+
if (!allTempIds.has(tempId)) {
|
|
930
|
+
throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') which does not exist in the generated options.`);
|
|
634
931
|
}
|
|
635
|
-
return
|
|
932
|
+
return tempIdToFinalIdMap[tempId];
|
|
636
933
|
});
|
|
637
|
-
if (finalCorrectAnswerIds.length < clientInput.minCorrectAnswers || finalCorrectAnswerIds.length > clientInput.maxCorrectAnswers) {
|
|
638
|
-
throw new Error(`AI generated ${finalCorrectAnswerIds.length} correct answers, which is outside the requested range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
|
|
639
|
-
}
|
|
640
|
-
if (finalOptions.length !== clientInput.numberOfOptions) {
|
|
641
|
-
throw new Error(`AI generated ${finalOptions.length} options, but ${clientInput.numberOfOptions} were requested.`);
|
|
642
|
-
}
|
|
643
934
|
const completeQuestion = {
|
|
644
935
|
id: generateUniqueId("mrq_ai_"),
|
|
645
936
|
questionType: "multiple_response",
|
|
@@ -648,280 +939,348 @@ Return only the JSON response.`;
|
|
|
648
939
|
correctAnswerIds: finalCorrectAnswerIds,
|
|
649
940
|
explanation: aiGeneratedContent.explanation,
|
|
650
941
|
points: aiGeneratedContent.points,
|
|
651
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
942
|
+
topic: aiGeneratedContent.topic || ((_h = clientInput.quizContext) == null ? void 0 : _h.originalTopic),
|
|
652
943
|
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
653
|
-
contextCode:
|
|
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
|
|
654
950
|
};
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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));
|
|
661
966
|
}
|
|
662
|
-
} else {
|
|
663
|
-
throw new Error("AI did not return content for the MRQ question.");
|
|
664
967
|
}
|
|
665
|
-
} catch (error) {
|
|
666
|
-
console.error("Error generating MRQ question:", error);
|
|
667
|
-
throw new Error(`Failed to generate MRQ question: ${error.message}`);
|
|
668
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 };
|
|
669
974
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
677
|
-
// <-- ĐÃ THÊM
|
|
678
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
679
|
-
allowDecimals: zod.z.boolean().optional().default(true).describe("Whether the answer can be a decimal."),
|
|
680
|
-
minRange: zod.z.number().optional().describe("Optional minimum value for the answer."),
|
|
681
|
-
maxRange: zod.z.number().optional().describe("Optional maximum value for the answer."),
|
|
682
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
683
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
684
|
-
});
|
|
685
|
-
zod.z.object({
|
|
686
|
-
prompt: zod.z.string().describe("The question statement that expects a numerical answer."),
|
|
687
|
-
answer: zod.z.number().describe("The precise numerical correct answer."),
|
|
688
|
-
tolerance: zod.z.number().min(0).optional().default(0).describe("The acceptable range of error (plus or minus). Default is 0 for exact match."),
|
|
689
|
-
explanation: zod.z.string().optional().describe("Explanation for the correct answer."),
|
|
690
|
-
points: zod.z.number().optional().default(10),
|
|
691
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
692
|
-
topic: zod.z.string().optional()
|
|
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)
|
|
693
981
|
});
|
|
694
|
-
var
|
|
695
|
-
|
|
696
|
-
questionType: zod.z.literal("numeric"),
|
|
697
|
-
prompt: zod.z.string().min(1),
|
|
982
|
+
var AINumericOutputFieldsSchema = zod.z.object({
|
|
983
|
+
prompt: zod.z.string(),
|
|
698
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
|
|
699
986
|
tolerance: zod.z.number().min(0).optional(),
|
|
700
|
-
points: zod.z.number().min(0).optional(),
|
|
701
987
|
explanation: zod.z.string().optional(),
|
|
702
|
-
|
|
703
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
704
|
-
bloomLevel: zod.z.string().optional(),
|
|
988
|
+
points: zod.z.number().optional().default(10),
|
|
705
989
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
category: zod.z.string().optional(),
|
|
710
|
-
topic: zod.z.string().optional()
|
|
990
|
+
topic: zod.z.string().optional(),
|
|
991
|
+
verifiedCategory: zod.z.string().optional()
|
|
992
|
+
// Thêm để xác thực
|
|
711
993
|
});
|
|
712
|
-
zod.z.object({
|
|
713
|
-
question: NumericQuestionZodSchema.optional().describe("The generated Numeric question.")
|
|
714
|
-
});
|
|
715
|
-
async function generateNumericQuestion(clientInput, apiKey) {
|
|
716
|
-
try {
|
|
717
|
-
if (clientInput.minRange !== void 0 && clientInput.maxRange !== void 0 && clientInput.minRange > clientInput.maxRange) {
|
|
718
|
-
throw new Error("minRange cannot be greater than maxRange.");
|
|
719
|
-
}
|
|
720
|
-
const ai = genkit.genkit({
|
|
721
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
722
|
-
model: googleai.gemini20Flash
|
|
723
|
-
});
|
|
724
|
-
const promptText = `You are an expert quiz question writer.
|
|
725
|
-
Generate a single Numeric question in ${clientInput.language} based on the following inputs. The question must require a numerical answer.
|
|
726
994
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
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.
|
|
737
1004
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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.
|
|
745
1031
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
Allow Decimals: ${clientInput.allowDecimals}
|
|
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.
|
|
751
1036
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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}.`);
|
|
762
1101
|
}
|
|
763
|
-
if (clientInput.
|
|
764
|
-
|
|
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}.`);
|
|
765
1104
|
}
|
|
766
|
-
if (clientInput.
|
|
767
|
-
|
|
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
|
+
}
|
|
768
1114
|
}
|
|
769
1115
|
const completeQuestion = {
|
|
770
1116
|
id: generateUniqueId("num_ai_"),
|
|
771
1117
|
questionType: "numeric",
|
|
772
1118
|
prompt: aiGeneratedContent.prompt,
|
|
773
|
-
answer:
|
|
774
|
-
tolerance: aiGeneratedContent.tolerance,
|
|
1119
|
+
answer: aiGeneratedContent.answer,
|
|
1120
|
+
tolerance: (_i = (_h = clientInput.tolerance) != null ? _h : aiGeneratedContent.tolerance) != null ? _i : 0,
|
|
775
1121
|
explanation: aiGeneratedContent.explanation,
|
|
776
1122
|
points: aiGeneratedContent.points,
|
|
777
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
778
|
-
difficulty:
|
|
779
|
-
contextCode:
|
|
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
|
|
780
1131
|
};
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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));
|
|
787
1147
|
}
|
|
788
|
-
} else {
|
|
789
|
-
throw new Error("AI did not return content for the Numeric question.");
|
|
790
1148
|
}
|
|
791
|
-
} catch (error) {
|
|
792
|
-
console.error("Error generating Numeric question:", error);
|
|
793
|
-
throw new Error(`Failed to generate Numeric question: ${error.message}`);
|
|
794
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 };
|
|
795
1155
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return match ? match[1].trim() : text.trim();
|
|
799
|
-
}
|
|
800
|
-
zod.z.object({
|
|
801
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
802
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
803
|
-
// <-- ĐÃ THÊM
|
|
804
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
805
|
-
numberOfItems: zod.z.number().int().min(2).max(10).optional().default(4).describe("Number of items to sequence (2-10)."),
|
|
806
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
807
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
808
|
-
});
|
|
809
|
-
zod.z.object({
|
|
810
|
-
prompt: zod.z.string().describe("The instruction for sequencing."),
|
|
811
|
-
itemsContent: zod.z.array(zod.z.string().min(1)).min(2).describe("An array of strings for each item to be sequenced."),
|
|
812
|
-
correctOrderContent: zod.z.array(zod.z.string().min(1)).min(2).describe("An array of the same strings from 'itemsContent', but in the correct sequence."),
|
|
813
|
-
explanation: zod.z.string().optional().describe("Explanation for the correct sequence."),
|
|
814
|
-
points: zod.z.number().optional().default(10),
|
|
815
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
816
|
-
topic: zod.z.string().optional()
|
|
1156
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
1157
|
+
numberOfItems: zod.z.number().int().min(2).max(10).optional().default(4)
|
|
817
1158
|
});
|
|
818
|
-
var
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
correctOrder: zod.z.array(zod.z.string()).min(2),
|
|
824
|
-
// Array of item IDs
|
|
825
|
-
points: zod.z.number().min(0).optional(),
|
|
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."),
|
|
826
1164
|
explanation: zod.z.string().optional(),
|
|
827
|
-
|
|
828
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
829
|
-
bloomLevel: zod.z.string().optional(),
|
|
1165
|
+
points: zod.z.number().optional().default(10),
|
|
830
1166
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
category: zod.z.string().optional(),
|
|
835
|
-
topic: zod.z.string().optional()
|
|
836
|
-
}).refine((data) => {
|
|
837
|
-
const itemIds = new Set(data.items.map((item) => item.id));
|
|
838
|
-
return data.correctOrder.every((id) => itemIds.has(id));
|
|
839
|
-
}, {
|
|
840
|
-
message: "Every ID in correctOrder must correspond to an item in the items array.",
|
|
841
|
-
path: ["correctOrder"]
|
|
842
|
-
}).refine((data) => {
|
|
843
|
-
return new Set(data.correctOrder).size === data.items.length && data.correctOrder.length === data.items.length;
|
|
844
|
-
}, {
|
|
845
|
-
message: "The correctOrder array must contain all item IDs exactly once.",
|
|
846
|
-
path: ["correctOrder"]
|
|
847
|
-
}).refine((data) => {
|
|
848
|
-
const itemIds = data.items.map((item) => item.id);
|
|
849
|
-
return itemIds.length === new Set(itemIds).size;
|
|
850
|
-
}, {
|
|
851
|
-
message: "All item IDs must be unique.",
|
|
852
|
-
path: ["items"]
|
|
853
|
-
});
|
|
854
|
-
zod.z.object({
|
|
855
|
-
question: SequenceQuestionZodSchema.optional().describe("The generated Sequence question.")
|
|
1167
|
+
topic: zod.z.string().optional(),
|
|
1168
|
+
verifiedCategory: zod.z.string().optional()
|
|
1169
|
+
// Thêm để xác thực
|
|
856
1170
|
});
|
|
857
|
-
async function generateSequenceQuestion(clientInput, apiKey) {
|
|
858
|
-
try {
|
|
859
|
-
const ai = genkit.genkit({
|
|
860
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
861
|
-
model: googleai.gemini20Flash
|
|
862
|
-
});
|
|
863
|
-
const promptText = `You are an expert quiz question writer specializing in Sequence questions.
|
|
864
|
-
Generate a single Sequence question in ${clientInput.language} based on the following inputs.
|
|
865
1171
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
"correctOrderContent": [
|
|
876
|
-
"Invasion of Poland",
|
|
877
|
-
"Attack on Pearl Harbor",
|
|
878
|
-
"Battle of Stalingrad",
|
|
879
|
-
"D-Day (Normandy Landings)"
|
|
880
|
-
],
|
|
881
|
-
"explanation": "The Invasion of Poland started the war in Europe, followed by the US entry after Pearl Harbor, the turning point at Stalingrad, and finally the D-Day invasion.",
|
|
882
|
-
"points": 10,
|
|
883
|
-
"difficulty": "medium",
|
|
884
|
-
"topic": "World War II History"
|
|
885
|
-
}
|
|
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.
|
|
886
1181
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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.
|
|
893
1207
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
Number of Items: ${clientInput.numberOfItems}
|
|
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.
|
|
899
1212
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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);
|
|
909
1256
|
}
|
|
910
|
-
|
|
911
|
-
|
|
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.`);
|
|
912
1270
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
});
|
|
919
|
-
const finalCorrectOrder = aiGeneratedContent.correctOrderContent.map((content) => {
|
|
920
|
-
const id = contentToIdMap[content];
|
|
921
|
-
if (!id) {
|
|
922
|
-
throw new Error(`Content "${content}" from 'correctOrderContent' was not found in 'itemsContent'.`);
|
|
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}`);
|
|
923
1276
|
}
|
|
924
|
-
|
|
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);
|
|
925
1284
|
});
|
|
926
1285
|
const completeQuestion = {
|
|
927
1286
|
id: generateUniqueId("seq_ai_"),
|
|
@@ -931,212 +1290,307 @@ Return only the JSON response.`;
|
|
|
931
1290
|
correctOrder: finalCorrectOrder,
|
|
932
1291
|
explanation: aiGeneratedContent.explanation,
|
|
933
1292
|
points: aiGeneratedContent.points,
|
|
934
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
935
|
-
difficulty:
|
|
936
|
-
contextCode:
|
|
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
|
|
937
1301
|
};
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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));
|
|
944
1317
|
}
|
|
945
|
-
} else {
|
|
946
|
-
throw new Error("AI did not return content for the Sequence question.");
|
|
947
1318
|
}
|
|
948
|
-
} catch (error) {
|
|
949
|
-
console.error("Error generating Sequence question:", error);
|
|
950
|
-
throw new Error(`Failed to generate Sequence question: ${error.message}`);
|
|
951
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 };
|
|
952
1325
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
return match ? match[1].trim() : text.trim();
|
|
956
|
-
}
|
|
957
|
-
zod.z.object({
|
|
958
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
959
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
960
|
-
// <-- ĐÃ THÊM
|
|
961
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
962
|
-
isCaseSensitive: zod.z.boolean().optional().default(false).describe("Whether the answer should be case-sensitive."),
|
|
963
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
964
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
965
|
-
});
|
|
966
|
-
zod.z.object({
|
|
967
|
-
prompt: zod.z.string().describe("The question statement."),
|
|
968
|
-
acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1).describe("An array of acceptable short answers."),
|
|
969
|
-
isCaseSensitive: zod.z.boolean().optional().describe("Should the answer evaluation be case sensitive?"),
|
|
970
|
-
explanation: zod.z.string().optional().describe("Explanation for the correct answer(s)."),
|
|
971
|
-
points: zod.z.number().optional().default(10),
|
|
972
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
973
|
-
topic: zod.z.string().optional()
|
|
1326
|
+
BaseQuestionGenerationClientInputSchema.extend({
|
|
1327
|
+
isCaseSensitive: zod.z.boolean().optional().default(false)
|
|
974
1328
|
});
|
|
975
|
-
var
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1),
|
|
980
|
-
isCaseSensitive: zod.z.boolean().optional(),
|
|
981
|
-
points: zod.z.number().min(0).optional(),
|
|
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
|
|
982
1333
|
explanation: zod.z.string().optional(),
|
|
983
|
-
|
|
984
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
985
|
-
bloomLevel: zod.z.string().optional(),
|
|
1334
|
+
points: zod.z.number().optional().default(10),
|
|
986
1335
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
category: zod.z.string().optional(),
|
|
991
|
-
topic: zod.z.string().optional()
|
|
992
|
-
});
|
|
993
|
-
zod.z.object({
|
|
994
|
-
question: ShortAnswerQuestionZodSchema.optional().describe("The generated Short Answer question.")
|
|
1336
|
+
topic: zod.z.string().optional(),
|
|
1337
|
+
verifiedCategory: zod.z.string().optional()
|
|
1338
|
+
// Thêm để xác thực
|
|
995
1339
|
});
|
|
996
|
-
async function generateShortAnswerQuestion(clientInput, apiKey) {
|
|
997
|
-
var _a;
|
|
998
|
-
try {
|
|
999
|
-
const ai = genkit.genkit({
|
|
1000
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
1001
|
-
model: googleai.gemini20Flash
|
|
1002
|
-
});
|
|
1003
|
-
const promptText = `You are an expert quiz question writer.
|
|
1004
|
-
Generate a single Short Answer question in ${clientInput.language} based on the following inputs.
|
|
1005
1340
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
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.
|
|
1016
1350
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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.
|
|
1022
1371
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
Case Sensitive: ${clientInput.isCaseSensitive}
|
|
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.
|
|
1028
1376
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
+
}
|
|
1036
1438
|
const completeQuestion = {
|
|
1037
1439
|
id: generateUniqueId("saq_ai_"),
|
|
1038
1440
|
questionType: "short_answer",
|
|
1039
1441
|
prompt: aiGeneratedContent.prompt,
|
|
1040
1442
|
acceptedAnswers: aiGeneratedContent.acceptedAnswers,
|
|
1041
|
-
|
|
1042
|
-
isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
|
|
1443
|
+
isCaseSensitive: clientInput.isCaseSensitive,
|
|
1043
1444
|
explanation: aiGeneratedContent.explanation,
|
|
1044
1445
|
points: aiGeneratedContent.points,
|
|
1045
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
1046
|
-
difficulty:
|
|
1047
|
-
contextCode:
|
|
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
|
|
1048
1454
|
};
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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));
|
|
1055
1470
|
}
|
|
1056
|
-
} else {
|
|
1057
|
-
throw new Error("AI did not return content for the Short Answer question.");
|
|
1058
1471
|
}
|
|
1059
|
-
} catch (error) {
|
|
1060
|
-
console.error("Error generating Short Answer question:", error);
|
|
1061
|
-
throw new Error(`Failed to generate Short Answer question: ${error.message}`);
|
|
1062
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 };
|
|
1063
1478
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
zod.z.object({
|
|
1069
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
1070
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
1071
|
-
// <-- ĐÃ THÊM
|
|
1072
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
1073
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question."),
|
|
1074
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context.")
|
|
1075
|
-
});
|
|
1076
|
-
zod.z.object({
|
|
1077
|
-
prompt: zod.z.string().describe("The statement to be evaluated as true or false."),
|
|
1078
|
-
correctAnswer: zod.z.boolean().describe("The correct answer (true or false)."),
|
|
1079
|
-
explanation: zod.z.string().optional().describe("A brief explanation of why the answer is correct."),
|
|
1080
|
-
points: zod.z.number().optional().default(10),
|
|
1081
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1082
|
-
topic: zod.z.string().optional()
|
|
1083
|
-
});
|
|
1084
|
-
var TrueFalseQuestionZodSchema = zod.z.object({
|
|
1085
|
-
id: zod.z.string(),
|
|
1086
|
-
questionType: zod.z.literal("true_false"),
|
|
1087
|
-
prompt: zod.z.string().min(1),
|
|
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."),
|
|
1088
1482
|
correctAnswer: zod.z.boolean(),
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
learningObjective: zod.z.string().optional(),
|
|
1092
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1093
|
-
bloomLevel: zod.z.string().optional(),
|
|
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),
|
|
1094
1485
|
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
category: zod.z.string().optional(),
|
|
1099
|
-
topic: zod.z.string().optional()
|
|
1486
|
+
topic: zod.z.string().optional(),
|
|
1487
|
+
verifiedCategory: zod.z.string().optional()
|
|
1488
|
+
// Thêm để xác thực
|
|
1100
1489
|
});
|
|
1101
|
-
zod.z.object({
|
|
1102
|
-
question: TrueFalseQuestionZodSchema.optional().describe("The generated True/False question.")
|
|
1103
|
-
});
|
|
1104
|
-
async function generateTrueFalseQuestion(clientInput, apiKey) {
|
|
1105
|
-
try {
|
|
1106
|
-
const ai = genkit.genkit({
|
|
1107
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
1108
|
-
model: googleai.gemini20Flash
|
|
1109
|
-
});
|
|
1110
|
-
const promptText = `You are an expert quiz question writer.
|
|
1111
|
-
Generate a single True/False question in ${clientInput.language} based on the following inputs.
|
|
1112
1490
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
"
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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.
|
|
1122
1500
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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.
|
|
1127
1523
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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.
|
|
1132
1529
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
+
}
|
|
1140
1594
|
const completeQuestion = {
|
|
1141
1595
|
id: generateUniqueId("tf_ai_"),
|
|
1142
1596
|
questionType: "true_false",
|
|
@@ -1144,164 +1598,1126 @@ Return only the JSON response.`;
|
|
|
1144
1598
|
correctAnswer: aiGeneratedContent.correctAnswer,
|
|
1145
1599
|
explanation: aiGeneratedContent.explanation,
|
|
1146
1600
|
points: aiGeneratedContent.points,
|
|
1147
|
-
topic: aiGeneratedContent.topic || clientInput.
|
|
1148
|
-
difficulty:
|
|
1149
|
-
contextCode:
|
|
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
|
|
1150
1609
|
};
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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));
|
|
1157
1625
|
}
|
|
1158
|
-
} else {
|
|
1159
|
-
throw new Error("AI did not return content for the True/False question.");
|
|
1160
1626
|
}
|
|
1161
|
-
} catch (error) {
|
|
1162
|
-
console.error("Error generating True/False question:", error);
|
|
1163
|
-
throw new Error(`Failed to generate True/False question: ${error.message}`);
|
|
1164
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 };
|
|
1165
1633
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
];
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
selectedQuestionTypes: zod.z.array(zod.z.enum(fullQuizSupportedQuestionTypesArray)).min(1)
|
|
1195
|
-
});
|
|
1196
|
-
var PlannedQuestionSchema = zod.z.object({
|
|
1197
|
-
plannedTopic: zod.z.string().min(1).describe("The specific topic for this question."),
|
|
1198
|
-
plannedQuestionType: zod.z.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
|
|
1199
|
-
plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned.")
|
|
1200
|
-
});
|
|
1201
|
-
var GenerateQuizPlanOutputSchema = zod.z.object({
|
|
1202
|
-
quizPlan: zod.z.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz.")
|
|
1203
|
-
});
|
|
1204
|
-
async function generateQuizPlan(clientInput, apiKey) {
|
|
1205
|
-
try {
|
|
1206
|
-
const totalTopicRatio = clientInput.topics.reduce((sum, t) => sum + t.ratio, 0);
|
|
1207
|
-
if (Math.abs(totalTopicRatio - 100) > 1) {
|
|
1208
|
-
throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
|
|
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
|
+
}
|
|
1209
1662
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
+
}
|
|
1213
1683
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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}"`;
|
|
1217
1730
|
});
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
+
});
|
|
1222
1954
|
|
|
1223
|
-
|
|
1224
|
-
{
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
+
`;
|
|
1229
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
|
|
1230
2103
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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):
|
|
1234
2196
|
${topicsDistribution}
|
|
1235
|
-
|
|
2197
|
+
|
|
2198
|
+
**Cognitive Complexity Distribution** (follow precisely):
|
|
1236
2199
|
${bloomDistribution}
|
|
1237
|
-
4. **Allowed Question Types**: For each planned question, 'plannedQuestionType' must be one of these types: ${allowedQuestionTypes}. Use a variety of these types.
|
|
1238
|
-
5. **Topic Specificity**: The 'plannedTopic' for each question should be a specific sub-topic or aspect related to one of the main topics provided, and must be in ${clientInput.language}.
|
|
1239
2200
|
|
|
1240
|
-
|
|
1241
|
-
The final 'quizPlan' array must have exactly ${clientInput.totalQuestions} elements.
|
|
2201
|
+
**Available Question Arsenal**: ${allowedQuestionTypes}
|
|
1242
2202
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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);
|
|
1251
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();
|
|
1252
2294
|
if (aiGeneratedContent.quizPlan.length !== clientInput.totalQuestions) {
|
|
1253
|
-
throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested
|
|
2295
|
+
throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested.`);
|
|
1254
2296
|
}
|
|
2297
|
+
const invalidTypes = [];
|
|
1255
2298
|
aiGeneratedContent.quizPlan.forEach((item, index) => {
|
|
1256
2299
|
if (!clientInput.selectedQuestionTypes.includes(item.plannedQuestionType)) {
|
|
1257
|
-
|
|
2300
|
+
invalidTypes.push(`Question ${index + 1}: '${item.plannedQuestionType}'`);
|
|
1258
2301
|
}
|
|
1259
2302
|
});
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
return validatedPlan;
|
|
1263
|
-
} catch (validationError) {
|
|
1264
|
-
console.error("Quiz plan validation failed:", validationError);
|
|
1265
|
-
throw new Error(`Generated quiz plan failed validation: ${validationError}`);
|
|
2303
|
+
if (invalidTypes.length > 0) {
|
|
2304
|
+
throw new Error(`Invalid question types found: ${invalidTypes.join(", ")}`);
|
|
1266
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;
|
|
1267
2336
|
} catch (error) {
|
|
1268
|
-
|
|
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");
|
|
1269
2344
|
throw new Error(`Failed to generate Quiz Plan: ${error.message}`);
|
|
1270
2345
|
}
|
|
1271
2346
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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"
|
|
1284
2541
|
];
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
+
}
|
|
1294
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;
|
|
1295
2715
|
let bloomScore = 1;
|
|
1296
|
-
if (
|
|
1297
|
-
else if (
|
|
2716
|
+
if (plannedBloomLevel === "understanding") bloomScore = 2;
|
|
2717
|
+
else if (plannedBloomLevel === "applying") bloomScore = 3;
|
|
2718
|
+
else if (["analyzing", "evaluating", "creating"].includes(plannedBloomLevel)) bloomScore = 4;
|
|
1298
2719
|
let questionTypeScore = 1;
|
|
1299
|
-
switch (
|
|
1300
|
-
case "true_false":
|
|
1301
|
-
case "multiple_choice":
|
|
1302
|
-
case "short_answer":
|
|
1303
|
-
questionTypeScore = 1;
|
|
1304
|
-
break;
|
|
2720
|
+
switch (plannedQuestionType) {
|
|
1305
2721
|
case "matching":
|
|
1306
2722
|
case "fill_in_the_blanks":
|
|
1307
2723
|
case "numeric":
|
|
@@ -1310,140 +2726,870 @@ var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomCo
|
|
|
1310
2726
|
case "sequence":
|
|
1311
2727
|
case "multiple_response":
|
|
1312
2728
|
case "drag_and_drop":
|
|
2729
|
+
case "coding":
|
|
1313
2730
|
questionTypeScore = 3;
|
|
1314
2731
|
break;
|
|
1315
|
-
default:
|
|
1316
|
-
questionTypeScore = 1;
|
|
1317
2732
|
}
|
|
1318
2733
|
const totalScore = bloomScore + contextScore + questionTypeScore;
|
|
1319
2734
|
if (totalScore <= 4) return "easy";
|
|
1320
|
-
if (totalScore <=
|
|
2735
|
+
if (totalScore <= 7) return "medium";
|
|
1321
2736
|
return "hard";
|
|
1322
2737
|
};
|
|
1323
|
-
var PlannedQuestionSchema2 = zod.z.object({
|
|
1324
|
-
plannedTopic: zod.z.string().min(1),
|
|
1325
|
-
plannedQuestionType: zod.z.string(),
|
|
1326
|
-
// Giữ dạng string để linh hoạt, sẽ kiểm tra trong logic
|
|
1327
|
-
plannedBloomLevel: zod.z.enum(["remembering", "understanding", "applying"]),
|
|
1328
|
-
plannedContextId: zod.z.string().optional()
|
|
1329
|
-
});
|
|
1330
|
-
zod.z.object({
|
|
1331
|
-
quizPlan: zod.z.array(PlannedQuestionSchema2).min(1),
|
|
1332
|
-
language: zod.z.string().optional().default("English").describe("The language for the generated questions."),
|
|
1333
|
-
// <-- ĐÃ THÊM
|
|
1334
|
-
selectedContextIds: zod.z.array(zod.z.string()).optional(),
|
|
1335
|
-
customContextInput: zod.z.string().optional()
|
|
1336
|
-
});
|
|
1337
|
-
var GenerationErrorSchema = zod.z.object({
|
|
1338
|
-
plannedQuestionIndex: zod.z.number(),
|
|
1339
|
-
plannedTopic: zod.z.string(),
|
|
1340
|
-
plannedQuestionType: zod.z.string(),
|
|
1341
|
-
error: zod.z.string()
|
|
1342
|
-
});
|
|
1343
|
-
zod.z.object({
|
|
1344
|
-
generatedQuestions: zod.z.array(zod.z.any()),
|
|
1345
|
-
// z.any() là thực tế vì union của tất cả các loại câu hỏi rất phức tạp
|
|
1346
|
-
errors: zod.z.array(GenerationErrorSchema).optional()
|
|
1347
|
-
});
|
|
1348
2738
|
async function generateQuestionsFromQuizPlan(clientInput, apiKey) {
|
|
1349
2739
|
var _a, _b;
|
|
1350
|
-
const { quizPlan,
|
|
2740
|
+
const { quizPlan, language, imageContexts } = clientInput;
|
|
1351
2741
|
const generatedQuestions = [];
|
|
1352
2742
|
const errors = [];
|
|
2743
|
+
const allLearningObjectives = TopicDataService.getData();
|
|
1353
2744
|
for (let i = 0; i < quizPlan.length; i++) {
|
|
1354
2745
|
const plannedQ = quizPlan[i];
|
|
1355
|
-
let
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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;
|
|
1409
2825
|
break;
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if (question.points === void 0) question.points = 10;
|
|
1420
|
-
generatedQuestions.push(question);
|
|
1421
|
-
} else if (!generationError) {
|
|
1422
|
-
generationError = `AI did not return a question object for type '${plannedQ.plannedQuestionType}'.`;
|
|
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
|
+
}
|
|
1423
2835
|
}
|
|
1424
|
-
} catch (e) {
|
|
1425
|
-
generationError = e.message || `An unknown error occurred.`;
|
|
1426
2836
|
}
|
|
1427
|
-
if (
|
|
1428
|
-
console.error(`Error generating question at index ${i} (Topic: ${plannedQ.plannedTopic}): ${generationError}`);
|
|
2837
|
+
if (!questionGenerated && lastError) {
|
|
1429
2838
|
errors.push({
|
|
1430
2839
|
plannedQuestionIndex: i,
|
|
1431
2840
|
plannedTopic: plannedQ.plannedTopic,
|
|
1432
2841
|
plannedQuestionType: plannedQ.plannedQuestionType,
|
|
1433
|
-
error:
|
|
2842
|
+
error: lastError.message || "Unknown error after all retries."
|
|
1434
2843
|
});
|
|
1435
2844
|
}
|
|
1436
2845
|
}
|
|
1437
2846
|
return { generatedQuestions, errors: errors.length > 0 ? errors : void 0 };
|
|
1438
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
|
+
}
|
|
1439
3577
|
|
|
3578
|
+
exports.assessAndMapDocument = assessAndMapDocument;
|
|
1440
3579
|
exports.generateFillInTheBlanksQuestion = generateFillInTheBlanksQuestion;
|
|
3580
|
+
exports.generateLearningAnalysis = generateLearningAnalysis;
|
|
1441
3581
|
exports.generateMCQQuestion = generateMCQQuestion;
|
|
1442
3582
|
exports.generateMRQQuestion = generateMRQQuestion;
|
|
1443
3583
|
exports.generateMatchingQuestion = generateMatchingQuestion;
|
|
3584
|
+
exports.generateMotivationalQuote = generateMotivationalQuote;
|
|
1444
3585
|
exports.generateNumericQuestion = generateNumericQuestion;
|
|
3586
|
+
exports.generatePracticeSuggestion = generatePracticeSuggestion;
|
|
1445
3587
|
exports.generateQuestionsFromQuizPlan = generateQuestionsFromQuizPlan;
|
|
3588
|
+
exports.generateQuizFromText = generateQuizFromText;
|
|
1446
3589
|
exports.generateQuizPlan = generateQuizPlan;
|
|
3590
|
+
exports.generateQuizReview = generateQuizReview;
|
|
1447
3591
|
exports.generateSequenceQuestion = generateSequenceQuestion;
|
|
1448
3592
|
exports.generateShortAnswerQuestion = generateShortAnswerQuestion;
|
|
3593
|
+
exports.generateSingleKnowledgeCard = generateSingleKnowledgeCard;
|
|
1449
3594
|
exports.generateTrueFalseQuestion = generateTrueFalseQuestion;
|
|
3595
|
+
exports.planKnowledgeCards = planKnowledgeCards;
|