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