@thanh01.pmt/interactive-quiz-kit 1.0.21 → 1.0.22
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/package.json +1 -1
- package/dist/ai.cjs +0 -1449
- package/dist/ai.js +0 -1438
- package/dist/authoring.cjs +0 -9447
- package/dist/authoring.js +0 -9379
- package/dist/index.cjs +0 -2458
- package/dist/index.js +0 -2440
- package/dist/player.cjs +0 -2942
- package/dist/player.js +0 -2906
- package/dist/react-ui.cjs +0 -9584
- package/dist/react-ui.js +0 -9505
package/dist/ai.cjs
DELETED
|
@@ -1,1449 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var zod = require('zod');
|
|
4
|
-
var genkit = require('genkit');
|
|
5
|
-
var googleai = require('@genkit-ai/googleai');
|
|
6
|
-
|
|
7
|
-
var __defProp = Object.defineProperty;
|
|
8
|
-
var __defProps = Object.defineProperties;
|
|
9
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
10
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
11
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
13
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
14
|
-
var __spreadValues = (a, b) => {
|
|
15
|
-
for (var prop in b || (b = {}))
|
|
16
|
-
if (__hasOwnProp.call(b, prop))
|
|
17
|
-
__defNormalProp(a, prop, b[prop]);
|
|
18
|
-
if (__getOwnPropSymbols)
|
|
19
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
20
|
-
if (__propIsEnum.call(b, prop))
|
|
21
|
-
__defNormalProp(a, prop, b[prop]);
|
|
22
|
-
}
|
|
23
|
-
return a;
|
|
24
|
-
};
|
|
25
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
26
|
-
|
|
27
|
-
// src/utils/idGenerators.ts
|
|
28
|
-
function generateUniqueId(prefix = "id_") {
|
|
29
|
-
return prefix + Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// src/ai/flows/generate-fitb-question.ts
|
|
33
|
-
function extractJsonFromMarkdown(text) {
|
|
34
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
35
|
-
return match ? match[1].trim() : text.trim();
|
|
36
|
-
}
|
|
37
|
-
zod.z.object({
|
|
38
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
39
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
40
|
-
// <-- ĐÃ THÊM
|
|
41
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
42
|
-
numberOfBlanks: zod.z.number().int().min(1).max(5).optional().default(1).describe("Number of blanks to include (1-5)."),
|
|
43
|
-
isCaseSensitive: zod.z.boolean().optional().default(false).describe("Whether answers should be case-sensitive."),
|
|
44
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question."),
|
|
45
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context.")
|
|
46
|
-
});
|
|
47
|
-
zod.z.object({
|
|
48
|
-
prompt: zod.z.string().describe("The overall instruction for the question."),
|
|
49
|
-
sentenceWithPlaceholders: zod.z.string().describe("The sentence containing placeholders like {{placeholder_name}}."),
|
|
50
|
-
blanks: zod.z.array(
|
|
51
|
-
zod.z.object({
|
|
52
|
-
placeholder: zod.z.string().describe("The placeholder name (without curly braces)."),
|
|
53
|
-
acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1).describe("Array of acceptable answers.")
|
|
54
|
-
})
|
|
55
|
-
).min(1),
|
|
56
|
-
isCaseSensitive: zod.z.boolean().optional(),
|
|
57
|
-
explanation: zod.z.string().optional(),
|
|
58
|
-
points: zod.z.number().optional().default(10),
|
|
59
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
60
|
-
topic: zod.z.string().optional()
|
|
61
|
-
});
|
|
62
|
-
var FillInTheBlanksQuestionZodSchema = zod.z.object({
|
|
63
|
-
id: zod.z.string(),
|
|
64
|
-
questionType: zod.z.literal("fill_in_the_blanks"),
|
|
65
|
-
prompt: zod.z.string().min(1),
|
|
66
|
-
segments: zod.z.array(zod.z.object({
|
|
67
|
-
type: zod.z.enum(["text", "blank"]),
|
|
68
|
-
content: zod.z.string().optional(),
|
|
69
|
-
// Only for 'text' type
|
|
70
|
-
id: zod.z.string().optional()
|
|
71
|
-
// Only for 'blank' type
|
|
72
|
-
})).min(1),
|
|
73
|
-
answers: zod.z.array(zod.z.object({
|
|
74
|
-
blankId: zod.z.string(),
|
|
75
|
-
acceptedValues: zod.z.array(zod.z.string().min(1)).min(1)
|
|
76
|
-
})).min(1),
|
|
77
|
-
isCaseSensitive: zod.z.boolean().optional(),
|
|
78
|
-
points: zod.z.number().min(0).optional(),
|
|
79
|
-
explanation: zod.z.string().optional()
|
|
80
|
-
// ... other fields
|
|
81
|
-
}).refine((data) => {
|
|
82
|
-
const segmentBlankIds = new Set(data.segments.filter((s) => s.type === "blank").map((s) => s.id));
|
|
83
|
-
const answerBlankIds = new Set(data.answers.map((a) => a.blankId));
|
|
84
|
-
if (segmentBlankIds.size !== answerBlankIds.size) return false;
|
|
85
|
-
for (const id of segmentBlankIds) {
|
|
86
|
-
if (!answerBlankIds.has(id || "")) return false;
|
|
87
|
-
}
|
|
88
|
-
return true;
|
|
89
|
-
}, {
|
|
90
|
-
message: "There must be a 1-to-1 correspondence between blank segments and answer definitions.",
|
|
91
|
-
path: ["answers"]
|
|
92
|
-
});
|
|
93
|
-
zod.z.object({
|
|
94
|
-
question: FillInTheBlanksQuestionZodSchema.optional().describe("The generated question.")
|
|
95
|
-
});
|
|
96
|
-
async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
|
|
97
|
-
var _a;
|
|
98
|
-
try {
|
|
99
|
-
const ai = genkit.genkit({
|
|
100
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
101
|
-
model: googleai.gemini20Flash
|
|
102
|
-
});
|
|
103
|
-
const promptText = `You are an expert quiz question writer.
|
|
104
|
-
Generate a single Fill-In-The-Blanks question in ${clientInput.language} with approximately ${clientInput.numberOfBlanks} blank(s).
|
|
105
|
-
|
|
106
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
107
|
-
{
|
|
108
|
-
"prompt": "Complete the famous saying.",
|
|
109
|
-
"sentenceWithPlaceholders": "Roses are {{color1}}, violets are {{color2}}.",
|
|
110
|
-
"blanks": [
|
|
111
|
-
{ "placeholder": "color1", "acceptedAnswers": ["red"] },
|
|
112
|
-
{ "placeholder": "color2", "acceptedAnswers": ["blue"] }
|
|
113
|
-
],
|
|
114
|
-
"isCaseSensitive": false,
|
|
115
|
-
"explanation": "This is a classic nursery rhyme.",
|
|
116
|
-
"points": 10,
|
|
117
|
-
"difficulty": "easy",
|
|
118
|
-
"topic": "Nursery Rhymes"
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
Requirements:
|
|
122
|
-
- Use double curly braces for placeholders, like {{placeholder_name}}.
|
|
123
|
-
- Each placeholder name inside the braces must be unique.
|
|
124
|
-
- The 'blanks' array must define the accepted answers for every unique placeholder in the sentence.
|
|
125
|
-
- The 'placeholder' value in the 'blanks' array should NOT include the curly braces.
|
|
126
|
-
- The content of 'prompt', 'sentenceWithPlaceholders', 'blanks.acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
127
|
-
|
|
128
|
-
Topic: ${clientInput.topic}
|
|
129
|
-
Language: ${clientInput.language}
|
|
130
|
-
Difficulty: ${clientInput.difficulty}
|
|
131
|
-
Target Number of Blanks: ${clientInput.numberOfBlanks}
|
|
132
|
-
Case Sensitive: ${clientInput.isCaseSensitive}
|
|
133
|
-
|
|
134
|
-
Return only the JSON response.`;
|
|
135
|
-
const response = await ai.generate(promptText);
|
|
136
|
-
const rawText = response.text;
|
|
137
|
-
const jsonText = extractJsonFromMarkdown(rawText);
|
|
138
|
-
console.log("AI Response:", jsonText);
|
|
139
|
-
const aiGeneratedContent = JSON.parse(jsonText);
|
|
140
|
-
if (aiGeneratedContent) {
|
|
141
|
-
const segments = [];
|
|
142
|
-
const answers = [];
|
|
143
|
-
const placeholderToBlankIdMap = {};
|
|
144
|
-
aiGeneratedContent.blanks.forEach((blankInfo) => {
|
|
145
|
-
const blankId = generateUniqueId("blank_");
|
|
146
|
-
placeholderToBlankIdMap[blankInfo.placeholder] = blankId;
|
|
147
|
-
answers.push({
|
|
148
|
-
blankId,
|
|
149
|
-
acceptedValues: blankInfo.acceptedAnswers
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
const placeholderRegex = /\{\{([^}]+)\}\}/g;
|
|
153
|
-
let lastIndex = 0;
|
|
154
|
-
let match;
|
|
155
|
-
while ((match = placeholderRegex.exec(aiGeneratedContent.sentenceWithPlaceholders)) !== null) {
|
|
156
|
-
const placeholderName = match[1];
|
|
157
|
-
const blankId = placeholderToBlankIdMap[placeholderName];
|
|
158
|
-
if (match.index > lastIndex) {
|
|
159
|
-
segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex, match.index) });
|
|
160
|
-
}
|
|
161
|
-
if (blankId) {
|
|
162
|
-
segments.push({ type: "blank", id: blankId });
|
|
163
|
-
} else {
|
|
164
|
-
console.warn(`Placeholder {{${placeholderName}}} found in sentence but not defined in blanks array. Treating as literal text.`);
|
|
165
|
-
segments.push({ type: "text", content: match[0] });
|
|
166
|
-
}
|
|
167
|
-
lastIndex = placeholderRegex.lastIndex;
|
|
168
|
-
}
|
|
169
|
-
if (lastIndex < aiGeneratedContent.sentenceWithPlaceholders.length) {
|
|
170
|
-
segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex) });
|
|
171
|
-
}
|
|
172
|
-
const completeQuestion = {
|
|
173
|
-
id: generateUniqueId("fitb_ai_"),
|
|
174
|
-
questionType: "fill_in_the_blanks",
|
|
175
|
-
prompt: aiGeneratedContent.prompt,
|
|
176
|
-
segments,
|
|
177
|
-
answers,
|
|
178
|
-
isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
|
|
179
|
-
explanation: aiGeneratedContent.explanation,
|
|
180
|
-
points: aiGeneratedContent.points,
|
|
181
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
182
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
183
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
184
|
-
};
|
|
185
|
-
try {
|
|
186
|
-
const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
|
|
187
|
-
return { question: validatedQuestion };
|
|
188
|
-
} catch (validationError) {
|
|
189
|
-
console.error("Question validation failed:", validationError);
|
|
190
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
throw new Error("AI did not return content for the Fill-In-The-Blanks question.");
|
|
194
|
-
}
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.error("Error generating Fill-In-The-Blanks question:", error);
|
|
197
|
-
throw new Error(`Failed to generate Fill-In-The-Blanks question: ${error.message}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function extractJsonFromMarkdown2(text) {
|
|
201
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
202
|
-
return match ? match[1].trim() : text.trim();
|
|
203
|
-
}
|
|
204
|
-
zod.z.object({
|
|
205
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
206
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
207
|
-
// <-- ĐÃ THÊM
|
|
208
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
209
|
-
numberOfPairs: zod.z.number().int().min(2).max(8).optional().default(4).describe("Number of pairs to match (2-8)."),
|
|
210
|
-
shuffleOptions: zod.z.boolean().optional().default(true).describe("Whether the options should be shuffled."),
|
|
211
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question."),
|
|
212
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context.")
|
|
213
|
-
});
|
|
214
|
-
zod.z.object({
|
|
215
|
-
prompt: zod.z.string().describe("The overall instruction (e.g., 'Match the terms to their definitions.')."),
|
|
216
|
-
correctPairs: zod.z.array(
|
|
217
|
-
zod.z.object({
|
|
218
|
-
promptText: zod.z.string().min(1).describe("The text for a prompt item (e.g., a term)."),
|
|
219
|
-
optionText: zod.z.string().min(1).describe("The text for the corresponding option item (e.g., its definition).")
|
|
220
|
-
})
|
|
221
|
-
).min(2),
|
|
222
|
-
explanation: zod.z.string().optional(),
|
|
223
|
-
points: zod.z.number().optional().default(10),
|
|
224
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
225
|
-
topic: zod.z.string().optional()
|
|
226
|
-
});
|
|
227
|
-
var MatchingQuestionZodSchema = zod.z.object({
|
|
228
|
-
id: zod.z.string(),
|
|
229
|
-
questionType: zod.z.literal("matching"),
|
|
230
|
-
prompt: zod.z.string().min(1),
|
|
231
|
-
prompts: zod.z.array(zod.z.object({ id: zod.z.string(), content: zod.z.string().min(1) })).min(2),
|
|
232
|
-
options: zod.z.array(zod.z.object({ id: zod.z.string(), content: zod.z.string().min(1) })).min(2),
|
|
233
|
-
correctAnswerMap: zod.z.array(zod.z.object({ promptId: zod.z.string(), optionId: zod.z.string() })).min(2),
|
|
234
|
-
shuffleOptions: zod.z.boolean().optional(),
|
|
235
|
-
points: zod.z.number().min(0).optional(),
|
|
236
|
-
explanation: zod.z.string().optional()
|
|
237
|
-
// ... other fields
|
|
238
|
-
}).refine((data) => {
|
|
239
|
-
const promptIds = new Set(data.prompts.map((p) => p.id));
|
|
240
|
-
const optionIds = new Set(data.options.map((o) => o.id));
|
|
241
|
-
return data.correctAnswerMap.every(
|
|
242
|
-
(map) => promptIds.has(map.promptId) && optionIds.has(map.optionId)
|
|
243
|
-
);
|
|
244
|
-
}, {
|
|
245
|
-
message: "All IDs in correctAnswerMap must exist in the prompts and options arrays.",
|
|
246
|
-
path: ["correctAnswerMap"]
|
|
247
|
-
}).refine((data) => {
|
|
248
|
-
const mappedPromptIds = new Set(data.correctAnswerMap.map((m) => m.promptId));
|
|
249
|
-
return mappedPromptIds.size === data.prompts.length && data.correctAnswerMap.length === data.prompts.length;
|
|
250
|
-
}, {
|
|
251
|
-
message: "Each prompt must be mapped exactly once in the correctAnswerMap.",
|
|
252
|
-
path: ["correctAnswerMap"]
|
|
253
|
-
});
|
|
254
|
-
zod.z.object({
|
|
255
|
-
question: MatchingQuestionZodSchema.optional().describe("The generated Matching question.")
|
|
256
|
-
});
|
|
257
|
-
async function generateMatchingQuestion(clientInput, apiKey) {
|
|
258
|
-
try {
|
|
259
|
-
const ai = genkit.genkit({
|
|
260
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
261
|
-
model: googleai.gemini20Flash
|
|
262
|
-
});
|
|
263
|
-
const promptText = `You are an expert quiz question writer.
|
|
264
|
-
Generate a single Matching question in ${clientInput.language} with exactly ${clientInput.numberOfPairs} correct pairs.
|
|
265
|
-
|
|
266
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
267
|
-
{
|
|
268
|
-
"prompt": "Match each country to its capital city.",
|
|
269
|
-
"correctPairs": [
|
|
270
|
-
{ "promptText": "France", "optionText": "Paris" },
|
|
271
|
-
{ "promptText": "Japan", "optionText": "Tokyo" },
|
|
272
|
-
{ "promptText": "Egypt", "optionText": "Cairo" },
|
|
273
|
-
{ "promptText": "Brazil", "optionText": "Bras\xEDlia" }
|
|
274
|
-
],
|
|
275
|
-
"explanation": "These are the capital cities for the respective countries.",
|
|
276
|
-
"points": 10,
|
|
277
|
-
"difficulty": "medium",
|
|
278
|
-
"topic": "World Geography"
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
Requirements:
|
|
282
|
-
- The 'correctPairs' array must contain exactly ${clientInput.numberOfPairs} objects.
|
|
283
|
-
- Each object in 'correctPairs' must have a 'promptText' and its corresponding 'optionText'.
|
|
284
|
-
- Ensure the content of all 'promptText' values are unique.
|
|
285
|
-
- Ensure the content of all 'optionText' values are unique.
|
|
286
|
-
- The content of 'prompt', 'correctPairs.promptText', 'correctPairs.optionText', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
287
|
-
|
|
288
|
-
Topic: ${clientInput.topic}
|
|
289
|
-
Language: ${clientInput.language}
|
|
290
|
-
Difficulty: ${clientInput.difficulty}
|
|
291
|
-
Number of Pairs: ${clientInput.numberOfPairs}
|
|
292
|
-
|
|
293
|
-
Return only the JSON response.`;
|
|
294
|
-
const response = await ai.generate(promptText);
|
|
295
|
-
const rawText = response.text;
|
|
296
|
-
const jsonText = extractJsonFromMarkdown2(rawText);
|
|
297
|
-
console.log("AI Response:", jsonText);
|
|
298
|
-
const aiGeneratedContent = JSON.parse(jsonText);
|
|
299
|
-
if (aiGeneratedContent) {
|
|
300
|
-
if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
|
|
301
|
-
throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were requested.`);
|
|
302
|
-
}
|
|
303
|
-
const finalPrompts = [];
|
|
304
|
-
const finalOptions = [];
|
|
305
|
-
const finalCorrectAnswerMap = [];
|
|
306
|
-
aiGeneratedContent.correctPairs.forEach((pair) => {
|
|
307
|
-
const promptId = generateUniqueId("m_p_");
|
|
308
|
-
const optionId = generateUniqueId("m_o_");
|
|
309
|
-
finalPrompts.push({ id: promptId, content: pair.promptText });
|
|
310
|
-
finalOptions.push({ id: optionId, content: pair.optionText });
|
|
311
|
-
finalCorrectAnswerMap.push({ promptId, optionId });
|
|
312
|
-
});
|
|
313
|
-
const completeQuestion = {
|
|
314
|
-
id: generateUniqueId("match_ai_"),
|
|
315
|
-
questionType: "matching",
|
|
316
|
-
prompt: aiGeneratedContent.prompt,
|
|
317
|
-
prompts: finalPrompts,
|
|
318
|
-
options: finalOptions,
|
|
319
|
-
correctAnswerMap: finalCorrectAnswerMap,
|
|
320
|
-
shuffleOptions: clientInput.shuffleOptions,
|
|
321
|
-
explanation: aiGeneratedContent.explanation,
|
|
322
|
-
points: aiGeneratedContent.points,
|
|
323
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
324
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
325
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
326
|
-
};
|
|
327
|
-
try {
|
|
328
|
-
const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
|
|
329
|
-
return { question: validatedQuestion };
|
|
330
|
-
} catch (validationError) {
|
|
331
|
-
console.error("Question validation failed:", validationError);
|
|
332
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
throw new Error("AI did not return content for the Matching question.");
|
|
336
|
-
}
|
|
337
|
-
} catch (error) {
|
|
338
|
-
console.error("Error generating Matching question:", error);
|
|
339
|
-
throw new Error(`Failed to generate Matching question: ${error.message}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function extractJsonFromMarkdown3(text) {
|
|
343
|
-
return text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
344
|
-
}
|
|
345
|
-
zod.z.object({
|
|
346
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
347
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
348
|
-
// <-- ĐÃ THÊM
|
|
349
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
350
|
-
numberOfOptions: zod.z.number().int().min(2).max(6).optional().default(4).describe("Number of answer options to generate (2-6)."),
|
|
351
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
352
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
353
|
-
});
|
|
354
|
-
zod.z.object({
|
|
355
|
-
prompt: zod.z.string().describe("The question statement itself."),
|
|
356
|
-
options: zod.z.array(
|
|
357
|
-
zod.z.object({
|
|
358
|
-
tempId: zod.z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B', '1', '2')."),
|
|
359
|
-
text: zod.z.string().describe("The text content of this answer option.")
|
|
360
|
-
})
|
|
361
|
-
).min(2).max(6),
|
|
362
|
-
correctTempOptionId: zod.z.string().describe("The temporary ID of the correct option from the generated options array."),
|
|
363
|
-
explanation: zod.z.string().optional().describe("A brief explanation of why the answer is correct."),
|
|
364
|
-
points: zod.z.number().optional().default(10).describe("Points for a correct answer."),
|
|
365
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
|
|
366
|
-
topic: zod.z.string().optional().describe("Refined topic.")
|
|
367
|
-
});
|
|
368
|
-
var MultipleChoiceQuestionZodSchema = zod.z.object({
|
|
369
|
-
id: zod.z.string(),
|
|
370
|
-
questionType: zod.z.literal("multiple_choice"),
|
|
371
|
-
prompt: zod.z.string().min(1),
|
|
372
|
-
options: zod.z.array(zod.z.object({ id: zod.z.string(), text: zod.z.string().min(1) })).min(2).max(10),
|
|
373
|
-
correctAnswerId: zod.z.string(),
|
|
374
|
-
points: zod.z.number().min(0).optional(),
|
|
375
|
-
explanation: zod.z.string().optional(),
|
|
376
|
-
learningObjective: zod.z.string().optional(),
|
|
377
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
378
|
-
bloomLevel: zod.z.string().optional(),
|
|
379
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
380
|
-
contextCode: zod.z.string().optional(),
|
|
381
|
-
gradeBand: zod.z.string().optional(),
|
|
382
|
-
course: zod.z.string().optional(),
|
|
383
|
-
category: zod.z.string().optional(),
|
|
384
|
-
topic: zod.z.string().optional()
|
|
385
|
-
}).refine((data) => {
|
|
386
|
-
return data.options.some((option) => option.id === data.correctAnswerId);
|
|
387
|
-
}, {
|
|
388
|
-
message: "correctAnswerId must match one of the option IDs",
|
|
389
|
-
path: ["correctAnswerId"]
|
|
390
|
-
}).refine((data) => {
|
|
391
|
-
const optionIds = data.options.map((opt) => opt.id);
|
|
392
|
-
return optionIds.length === new Set(optionIds).size;
|
|
393
|
-
}, {
|
|
394
|
-
message: "All option IDs must be unique",
|
|
395
|
-
path: ["options"]
|
|
396
|
-
});
|
|
397
|
-
zod.z.object({
|
|
398
|
-
question: MultipleChoiceQuestionZodSchema.optional().describe("The generated Multiple Choice question.")
|
|
399
|
-
});
|
|
400
|
-
async function generateMCQQuestion(clientInput, apiKey) {
|
|
401
|
-
try {
|
|
402
|
-
const ai = genkit.genkit({
|
|
403
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
404
|
-
model: googleai.gemini20Flash
|
|
405
|
-
});
|
|
406
|
-
const promptText = `You are an expert quiz question writer.
|
|
407
|
-
Generate a single Multiple Choice question in ${clientInput.language} based on the following inputs.
|
|
408
|
-
|
|
409
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
410
|
-
{
|
|
411
|
-
"prompt": "Your question here",
|
|
412
|
-
"options": [
|
|
413
|
-
{ "tempId": "A", "text": "First option text" },
|
|
414
|
-
{ "tempId": "B", "text": "Second option text" },
|
|
415
|
-
{ "tempId": "C", "text": "Third option text" },
|
|
416
|
-
{ "tempId": "D", "text": "Fourth option text" }
|
|
417
|
-
],
|
|
418
|
-
"correctTempOptionId": "C",
|
|
419
|
-
"explanation": "Brief explanation",
|
|
420
|
-
"points": 10,
|
|
421
|
-
"difficulty": "medium",
|
|
422
|
-
"topic": "refined topic"
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
Requirements:
|
|
426
|
-
- Generate exactly ${clientInput.numberOfOptions} options.
|
|
427
|
-
- Use tempId values like "A", "B", "C", "D" or "option_1", "option_2", etc.
|
|
428
|
-
- Make sure correctTempOptionId matches one of the tempId values in options array.
|
|
429
|
-
- Each option must have both "tempId" and "text" fields.
|
|
430
|
-
- The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
431
|
-
|
|
432
|
-
Topic: ${clientInput.topic}
|
|
433
|
-
Language: ${clientInput.language}
|
|
434
|
-
${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
|
|
435
|
-
Difficulty: ${clientInput.difficulty}
|
|
436
|
-
Number of Options: ${clientInput.numberOfOptions}
|
|
437
|
-
|
|
438
|
-
Return only the JSON response.`;
|
|
439
|
-
const response = await ai.generate(promptText);
|
|
440
|
-
const rawText = response.text;
|
|
441
|
-
const jsonText = extractJsonFromMarkdown3(rawText);
|
|
442
|
-
console.log("AI Response:", jsonText);
|
|
443
|
-
let aiGeneratedContent = JSON.parse(jsonText);
|
|
444
|
-
if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
|
|
445
|
-
const normalizedOptions = aiGeneratedContent.options.map((option, index) => {
|
|
446
|
-
if (typeof option === "object" && !option.tempId) {
|
|
447
|
-
const key = Object.keys(option)[0];
|
|
448
|
-
const text = option[key];
|
|
449
|
-
return { tempId: key, text };
|
|
450
|
-
}
|
|
451
|
-
return option;
|
|
452
|
-
});
|
|
453
|
-
aiGeneratedContent.options = normalizedOptions;
|
|
454
|
-
}
|
|
455
|
-
console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
|
|
456
|
-
if (aiGeneratedContent) {
|
|
457
|
-
const finalOptions = [];
|
|
458
|
-
const tempIdToFinalIdMap = {};
|
|
459
|
-
aiGeneratedContent.options.forEach((aiOption) => {
|
|
460
|
-
const finalId = generateUniqueId("opt_");
|
|
461
|
-
finalOptions.push({ id: finalId, text: aiOption.text });
|
|
462
|
-
tempIdToFinalIdMap[aiOption.tempId] = finalId;
|
|
463
|
-
});
|
|
464
|
-
const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
|
|
465
|
-
if (!finalCorrectAnswerId) {
|
|
466
|
-
throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid or does not match any generated option tempId.`);
|
|
467
|
-
}
|
|
468
|
-
const completeQuestion = {
|
|
469
|
-
id: generateUniqueId("mcq_ai_"),
|
|
470
|
-
questionType: "multiple_choice",
|
|
471
|
-
prompt: aiGeneratedContent.prompt,
|
|
472
|
-
options: finalOptions,
|
|
473
|
-
correctAnswerId: finalCorrectAnswerId,
|
|
474
|
-
explanation: aiGeneratedContent.explanation,
|
|
475
|
-
points: aiGeneratedContent.points,
|
|
476
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
477
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
478
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
479
|
-
};
|
|
480
|
-
try {
|
|
481
|
-
const validatedQuestion = MultipleChoiceQuestionZodSchema.parse(completeQuestion);
|
|
482
|
-
return { question: validatedQuestion };
|
|
483
|
-
} catch (validationError) {
|
|
484
|
-
console.error("Question validation failed:", validationError);
|
|
485
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
486
|
-
}
|
|
487
|
-
} else {
|
|
488
|
-
throw new Error("AI did not return content for the MCQ question.");
|
|
489
|
-
}
|
|
490
|
-
} catch (error) {
|
|
491
|
-
console.error("Error generating MCQ question:", error);
|
|
492
|
-
throw new Error(`Failed to generate MCQ question: ${error.message}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
function extractJsonFromMarkdown4(text) {
|
|
496
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
497
|
-
return match ? match[1].trim() : text.trim();
|
|
498
|
-
}
|
|
499
|
-
zod.z.object({
|
|
500
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
501
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
502
|
-
// <-- ĐÃ THÊM
|
|
503
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
504
|
-
numberOfOptions: zod.z.number().int().min(2).max(8).optional().default(5).describe("Number of answer options to generate (2-8)."),
|
|
505
|
-
minCorrectAnswers: zod.z.number().int().min(1).optional().default(2).describe("Minimum number of correct answers among the options."),
|
|
506
|
-
maxCorrectAnswers: zod.z.number().int().min(1).optional().default(3).describe("Maximum number of correct answers (must be <= numberOfOptions)."),
|
|
507
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
508
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
509
|
-
});
|
|
510
|
-
zod.z.object({
|
|
511
|
-
prompt: zod.z.string().describe("The question statement itself."),
|
|
512
|
-
options: zod.z.array(
|
|
513
|
-
zod.z.object({
|
|
514
|
-
tempId: zod.z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B')."),
|
|
515
|
-
text: zod.z.string().describe("The text content of this answer option.")
|
|
516
|
-
})
|
|
517
|
-
).min(2).max(8),
|
|
518
|
-
correctTempOptionIds: zod.z.array(zod.z.string()).min(1).describe("An array of temporary IDs of the correct options."),
|
|
519
|
-
explanation: zod.z.string().optional().describe("A brief explanation of why the answers are correct."),
|
|
520
|
-
points: zod.z.number().optional().default(10).describe("Points for a correct answer."),
|
|
521
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
|
|
522
|
-
topic: zod.z.string().optional().describe("Refined topic.")
|
|
523
|
-
});
|
|
524
|
-
var MultipleResponseQuestionZodSchema = zod.z.object({
|
|
525
|
-
id: zod.z.string(),
|
|
526
|
-
questionType: zod.z.literal("multiple_response"),
|
|
527
|
-
prompt: zod.z.string().min(1),
|
|
528
|
-
options: zod.z.array(zod.z.object({ id: zod.z.string(), text: zod.z.string().min(1) })).min(2).max(10),
|
|
529
|
-
correctAnswerIds: zod.z.array(zod.z.string()).min(1),
|
|
530
|
-
points: zod.z.number().min(0).optional(),
|
|
531
|
-
explanation: zod.z.string().optional(),
|
|
532
|
-
learningObjective: zod.z.string().optional(),
|
|
533
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
534
|
-
bloomLevel: zod.z.string().optional(),
|
|
535
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
536
|
-
contextCode: zod.z.string().optional(),
|
|
537
|
-
gradeBand: zod.z.string().optional(),
|
|
538
|
-
course: zod.z.string().optional(),
|
|
539
|
-
category: zod.z.string().optional(),
|
|
540
|
-
topic: zod.z.string().optional()
|
|
541
|
-
}).refine((data) => {
|
|
542
|
-
const optionIds = new Set(data.options.map((option) => option.id));
|
|
543
|
-
return data.correctAnswerIds.every((correctId) => optionIds.has(correctId));
|
|
544
|
-
}, {
|
|
545
|
-
message: "All correctAnswerIds must match one of the option IDs",
|
|
546
|
-
path: ["correctAnswerIds"]
|
|
547
|
-
}).refine((data) => {
|
|
548
|
-
const optionIds = data.options.map((opt) => opt.id);
|
|
549
|
-
return optionIds.length === new Set(optionIds).size;
|
|
550
|
-
}, {
|
|
551
|
-
message: "All option IDs must be unique",
|
|
552
|
-
path: ["options"]
|
|
553
|
-
});
|
|
554
|
-
zod.z.object({
|
|
555
|
-
question: MultipleResponseQuestionZodSchema.optional().describe("The generated Multiple Response question.")
|
|
556
|
-
});
|
|
557
|
-
async function generateMRQQuestion(clientInput, apiKey) {
|
|
558
|
-
try {
|
|
559
|
-
if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
|
|
560
|
-
throw new Error("Minimum correct answers cannot exceed maximum correct answers.");
|
|
561
|
-
}
|
|
562
|
-
if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
|
|
563
|
-
throw new Error("Maximum correct answers must be less than the total number of options.");
|
|
564
|
-
}
|
|
565
|
-
const ai = genkit.genkit({
|
|
566
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
567
|
-
model: googleai.gemini20Flash
|
|
568
|
-
});
|
|
569
|
-
const promptText = `You are an expert quiz question writer specializing in Multiple Response questions.
|
|
570
|
-
Generate a single Multiple Response question in ${clientInput.language} based on the following inputs.
|
|
571
|
-
|
|
572
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
573
|
-
{
|
|
574
|
-
"prompt": "Your question here (e.g., 'Which of the following are primary colors?')",
|
|
575
|
-
"options": [
|
|
576
|
-
{ "tempId": "A", "text": "First option" },
|
|
577
|
-
{ "tempId": "B", "text": "Second option" },
|
|
578
|
-
{ "tempId": "C", "text": "Third option" },
|
|
579
|
-
{ "tempId": "D", "text": "Fourth option" },
|
|
580
|
-
{ "tempId": "E", "text": "Fifth option" }
|
|
581
|
-
],
|
|
582
|
-
"correctTempOptionIds": ["A", "C"],
|
|
583
|
-
"explanation": "Brief explanation of all correct answers.",
|
|
584
|
-
"points": 10,
|
|
585
|
-
"difficulty": "medium",
|
|
586
|
-
"topic": "refined topic"
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
Requirements:
|
|
590
|
-
- Generate exactly ${clientInput.numberOfOptions} options.
|
|
591
|
-
- Generate between ${clientInput.minCorrectAnswers} and ${clientInput.maxCorrectAnswers} correct answers.
|
|
592
|
-
- Use tempId values like "A", "B", "C", etc.
|
|
593
|
-
- Make sure all IDs in correctTempOptionIds match a tempId in the options array.
|
|
594
|
-
- Each option must have both "tempId" and "text" fields.
|
|
595
|
-
- The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
596
|
-
|
|
597
|
-
Topic: ${clientInput.topic}
|
|
598
|
-
Language: ${clientInput.language}
|
|
599
|
-
${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
|
|
600
|
-
Difficulty: ${clientInput.difficulty}
|
|
601
|
-
Number of Options: ${clientInput.numberOfOptions}
|
|
602
|
-
Min Correct Answers: ${clientInput.minCorrectAnswers}
|
|
603
|
-
Max Correct Answers: ${clientInput.maxCorrectAnswers}
|
|
604
|
-
|
|
605
|
-
Return only the JSON response.`;
|
|
606
|
-
const response = await ai.generate(promptText);
|
|
607
|
-
const rawText = response.text;
|
|
608
|
-
const jsonText = extractJsonFromMarkdown4(rawText);
|
|
609
|
-
console.log("AI Response:", jsonText);
|
|
610
|
-
let aiGeneratedContent = JSON.parse(jsonText);
|
|
611
|
-
if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
|
|
612
|
-
const normalizedOptions = aiGeneratedContent.options.map((option) => {
|
|
613
|
-
if (typeof option === "object" && !option.tempId && Object.keys(option).length === 1) {
|
|
614
|
-
const key = Object.keys(option)[0];
|
|
615
|
-
return { tempId: key, text: option[key] };
|
|
616
|
-
}
|
|
617
|
-
return option;
|
|
618
|
-
});
|
|
619
|
-
aiGeneratedContent.options = normalizedOptions;
|
|
620
|
-
}
|
|
621
|
-
console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
|
|
622
|
-
if (aiGeneratedContent) {
|
|
623
|
-
const finalOptions = [];
|
|
624
|
-
const tempIdToFinalIdMap = {};
|
|
625
|
-
aiGeneratedContent.options.forEach((aiOption) => {
|
|
626
|
-
const finalId = generateUniqueId("opt_mr_");
|
|
627
|
-
finalOptions.push({ id: finalId, text: aiOption.text });
|
|
628
|
-
tempIdToFinalIdMap[aiOption.tempId] = finalId;
|
|
629
|
-
});
|
|
630
|
-
const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
|
|
631
|
-
const finalId = tempIdToFinalIdMap[tempId];
|
|
632
|
-
if (!finalId) {
|
|
633
|
-
throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') that does not map to any generated option.`);
|
|
634
|
-
}
|
|
635
|
-
return finalId;
|
|
636
|
-
});
|
|
637
|
-
if (finalCorrectAnswerIds.length < clientInput.minCorrectAnswers || finalCorrectAnswerIds.length > clientInput.maxCorrectAnswers) {
|
|
638
|
-
throw new Error(`AI generated ${finalCorrectAnswerIds.length} correct answers, which is outside the requested range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
|
|
639
|
-
}
|
|
640
|
-
if (finalOptions.length !== clientInput.numberOfOptions) {
|
|
641
|
-
throw new Error(`AI generated ${finalOptions.length} options, but ${clientInput.numberOfOptions} were requested.`);
|
|
642
|
-
}
|
|
643
|
-
const completeQuestion = {
|
|
644
|
-
id: generateUniqueId("mrq_ai_"),
|
|
645
|
-
questionType: "multiple_response",
|
|
646
|
-
prompt: aiGeneratedContent.prompt,
|
|
647
|
-
options: finalOptions,
|
|
648
|
-
correctAnswerIds: finalCorrectAnswerIds,
|
|
649
|
-
explanation: aiGeneratedContent.explanation,
|
|
650
|
-
points: aiGeneratedContent.points,
|
|
651
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
652
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
653
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
654
|
-
};
|
|
655
|
-
try {
|
|
656
|
-
const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
|
|
657
|
-
return { question: validatedQuestion };
|
|
658
|
-
} catch (validationError) {
|
|
659
|
-
console.error("Question validation failed:", validationError);
|
|
660
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
661
|
-
}
|
|
662
|
-
} else {
|
|
663
|
-
throw new Error("AI did not return content for the MRQ question.");
|
|
664
|
-
}
|
|
665
|
-
} catch (error) {
|
|
666
|
-
console.error("Error generating MRQ question:", error);
|
|
667
|
-
throw new Error(`Failed to generate MRQ question: ${error.message}`);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
function extractJsonFromMarkdown5(text) {
|
|
671
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
672
|
-
return match ? match[1].trim() : text.trim();
|
|
673
|
-
}
|
|
674
|
-
zod.z.object({
|
|
675
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
676
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
677
|
-
// <-- ĐÃ THÊM
|
|
678
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
679
|
-
allowDecimals: zod.z.boolean().optional().default(true).describe("Whether the answer can be a decimal."),
|
|
680
|
-
minRange: zod.z.number().optional().describe("Optional minimum value for the answer."),
|
|
681
|
-
maxRange: zod.z.number().optional().describe("Optional maximum value for the answer."),
|
|
682
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
683
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
684
|
-
});
|
|
685
|
-
zod.z.object({
|
|
686
|
-
prompt: zod.z.string().describe("The question statement that expects a numerical answer."),
|
|
687
|
-
answer: zod.z.number().describe("The precise numerical correct answer."),
|
|
688
|
-
tolerance: zod.z.number().min(0).optional().default(0).describe("The acceptable range of error (plus or minus). Default is 0 for exact match."),
|
|
689
|
-
explanation: zod.z.string().optional().describe("Explanation for the correct answer."),
|
|
690
|
-
points: zod.z.number().optional().default(10),
|
|
691
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
692
|
-
topic: zod.z.string().optional()
|
|
693
|
-
});
|
|
694
|
-
var NumericQuestionZodSchema = zod.z.object({
|
|
695
|
-
id: zod.z.string(),
|
|
696
|
-
questionType: zod.z.literal("numeric"),
|
|
697
|
-
prompt: zod.z.string().min(1),
|
|
698
|
-
answer: zod.z.number(),
|
|
699
|
-
tolerance: zod.z.number().min(0).optional(),
|
|
700
|
-
points: zod.z.number().min(0).optional(),
|
|
701
|
-
explanation: zod.z.string().optional(),
|
|
702
|
-
learningObjective: zod.z.string().optional(),
|
|
703
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
704
|
-
bloomLevel: zod.z.string().optional(),
|
|
705
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
706
|
-
contextCode: zod.z.string().optional(),
|
|
707
|
-
gradeBand: zod.z.string().optional(),
|
|
708
|
-
course: zod.z.string().optional(),
|
|
709
|
-
category: zod.z.string().optional(),
|
|
710
|
-
topic: zod.z.string().optional()
|
|
711
|
-
});
|
|
712
|
-
zod.z.object({
|
|
713
|
-
question: NumericQuestionZodSchema.optional().describe("The generated Numeric question.")
|
|
714
|
-
});
|
|
715
|
-
async function generateNumericQuestion(clientInput, apiKey) {
|
|
716
|
-
try {
|
|
717
|
-
if (clientInput.minRange !== void 0 && clientInput.maxRange !== void 0 && clientInput.minRange > clientInput.maxRange) {
|
|
718
|
-
throw new Error("minRange cannot be greater than maxRange.");
|
|
719
|
-
}
|
|
720
|
-
const ai = genkit.genkit({
|
|
721
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
722
|
-
model: googleai.gemini20Flash
|
|
723
|
-
});
|
|
724
|
-
const promptText = `You are an expert quiz question writer.
|
|
725
|
-
Generate a single Numeric question in ${clientInput.language} based on the following inputs. The question must require a numerical answer.
|
|
726
|
-
|
|
727
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
728
|
-
{
|
|
729
|
-
"prompt": "Your question here (e.g., 'What is the value of Pi rounded to two decimal places?')",
|
|
730
|
-
"answer": 3.14,
|
|
731
|
-
"tolerance": 0.01,
|
|
732
|
-
"explanation": "Pi is an irrational number, approximately 3.14159. Rounding to two decimal places gives 3.14.",
|
|
733
|
-
"points": 10,
|
|
734
|
-
"difficulty": "easy",
|
|
735
|
-
"topic": "Mathematics"
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
Requirements:
|
|
739
|
-
- The 'answer' must be a number.
|
|
740
|
-
- If 'allowDecimals' is false, the 'answer' must be an integer.
|
|
741
|
-
- 'tolerance' is the acceptable error range (+/-). A tolerance of 0 means the answer must be exact.
|
|
742
|
-
- The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
743
|
-
${clientInput.minRange !== void 0 ? `- The answer should ideally be >= ${clientInput.minRange}.` : ""}
|
|
744
|
-
${clientInput.maxRange !== void 0 ? `- The answer should ideally be <= ${clientInput.maxRange}.` : ""}
|
|
745
|
-
|
|
746
|
-
Topic: ${clientInput.topic}
|
|
747
|
-
Language: ${clientInput.language}
|
|
748
|
-
${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
|
|
749
|
-
Difficulty: ${clientInput.difficulty}
|
|
750
|
-
Allow Decimals: ${clientInput.allowDecimals}
|
|
751
|
-
|
|
752
|
-
Return only the JSON response.`;
|
|
753
|
-
const response = await ai.generate(promptText);
|
|
754
|
-
const rawText = response.text;
|
|
755
|
-
const jsonText = extractJsonFromMarkdown5(rawText);
|
|
756
|
-
console.log("AI Response:", jsonText);
|
|
757
|
-
const aiGeneratedContent = JSON.parse(jsonText);
|
|
758
|
-
if (aiGeneratedContent) {
|
|
759
|
-
let finalAnswer = aiGeneratedContent.answer;
|
|
760
|
-
if (!clientInput.allowDecimals) {
|
|
761
|
-
finalAnswer = Math.round(finalAnswer);
|
|
762
|
-
}
|
|
763
|
-
if (clientInput.minRange !== void 0 && finalAnswer < clientInput.minRange) {
|
|
764
|
-
console.warn(`AI generated answer ${finalAnswer} is below the requested minRange of ${clientInput.minRange}.`);
|
|
765
|
-
}
|
|
766
|
-
if (clientInput.maxRange !== void 0 && finalAnswer > clientInput.maxRange) {
|
|
767
|
-
console.warn(`AI generated answer ${finalAnswer} is above the requested maxRange of ${clientInput.maxRange}.`);
|
|
768
|
-
}
|
|
769
|
-
const completeQuestion = {
|
|
770
|
-
id: generateUniqueId("num_ai_"),
|
|
771
|
-
questionType: "numeric",
|
|
772
|
-
prompt: aiGeneratedContent.prompt,
|
|
773
|
-
answer: finalAnswer,
|
|
774
|
-
tolerance: aiGeneratedContent.tolerance,
|
|
775
|
-
explanation: aiGeneratedContent.explanation,
|
|
776
|
-
points: aiGeneratedContent.points,
|
|
777
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
778
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
779
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
780
|
-
};
|
|
781
|
-
try {
|
|
782
|
-
const validatedQuestion = NumericQuestionZodSchema.parse(completeQuestion);
|
|
783
|
-
return { question: validatedQuestion };
|
|
784
|
-
} catch (validationError) {
|
|
785
|
-
console.error("Question validation failed:", validationError);
|
|
786
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
787
|
-
}
|
|
788
|
-
} else {
|
|
789
|
-
throw new Error("AI did not return content for the Numeric question.");
|
|
790
|
-
}
|
|
791
|
-
} catch (error) {
|
|
792
|
-
console.error("Error generating Numeric question:", error);
|
|
793
|
-
throw new Error(`Failed to generate Numeric question: ${error.message}`);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
function extractJsonFromMarkdown6(text) {
|
|
797
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
798
|
-
return match ? match[1].trim() : text.trim();
|
|
799
|
-
}
|
|
800
|
-
zod.z.object({
|
|
801
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
802
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
803
|
-
// <-- ĐÃ THÊM
|
|
804
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
805
|
-
numberOfItems: zod.z.number().int().min(2).max(10).optional().default(4).describe("Number of items to sequence (2-10)."),
|
|
806
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
807
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
808
|
-
});
|
|
809
|
-
zod.z.object({
|
|
810
|
-
prompt: zod.z.string().describe("The instruction for sequencing."),
|
|
811
|
-
itemsContent: zod.z.array(zod.z.string().min(1)).min(2).describe("An array of strings for each item to be sequenced."),
|
|
812
|
-
correctOrderContent: zod.z.array(zod.z.string().min(1)).min(2).describe("An array of the same strings from 'itemsContent', but in the correct sequence."),
|
|
813
|
-
explanation: zod.z.string().optional().describe("Explanation for the correct sequence."),
|
|
814
|
-
points: zod.z.number().optional().default(10),
|
|
815
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
816
|
-
topic: zod.z.string().optional()
|
|
817
|
-
});
|
|
818
|
-
var SequenceQuestionZodSchema = zod.z.object({
|
|
819
|
-
id: zod.z.string(),
|
|
820
|
-
questionType: zod.z.literal("sequence"),
|
|
821
|
-
prompt: zod.z.string().min(1),
|
|
822
|
-
items: zod.z.array(zod.z.object({ id: zod.z.string(), content: zod.z.string().min(1) })).min(2),
|
|
823
|
-
correctOrder: zod.z.array(zod.z.string()).min(2),
|
|
824
|
-
// Array of item IDs
|
|
825
|
-
points: zod.z.number().min(0).optional(),
|
|
826
|
-
explanation: zod.z.string().optional(),
|
|
827
|
-
learningObjective: zod.z.string().optional(),
|
|
828
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
829
|
-
bloomLevel: zod.z.string().optional(),
|
|
830
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
831
|
-
contextCode: zod.z.string().optional(),
|
|
832
|
-
gradeBand: zod.z.string().optional(),
|
|
833
|
-
course: zod.z.string().optional(),
|
|
834
|
-
category: zod.z.string().optional(),
|
|
835
|
-
topic: zod.z.string().optional()
|
|
836
|
-
}).refine((data) => {
|
|
837
|
-
const itemIds = new Set(data.items.map((item) => item.id));
|
|
838
|
-
return data.correctOrder.every((id) => itemIds.has(id));
|
|
839
|
-
}, {
|
|
840
|
-
message: "Every ID in correctOrder must correspond to an item in the items array.",
|
|
841
|
-
path: ["correctOrder"]
|
|
842
|
-
}).refine((data) => {
|
|
843
|
-
return new Set(data.correctOrder).size === data.items.length && data.correctOrder.length === data.items.length;
|
|
844
|
-
}, {
|
|
845
|
-
message: "The correctOrder array must contain all item IDs exactly once.",
|
|
846
|
-
path: ["correctOrder"]
|
|
847
|
-
}).refine((data) => {
|
|
848
|
-
const itemIds = data.items.map((item) => item.id);
|
|
849
|
-
return itemIds.length === new Set(itemIds).size;
|
|
850
|
-
}, {
|
|
851
|
-
message: "All item IDs must be unique.",
|
|
852
|
-
path: ["items"]
|
|
853
|
-
});
|
|
854
|
-
zod.z.object({
|
|
855
|
-
question: SequenceQuestionZodSchema.optional().describe("The generated Sequence question.")
|
|
856
|
-
});
|
|
857
|
-
async function generateSequenceQuestion(clientInput, apiKey) {
|
|
858
|
-
try {
|
|
859
|
-
const ai = genkit.genkit({
|
|
860
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
861
|
-
model: googleai.gemini20Flash
|
|
862
|
-
});
|
|
863
|
-
const promptText = `You are an expert quiz question writer specializing in Sequence questions.
|
|
864
|
-
Generate a single Sequence question in ${clientInput.language} based on the following inputs.
|
|
865
|
-
|
|
866
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
867
|
-
{
|
|
868
|
-
"prompt": "Arrange the following events of World War II in chronological order.",
|
|
869
|
-
"itemsContent": [
|
|
870
|
-
"D-Day (Normandy Landings)",
|
|
871
|
-
"Invasion of Poland",
|
|
872
|
-
"Attack on Pearl Harbor",
|
|
873
|
-
"Battle of Stalingrad"
|
|
874
|
-
],
|
|
875
|
-
"correctOrderContent": [
|
|
876
|
-
"Invasion of Poland",
|
|
877
|
-
"Attack on Pearl Harbor",
|
|
878
|
-
"Battle of Stalingrad",
|
|
879
|
-
"D-Day (Normandy Landings)"
|
|
880
|
-
],
|
|
881
|
-
"explanation": "The Invasion of Poland started the war in Europe, followed by the US entry after Pearl Harbor, the turning point at Stalingrad, and finally the D-Day invasion.",
|
|
882
|
-
"points": 10,
|
|
883
|
-
"difficulty": "medium",
|
|
884
|
-
"topic": "World War II History"
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
Requirements:
|
|
888
|
-
- Generate exactly ${clientInput.numberOfItems} items to be sequenced.
|
|
889
|
-
- The 'itemsContent' array should contain the text for each item.
|
|
890
|
-
- The 'correctOrderContent' array must contain the exact same strings as 'itemsContent', but arranged in the correct sequence.
|
|
891
|
-
- Both arrays must have the same number of elements.
|
|
892
|
-
- The content of 'prompt', 'itemsContent', 'correctOrderContent', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
893
|
-
|
|
894
|
-
Topic: ${clientInput.topic}
|
|
895
|
-
Language: ${clientInput.language}
|
|
896
|
-
${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
|
|
897
|
-
Difficulty: ${clientInput.difficulty}
|
|
898
|
-
Number of Items: ${clientInput.numberOfItems}
|
|
899
|
-
|
|
900
|
-
Return only the JSON response.`;
|
|
901
|
-
const response = await ai.generate(promptText);
|
|
902
|
-
const rawText = response.text;
|
|
903
|
-
const jsonText = extractJsonFromMarkdown6(rawText);
|
|
904
|
-
console.log("AI Response:", jsonText);
|
|
905
|
-
const aiGeneratedContent = JSON.parse(jsonText);
|
|
906
|
-
if (aiGeneratedContent) {
|
|
907
|
-
if (aiGeneratedContent.itemsContent.length !== clientInput.numberOfItems || aiGeneratedContent.correctOrderContent.length !== clientInput.numberOfItems) {
|
|
908
|
-
throw new Error(`AI generated an incorrect number of items. Requested: ${clientInput.numberOfItems}, Got: ${aiGeneratedContent.itemsContent.length} items and ${aiGeneratedContent.correctOrderContent.length} in correct order.`);
|
|
909
|
-
}
|
|
910
|
-
if (new Set(aiGeneratedContent.itemsContent).size !== new Set(aiGeneratedContent.correctOrderContent).size) {
|
|
911
|
-
throw new Error("The set of items in 'itemsContent' and 'correctOrderContent' do not match.");
|
|
912
|
-
}
|
|
913
|
-
const contentToIdMap = {};
|
|
914
|
-
const finalItems = aiGeneratedContent.itemsContent.map((content) => {
|
|
915
|
-
const id = generateUniqueId("seqi_");
|
|
916
|
-
contentToIdMap[content] = id;
|
|
917
|
-
return { id, content };
|
|
918
|
-
});
|
|
919
|
-
const finalCorrectOrder = aiGeneratedContent.correctOrderContent.map((content) => {
|
|
920
|
-
const id = contentToIdMap[content];
|
|
921
|
-
if (!id) {
|
|
922
|
-
throw new Error(`Content "${content}" from 'correctOrderContent' was not found in 'itemsContent'.`);
|
|
923
|
-
}
|
|
924
|
-
return id;
|
|
925
|
-
});
|
|
926
|
-
const completeQuestion = {
|
|
927
|
-
id: generateUniqueId("seq_ai_"),
|
|
928
|
-
questionType: "sequence",
|
|
929
|
-
prompt: aiGeneratedContent.prompt,
|
|
930
|
-
items: finalItems,
|
|
931
|
-
correctOrder: finalCorrectOrder,
|
|
932
|
-
explanation: aiGeneratedContent.explanation,
|
|
933
|
-
points: aiGeneratedContent.points,
|
|
934
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
935
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
936
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
937
|
-
};
|
|
938
|
-
try {
|
|
939
|
-
const validatedQuestion = SequenceQuestionZodSchema.parse(completeQuestion);
|
|
940
|
-
return { question: validatedQuestion };
|
|
941
|
-
} catch (validationError) {
|
|
942
|
-
console.error("Question validation failed:", validationError);
|
|
943
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
944
|
-
}
|
|
945
|
-
} else {
|
|
946
|
-
throw new Error("AI did not return content for the Sequence question.");
|
|
947
|
-
}
|
|
948
|
-
} catch (error) {
|
|
949
|
-
console.error("Error generating Sequence question:", error);
|
|
950
|
-
throw new Error(`Failed to generate Sequence question: ${error.message}`);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
function extractJsonFromMarkdown7(text) {
|
|
954
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
955
|
-
return match ? match[1].trim() : text.trim();
|
|
956
|
-
}
|
|
957
|
-
zod.z.object({
|
|
958
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
959
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
960
|
-
// <-- ĐÃ THÊM
|
|
961
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
962
|
-
isCaseSensitive: zod.z.boolean().optional().default(false).describe("Whether the answer should be case-sensitive."),
|
|
963
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
|
|
964
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context, if any.")
|
|
965
|
-
});
|
|
966
|
-
zod.z.object({
|
|
967
|
-
prompt: zod.z.string().describe("The question statement."),
|
|
968
|
-
acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1).describe("An array of acceptable short answers."),
|
|
969
|
-
isCaseSensitive: zod.z.boolean().optional().describe("Should the answer evaluation be case sensitive?"),
|
|
970
|
-
explanation: zod.z.string().optional().describe("Explanation for the correct answer(s)."),
|
|
971
|
-
points: zod.z.number().optional().default(10),
|
|
972
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
973
|
-
topic: zod.z.string().optional()
|
|
974
|
-
});
|
|
975
|
-
var ShortAnswerQuestionZodSchema = zod.z.object({
|
|
976
|
-
id: zod.z.string(),
|
|
977
|
-
questionType: zod.z.literal("short_answer"),
|
|
978
|
-
prompt: zod.z.string().min(1),
|
|
979
|
-
acceptedAnswers: zod.z.array(zod.z.string().min(1)).min(1),
|
|
980
|
-
isCaseSensitive: zod.z.boolean().optional(),
|
|
981
|
-
points: zod.z.number().min(0).optional(),
|
|
982
|
-
explanation: zod.z.string().optional(),
|
|
983
|
-
learningObjective: zod.z.string().optional(),
|
|
984
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
985
|
-
bloomLevel: zod.z.string().optional(),
|
|
986
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
987
|
-
contextCode: zod.z.string().optional(),
|
|
988
|
-
gradeBand: zod.z.string().optional(),
|
|
989
|
-
course: zod.z.string().optional(),
|
|
990
|
-
category: zod.z.string().optional(),
|
|
991
|
-
topic: zod.z.string().optional()
|
|
992
|
-
});
|
|
993
|
-
zod.z.object({
|
|
994
|
-
question: ShortAnswerQuestionZodSchema.optional().describe("The generated Short Answer question.")
|
|
995
|
-
});
|
|
996
|
-
async function generateShortAnswerQuestion(clientInput, apiKey) {
|
|
997
|
-
var _a;
|
|
998
|
-
try {
|
|
999
|
-
const ai = genkit.genkit({
|
|
1000
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
1001
|
-
model: googleai.gemini20Flash
|
|
1002
|
-
});
|
|
1003
|
-
const promptText = `You are an expert quiz question writer.
|
|
1004
|
-
Generate a single Short Answer question in ${clientInput.language} based on the following inputs.
|
|
1005
|
-
|
|
1006
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
1007
|
-
{
|
|
1008
|
-
"prompt": "What is the capital of France?",
|
|
1009
|
-
"acceptedAnswers": ["Paris"],
|
|
1010
|
-
"isCaseSensitive": false,
|
|
1011
|
-
"explanation": "Paris has been the capital of France since the 10th century.",
|
|
1012
|
-
"points": 5,
|
|
1013
|
-
"difficulty": "easy",
|
|
1014
|
-
"topic": "World Capitals"
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
Requirements:
|
|
1018
|
-
- The 'acceptedAnswers' array must contain at least one possible correct answer.
|
|
1019
|
-
- If there are multiple common ways to phrase the answer (e.g., "USA", "United States"), include them in the array.
|
|
1020
|
-
- Set 'isCaseSensitive' to true or false based on the nature of the answer. Default to the input value.
|
|
1021
|
-
- The content of 'prompt', 'acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
1022
|
-
|
|
1023
|
-
Topic: ${clientInput.topic}
|
|
1024
|
-
Language: ${clientInput.language}
|
|
1025
|
-
${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
|
|
1026
|
-
Difficulty: ${clientInput.difficulty}
|
|
1027
|
-
Case Sensitive: ${clientInput.isCaseSensitive}
|
|
1028
|
-
|
|
1029
|
-
Return only the JSON response.`;
|
|
1030
|
-
const response = await ai.generate(promptText);
|
|
1031
|
-
const rawText = response.text;
|
|
1032
|
-
const jsonText = extractJsonFromMarkdown7(rawText);
|
|
1033
|
-
console.log("AI Response:", jsonText);
|
|
1034
|
-
const aiGeneratedContent = JSON.parse(jsonText);
|
|
1035
|
-
if (aiGeneratedContent) {
|
|
1036
|
-
const completeQuestion = {
|
|
1037
|
-
id: generateUniqueId("saq_ai_"),
|
|
1038
|
-
questionType: "short_answer",
|
|
1039
|
-
prompt: aiGeneratedContent.prompt,
|
|
1040
|
-
acceptedAnswers: aiGeneratedContent.acceptedAnswers,
|
|
1041
|
-
// Ưu tiên giá trị từ AI, nếu không có thì dùng giá trị đầu vào
|
|
1042
|
-
isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
|
|
1043
|
-
explanation: aiGeneratedContent.explanation,
|
|
1044
|
-
points: aiGeneratedContent.points,
|
|
1045
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
1046
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
1047
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
1048
|
-
};
|
|
1049
|
-
try {
|
|
1050
|
-
const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
|
|
1051
|
-
return { question: validatedQuestion };
|
|
1052
|
-
} catch (validationError) {
|
|
1053
|
-
console.error("Question validation failed:", validationError);
|
|
1054
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
1055
|
-
}
|
|
1056
|
-
} else {
|
|
1057
|
-
throw new Error("AI did not return content for the Short Answer question.");
|
|
1058
|
-
}
|
|
1059
|
-
} catch (error) {
|
|
1060
|
-
console.error("Error generating Short Answer question:", error);
|
|
1061
|
-
throw new Error(`Failed to generate Short Answer question: ${error.message}`);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
function extractJsonFromMarkdown8(text) {
|
|
1065
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1066
|
-
return match ? match[1].trim() : text.trim();
|
|
1067
|
-
}
|
|
1068
|
-
zod.z.object({
|
|
1069
|
-
topic: zod.z.string().describe("The topic for the question."),
|
|
1070
|
-
language: zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
|
|
1071
|
-
// <-- ĐÃ THÊM
|
|
1072
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
|
|
1073
|
-
contextDescription: zod.z.string().optional().describe("A specific context or scenario for the question."),
|
|
1074
|
-
selectedContextId: zod.z.string().optional().describe("The ID of the selected context.")
|
|
1075
|
-
});
|
|
1076
|
-
zod.z.object({
|
|
1077
|
-
prompt: zod.z.string().describe("The statement to be evaluated as true or false."),
|
|
1078
|
-
correctAnswer: zod.z.boolean().describe("The correct answer (true or false)."),
|
|
1079
|
-
explanation: zod.z.string().optional().describe("A brief explanation of why the answer is correct."),
|
|
1080
|
-
points: zod.z.number().optional().default(10),
|
|
1081
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1082
|
-
topic: zod.z.string().optional()
|
|
1083
|
-
});
|
|
1084
|
-
var TrueFalseQuestionZodSchema = zod.z.object({
|
|
1085
|
-
id: zod.z.string(),
|
|
1086
|
-
questionType: zod.z.literal("true_false"),
|
|
1087
|
-
prompt: zod.z.string().min(1),
|
|
1088
|
-
correctAnswer: zod.z.boolean(),
|
|
1089
|
-
points: zod.z.number().min(0).optional(),
|
|
1090
|
-
explanation: zod.z.string().optional(),
|
|
1091
|
-
learningObjective: zod.z.string().optional(),
|
|
1092
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1093
|
-
bloomLevel: zod.z.string().optional(),
|
|
1094
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1095
|
-
contextCode: zod.z.string().optional(),
|
|
1096
|
-
gradeBand: zod.z.string().optional(),
|
|
1097
|
-
course: zod.z.string().optional(),
|
|
1098
|
-
category: zod.z.string().optional(),
|
|
1099
|
-
topic: zod.z.string().optional()
|
|
1100
|
-
});
|
|
1101
|
-
zod.z.object({
|
|
1102
|
-
question: TrueFalseQuestionZodSchema.optional().describe("The generated True/False question.")
|
|
1103
|
-
});
|
|
1104
|
-
async function generateTrueFalseQuestion(clientInput, apiKey) {
|
|
1105
|
-
try {
|
|
1106
|
-
const ai = genkit.genkit({
|
|
1107
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
1108
|
-
model: googleai.gemini20Flash
|
|
1109
|
-
});
|
|
1110
|
-
const promptText = `You are an expert quiz question writer.
|
|
1111
|
-
Generate a single True/False question in ${clientInput.language} based on the following inputs.
|
|
1112
|
-
|
|
1113
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
1114
|
-
{
|
|
1115
|
-
"prompt": "The Earth is the fourth planet from the Sun.",
|
|
1116
|
-
"correctAnswer": false,
|
|
1117
|
-
"explanation": "The Earth is the third planet from the Sun. Mars is the fourth.",
|
|
1118
|
-
"points": 10,
|
|
1119
|
-
"difficulty": "easy",
|
|
1120
|
-
"topic": "Astronomy"
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
Requirements:
|
|
1124
|
-
- The 'prompt' must be a clear statement that is definitively true or false.
|
|
1125
|
-
- 'correctAnswer' must be a boolean value (true or false).
|
|
1126
|
-
- The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
|
|
1127
|
-
|
|
1128
|
-
Topic: ${clientInput.topic}
|
|
1129
|
-
Language: ${clientInput.language}
|
|
1130
|
-
${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
|
|
1131
|
-
Difficulty: ${clientInput.difficulty}
|
|
1132
|
-
|
|
1133
|
-
Return only the JSON response.`;
|
|
1134
|
-
const response = await ai.generate(promptText);
|
|
1135
|
-
const rawText = response.text;
|
|
1136
|
-
const jsonText = extractJsonFromMarkdown8(rawText);
|
|
1137
|
-
console.log("AI Response:", jsonText);
|
|
1138
|
-
const aiGeneratedContent = JSON.parse(jsonText);
|
|
1139
|
-
if (aiGeneratedContent) {
|
|
1140
|
-
const completeQuestion = {
|
|
1141
|
-
id: generateUniqueId("tf_ai_"),
|
|
1142
|
-
questionType: "true_false",
|
|
1143
|
-
prompt: aiGeneratedContent.prompt,
|
|
1144
|
-
correctAnswer: aiGeneratedContent.correctAnswer,
|
|
1145
|
-
explanation: aiGeneratedContent.explanation,
|
|
1146
|
-
points: aiGeneratedContent.points,
|
|
1147
|
-
topic: aiGeneratedContent.topic || clientInput.topic,
|
|
1148
|
-
difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
|
|
1149
|
-
contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
|
|
1150
|
-
};
|
|
1151
|
-
try {
|
|
1152
|
-
const validatedQuestion = TrueFalseQuestionZodSchema.parse(completeQuestion);
|
|
1153
|
-
return { question: validatedQuestion };
|
|
1154
|
-
} catch (validationError) {
|
|
1155
|
-
console.error("Question validation failed:", validationError);
|
|
1156
|
-
throw new Error(`Generated question failed validation: ${validationError}`);
|
|
1157
|
-
}
|
|
1158
|
-
} else {
|
|
1159
|
-
throw new Error("AI did not return content for the True/False question.");
|
|
1160
|
-
}
|
|
1161
|
-
} catch (error) {
|
|
1162
|
-
console.error("Error generating True/False question:", error);
|
|
1163
|
-
throw new Error(`Failed to generate True/False question: ${error.message}`);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
function extractJsonFromMarkdown9(text) {
|
|
1167
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1168
|
-
return match ? match[1].trim() : text.trim();
|
|
1169
|
-
}
|
|
1170
|
-
var fullQuizSupportedQuestionTypesArray = [
|
|
1171
|
-
"true_false",
|
|
1172
|
-
"multiple_choice",
|
|
1173
|
-
"multiple_response",
|
|
1174
|
-
"short_answer",
|
|
1175
|
-
"numeric",
|
|
1176
|
-
"fill_in_the_blanks",
|
|
1177
|
-
"sequence",
|
|
1178
|
-
"matching"
|
|
1179
|
-
];
|
|
1180
|
-
var BloomLevelStringsEnum = zod.z.enum(["remembering", "understanding", "applying"]);
|
|
1181
|
-
zod.z.object({
|
|
1182
|
-
language: zod.z.string().optional().default("English").describe('The language for the quiz plan (e.g., "Vietnamese", "English").'),
|
|
1183
|
-
// <-- ĐÃ THÊM
|
|
1184
|
-
totalQuestions: zod.z.number().int().min(1).max(50),
|
|
1185
|
-
topics: zod.z.array(zod.z.object({
|
|
1186
|
-
topic: zod.z.string().min(1),
|
|
1187
|
-
ratio: zod.z.number().min(0).max(100)
|
|
1188
|
-
})).min(1),
|
|
1189
|
-
bloomLevels: zod.z.array(zod.z.object({
|
|
1190
|
-
level: BloomLevelStringsEnum,
|
|
1191
|
-
ratio: zod.z.number().min(0).max(100)
|
|
1192
|
-
})).min(1),
|
|
1193
|
-
selectedContextIds: zod.z.array(zod.z.string()).optional(),
|
|
1194
|
-
selectedQuestionTypes: zod.z.array(zod.z.enum(fullQuizSupportedQuestionTypesArray)).min(1)
|
|
1195
|
-
});
|
|
1196
|
-
var PlannedQuestionSchema = zod.z.object({
|
|
1197
|
-
plannedTopic: zod.z.string().min(1).describe("The specific topic for this question."),
|
|
1198
|
-
plannedQuestionType: zod.z.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
|
|
1199
|
-
plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned.")
|
|
1200
|
-
});
|
|
1201
|
-
var GenerateQuizPlanOutputSchema = zod.z.object({
|
|
1202
|
-
quizPlan: zod.z.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz.")
|
|
1203
|
-
});
|
|
1204
|
-
async function generateQuizPlan(clientInput, apiKey) {
|
|
1205
|
-
try {
|
|
1206
|
-
const totalTopicRatio = clientInput.topics.reduce((sum, t) => sum + t.ratio, 0);
|
|
1207
|
-
if (Math.abs(totalTopicRatio - 100) > 1) {
|
|
1208
|
-
throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
|
|
1209
|
-
}
|
|
1210
|
-
const totalBloomRatio = clientInput.bloomLevels.reduce((sum, b) => sum + b.ratio, 0);
|
|
1211
|
-
if (Math.abs(totalBloomRatio - 100) > 1) {
|
|
1212
|
-
throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
|
|
1213
|
-
}
|
|
1214
|
-
const ai = genkit.genkit({
|
|
1215
|
-
plugins: [googleai.googleAI({ apiKey })],
|
|
1216
|
-
model: googleai.gemini20Flash
|
|
1217
|
-
});
|
|
1218
|
-
const topicsDistribution = clientInput.topics.map((t) => `- Topic: "${t.topic}", Ratio: ${t.ratio}%`).join("\n ");
|
|
1219
|
-
const bloomDistribution = clientInput.bloomLevels.map((b) => `- Level: "${b.level}", Ratio: ${b.ratio}%`).join("\n ");
|
|
1220
|
-
const allowedQuestionTypes = clientInput.selectedQuestionTypes.map((t) => `'${t}'`).join(", ");
|
|
1221
|
-
const promptText = `You are an expert educational content planner. Your task is to generate a detailed plan for a quiz in ${clientInput.language}.
|
|
1222
|
-
|
|
1223
|
-
IMPORTANT: Return the response as JSON with this EXACT format:
|
|
1224
|
-
{
|
|
1225
|
-
"quizPlan": [
|
|
1226
|
-
{ "plannedTopic": "Specific Topic A", "plannedQuestionType": "multiple_choice", "plannedBloomLevel": "remembering" },
|
|
1227
|
-
{ "plannedTopic": "Specific Topic B", "plannedQuestionType": "true_false", "plannedBloomLevel": "understanding" }
|
|
1228
|
-
]
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
Constraints and Guidelines:
|
|
1232
|
-
1. **Total Questions**: The output 'quizPlan' array must contain exactly ${clientInput.totalQuestions} elements.
|
|
1233
|
-
2. **Topic Distribution**: Distribute the questions across the following topics according to their specified ratios. Match these ratios as closely as possible.
|
|
1234
|
-
${topicsDistribution}
|
|
1235
|
-
3. **Bloom Level Distribution**: Distribute the questions across the following Bloom's Taxonomy levels according to their specified ratios.
|
|
1236
|
-
${bloomDistribution}
|
|
1237
|
-
4. **Allowed Question Types**: For each planned question, 'plannedQuestionType' must be one of these types: ${allowedQuestionTypes}. Use a variety of these types.
|
|
1238
|
-
5. **Topic Specificity**: The 'plannedTopic' for each question should be a specific sub-topic or aspect related to one of the main topics provided, and must be in ${clientInput.language}.
|
|
1239
|
-
|
|
1240
|
-
Carefully calculate the number of questions for each topic and Bloom level based on the total number of questions and the provided ratios. If ratios lead to fractional questions, round to the nearest whole number while ensuring the total number of questions remains exactly ${clientInput.totalQuestions}.
|
|
1241
|
-
The final 'quizPlan' array must have exactly ${clientInput.totalQuestions} elements.
|
|
1242
|
-
|
|
1243
|
-
Return only the JSON response.`;
|
|
1244
|
-
const response = await ai.generate(promptText);
|
|
1245
|
-
const rawText = response.text;
|
|
1246
|
-
const jsonText = extractJsonFromMarkdown9(rawText);
|
|
1247
|
-
console.log("AI Response:", jsonText);
|
|
1248
|
-
const aiGeneratedContent = JSON.parse(jsonText);
|
|
1249
|
-
if (!aiGeneratedContent || !aiGeneratedContent.quizPlan) {
|
|
1250
|
-
throw new Error("AI did not return a valid quiz plan structure.");
|
|
1251
|
-
}
|
|
1252
|
-
if (aiGeneratedContent.quizPlan.length !== clientInput.totalQuestions) {
|
|
1253
|
-
throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested. The plan must match the total number of questions.`);
|
|
1254
|
-
}
|
|
1255
|
-
aiGeneratedContent.quizPlan.forEach((item, index) => {
|
|
1256
|
-
if (!clientInput.selectedQuestionTypes.includes(item.plannedQuestionType)) {
|
|
1257
|
-
throw new Error(`AI planned question ${index + 1} with a disallowed question type: '${item.plannedQuestionType}'`);
|
|
1258
|
-
}
|
|
1259
|
-
});
|
|
1260
|
-
try {
|
|
1261
|
-
const validatedPlan = GenerateQuizPlanOutputSchema.parse(aiGeneratedContent);
|
|
1262
|
-
return validatedPlan;
|
|
1263
|
-
} catch (validationError) {
|
|
1264
|
-
console.error("Quiz plan validation failed:", validationError);
|
|
1265
|
-
throw new Error(`Generated quiz plan failed validation: ${validationError}`);
|
|
1266
|
-
}
|
|
1267
|
-
} catch (error) {
|
|
1268
|
-
console.error("Error generating Quiz Plan:", error);
|
|
1269
|
-
throw new Error(`Failed to generate Quiz Plan: ${error.message}`);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
var internalContextOptions = [
|
|
1273
|
-
// ... (giữ nguyên mảng này)
|
|
1274
|
-
{ contextId: "THEO_ABS", contextDescription: "L\xFD thuy\u1EBFt/Tr\u1EEBu t\u01B0\u1EE3ng" },
|
|
1275
|
-
{ contextId: "SPEC_CASE", contextDescription: "V\xED d\u1EE5 C\u1EE5 th\u1EC3/Tr\u01B0\u1EDDng h\u1EE3p Ri\xEAng" },
|
|
1276
|
-
{ contextId: "NAT_OBS", contextDescription: "Hi\u1EC7n t\u01B0\u1EE3ng T\u1EF1 nhi\xEAn/Quan s\xE1t" },
|
|
1277
|
-
{ contextId: "TECH_ENG", contextDescription: "\u1EE8ng d\u1EE5ng C\xF4ng ngh\u1EC7/K\u1EF9 thu\u1EADt" },
|
|
1278
|
-
{ contextId: "EXP_INV", contextDescription: "Th\xED nghi\u1EC7m/\u0110i\u1EC1u tra Khoa h\u1ECDc" },
|
|
1279
|
-
{ contextId: "REAL_PROB", contextDescription: "V\u1EA5n \u0111\u1EC1 Th\u1EF1c t\u1EBF/X\xE3 h\u1ED9i/M\xF4i tr\u01B0\u1EDDng" },
|
|
1280
|
-
{ contextId: "DATA_MOD", contextDescription: "Di\u1EC5n gi\u1EA3i D\u1EEF li\u1EC7u/M\xF4 h\xECnh h\xF3a" },
|
|
1281
|
-
{ contextId: "HIST_SCI", contextDescription: "L\u1ECBch s\u1EED/Ph\xE1t tri\u1EC3n Khoa h\u1ECDc" },
|
|
1282
|
-
{ contextId: "INTERDISC", contextDescription: "Li\xEAn ng\xE0nh (Interdisciplinary)" },
|
|
1283
|
-
{ contextId: "HYPO_COMP", contextDescription: "Gi\u1EA3 \u0111\u1ECBnh/So s\xE1nh T\xECnh hu\u1ED1ng" }
|
|
1284
|
-
];
|
|
1285
|
-
var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomContextInput) => {
|
|
1286
|
-
let contextScore = 1;
|
|
1287
|
-
const selectedContext = contextId ? internalContextOptions.find((c) => c.contextId === contextId) : void 0;
|
|
1288
|
-
if (selectedContext) {
|
|
1289
|
-
if (["THEO_ABS", "HIST_SCI"].includes(selectedContext.contextId)) contextScore = 1;
|
|
1290
|
-
else if (["SPEC_CASE", "NAT_OBS", "DATA_MOD", "INTERDISC", "HYPO_COMP"].includes(selectedContext.contextId)) contextScore = 2;
|
|
1291
|
-
else if (["TECH_ENG", "EXP_INV", "REAL_PROB"].includes(selectedContext.contextId)) contextScore = 3;
|
|
1292
|
-
} else if (generalCustomContextInput == null ? void 0 : generalCustomContextInput.trim()) {
|
|
1293
|
-
contextScore = 2;
|
|
1294
|
-
}
|
|
1295
|
-
let bloomScore = 1;
|
|
1296
|
-
if (bloomLevel === "understanding") bloomScore = 2;
|
|
1297
|
-
else if (bloomLevel === "applying") bloomScore = 3;
|
|
1298
|
-
let questionTypeScore = 1;
|
|
1299
|
-
switch (qType) {
|
|
1300
|
-
case "true_false":
|
|
1301
|
-
case "multiple_choice":
|
|
1302
|
-
case "short_answer":
|
|
1303
|
-
questionTypeScore = 1;
|
|
1304
|
-
break;
|
|
1305
|
-
case "matching":
|
|
1306
|
-
case "fill_in_the_blanks":
|
|
1307
|
-
case "numeric":
|
|
1308
|
-
questionTypeScore = 2;
|
|
1309
|
-
break;
|
|
1310
|
-
case "sequence":
|
|
1311
|
-
case "multiple_response":
|
|
1312
|
-
case "drag_and_drop":
|
|
1313
|
-
questionTypeScore = 3;
|
|
1314
|
-
break;
|
|
1315
|
-
default:
|
|
1316
|
-
questionTypeScore = 1;
|
|
1317
|
-
}
|
|
1318
|
-
const totalScore = bloomScore + contextScore + questionTypeScore;
|
|
1319
|
-
if (totalScore <= 4) return "easy";
|
|
1320
|
-
if (totalScore <= 6) return "medium";
|
|
1321
|
-
return "hard";
|
|
1322
|
-
};
|
|
1323
|
-
var PlannedQuestionSchema2 = zod.z.object({
|
|
1324
|
-
plannedTopic: zod.z.string().min(1),
|
|
1325
|
-
plannedQuestionType: zod.z.string(),
|
|
1326
|
-
// Giữ dạng string để linh hoạt, sẽ kiểm tra trong logic
|
|
1327
|
-
plannedBloomLevel: zod.z.enum(["remembering", "understanding", "applying"]),
|
|
1328
|
-
plannedContextId: zod.z.string().optional()
|
|
1329
|
-
});
|
|
1330
|
-
zod.z.object({
|
|
1331
|
-
quizPlan: zod.z.array(PlannedQuestionSchema2).min(1),
|
|
1332
|
-
language: zod.z.string().optional().default("English").describe("The language for the generated questions."),
|
|
1333
|
-
// <-- ĐÃ THÊM
|
|
1334
|
-
selectedContextIds: zod.z.array(zod.z.string()).optional(),
|
|
1335
|
-
customContextInput: zod.z.string().optional()
|
|
1336
|
-
});
|
|
1337
|
-
var GenerationErrorSchema = zod.z.object({
|
|
1338
|
-
plannedQuestionIndex: zod.z.number(),
|
|
1339
|
-
plannedTopic: zod.z.string(),
|
|
1340
|
-
plannedQuestionType: zod.z.string(),
|
|
1341
|
-
error: zod.z.string()
|
|
1342
|
-
});
|
|
1343
|
-
zod.z.object({
|
|
1344
|
-
generatedQuestions: zod.z.array(zod.z.any()),
|
|
1345
|
-
// z.any() là thực tế vì union của tất cả các loại câu hỏi rất phức tạp
|
|
1346
|
-
errors: zod.z.array(GenerationErrorSchema).optional()
|
|
1347
|
-
});
|
|
1348
|
-
async function generateQuestionsFromQuizPlan(clientInput, apiKey) {
|
|
1349
|
-
var _a, _b;
|
|
1350
|
-
const { quizPlan, selectedContextIds, customContextInput, language } = clientInput;
|
|
1351
|
-
const generatedQuestions = [];
|
|
1352
|
-
const errors = [];
|
|
1353
|
-
for (let i = 0; i < quizPlan.length; i++) {
|
|
1354
|
-
const plannedQ = quizPlan[i];
|
|
1355
|
-
let generationError = null;
|
|
1356
|
-
try {
|
|
1357
|
-
let contextDescriptionForAI;
|
|
1358
|
-
let contextIdForDifficultyCalc = plannedQ.plannedContextId;
|
|
1359
|
-
if (plannedQ.plannedContextId && plannedQ.plannedContextId !== "__custom__") {
|
|
1360
|
-
contextDescriptionForAI = (_a = internalContextOptions.find((c) => c.contextId === plannedQ.plannedContextId)) == null ? void 0 : _a.contextDescription;
|
|
1361
|
-
} else if (plannedQ.plannedContextId === "__custom__") {
|
|
1362
|
-
contextDescriptionForAI = customContextInput == null ? void 0 : customContextInput.trim();
|
|
1363
|
-
} else if ((selectedContextIds == null ? void 0 : selectedContextIds[0]) && selectedContextIds[0] !== "__custom__") {
|
|
1364
|
-
contextDescriptionForAI = (_b = internalContextOptions.find((c) => c.contextId === selectedContextIds[0])) == null ? void 0 : _b.contextDescription;
|
|
1365
|
-
if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = selectedContextIds[0];
|
|
1366
|
-
} else if (customContextInput == null ? void 0 : customContextInput.trim()) {
|
|
1367
|
-
contextDescriptionForAI = customContextInput.trim();
|
|
1368
|
-
if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = "__custom__";
|
|
1369
|
-
}
|
|
1370
|
-
const difficultyForAI = calculateCombinedDifficulty(
|
|
1371
|
-
contextIdForDifficultyCalc,
|
|
1372
|
-
plannedQ.plannedBloomLevel,
|
|
1373
|
-
plannedQ.plannedQuestionType,
|
|
1374
|
-
contextDescriptionForAI
|
|
1375
|
-
);
|
|
1376
|
-
const baseClientInput = {
|
|
1377
|
-
topic: plannedQ.plannedTopic,
|
|
1378
|
-
language,
|
|
1379
|
-
// <-- TRUYỀN `language` VÀO
|
|
1380
|
-
difficulty: difficultyForAI,
|
|
1381
|
-
contextDescription: contextDescriptionForAI,
|
|
1382
|
-
selectedContextId: plannedQ.plannedContextId || contextIdForDifficultyCalc
|
|
1383
|
-
};
|
|
1384
|
-
let result = {};
|
|
1385
|
-
switch (plannedQ.plannedQuestionType) {
|
|
1386
|
-
case "true_false":
|
|
1387
|
-
result = await generateTrueFalseQuestion(baseClientInput, apiKey);
|
|
1388
|
-
break;
|
|
1389
|
-
case "multiple_choice":
|
|
1390
|
-
result = await generateMCQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 4 }), apiKey);
|
|
1391
|
-
break;
|
|
1392
|
-
case "multiple_response":
|
|
1393
|
-
result = await generateMRQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 5, minCorrectAnswers: 2, maxCorrectAnswers: 3 }), apiKey);
|
|
1394
|
-
break;
|
|
1395
|
-
case "short_answer":
|
|
1396
|
-
result = await generateShortAnswerQuestion(__spreadProps(__spreadValues({}, baseClientInput), { isCaseSensitive: false }), apiKey);
|
|
1397
|
-
break;
|
|
1398
|
-
case "numeric":
|
|
1399
|
-
result = await generateNumericQuestion(__spreadProps(__spreadValues({}, baseClientInput), { allowDecimals: true }), apiKey);
|
|
1400
|
-
break;
|
|
1401
|
-
case "fill_in_the_blanks":
|
|
1402
|
-
result = await generateFillInTheBlanksQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfBlanks: 2, isCaseSensitive: false }), apiKey);
|
|
1403
|
-
break;
|
|
1404
|
-
case "sequence":
|
|
1405
|
-
result = await generateSequenceQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfItems: 4 }), apiKey);
|
|
1406
|
-
break;
|
|
1407
|
-
case "matching":
|
|
1408
|
-
result = await generateMatchingQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfPairs: 4, shuffleOptions: true }), apiKey);
|
|
1409
|
-
break;
|
|
1410
|
-
default:
|
|
1411
|
-
generationError = `Question type "${plannedQ.plannedQuestionType}" is not supported for automated generation.`;
|
|
1412
|
-
}
|
|
1413
|
-
if (result.question) {
|
|
1414
|
-
const question = result.question;
|
|
1415
|
-
question.topic = plannedQ.plannedTopic;
|
|
1416
|
-
question.bloomLevel = plannedQ.plannedBloomLevel;
|
|
1417
|
-
question.difficulty = difficultyForAI;
|
|
1418
|
-
question.contextCode = baseClientInput.selectedContextId;
|
|
1419
|
-
if (question.points === void 0) question.points = 10;
|
|
1420
|
-
generatedQuestions.push(question);
|
|
1421
|
-
} else if (!generationError) {
|
|
1422
|
-
generationError = `AI did not return a question object for type '${plannedQ.plannedQuestionType}'.`;
|
|
1423
|
-
}
|
|
1424
|
-
} catch (e) {
|
|
1425
|
-
generationError = e.message || `An unknown error occurred.`;
|
|
1426
|
-
}
|
|
1427
|
-
if (generationError) {
|
|
1428
|
-
console.error(`Error generating question at index ${i} (Topic: ${plannedQ.plannedTopic}): ${generationError}`);
|
|
1429
|
-
errors.push({
|
|
1430
|
-
plannedQuestionIndex: i,
|
|
1431
|
-
plannedTopic: plannedQ.plannedTopic,
|
|
1432
|
-
plannedQuestionType: plannedQ.plannedQuestionType,
|
|
1433
|
-
error: generationError
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
return { generatedQuestions, errors: errors.length > 0 ? errors : void 0 };
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
exports.generateFillInTheBlanksQuestion = generateFillInTheBlanksQuestion;
|
|
1441
|
-
exports.generateMCQQuestion = generateMCQQuestion;
|
|
1442
|
-
exports.generateMRQQuestion = generateMRQQuestion;
|
|
1443
|
-
exports.generateMatchingQuestion = generateMatchingQuestion;
|
|
1444
|
-
exports.generateNumericQuestion = generateNumericQuestion;
|
|
1445
|
-
exports.generateQuestionsFromQuizPlan = generateQuestionsFromQuizPlan;
|
|
1446
|
-
exports.generateQuizPlan = generateQuizPlan;
|
|
1447
|
-
exports.generateSequenceQuestion = generateSequenceQuestion;
|
|
1448
|
-
exports.generateShortAnswerQuestion = generateShortAnswerQuestion;
|
|
1449
|
-
exports.generateTrueFalseQuestion = generateTrueFalseQuestion;
|