@thanh01.pmt/interactive-quiz-kit 1.0.10 → 1.0.12

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/index.js DELETED
@@ -1,3173 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __defProps = Object.defineProperties;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
7
- var __getOwnPropNames = Object.getOwnPropertyNames;
8
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
- var __getProtoOf = Object.getPrototypeOf;
10
- var __hasOwnProp = Object.prototype.hasOwnProperty;
11
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
12
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
- var __spreadValues = (a, b) => {
14
- for (var prop in b || (b = {}))
15
- if (__hasOwnProp.call(b, prop))
16
- __defNormalProp(a, prop, b[prop]);
17
- if (__getOwnPropSymbols)
18
- for (var prop of __getOwnPropSymbols(b)) {
19
- if (__propIsEnum.call(b, prop))
20
- __defNormalProp(a, prop, b[prop]);
21
- }
22
- return a;
23
- };
24
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
- var __export = (target, all) => {
26
- for (var name in all)
27
- __defProp(target, name, { get: all[name], enumerable: true });
28
- };
29
- var __copyProps = (to, from, except, desc) => {
30
- if (from && typeof from === "object" || typeof from === "function") {
31
- for (let key of __getOwnPropNames(from))
32
- if (!__hasOwnProp.call(to, key) && key !== except)
33
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
34
- }
35
- return to;
36
- };
37
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
38
- // If the importer is in node compatibility mode or this is not an ESM
39
- // file that has been converted to a CommonJS file using a Babel-
40
- // compatible transform (i.e. "__esModule" has not been set), then set
41
- // "default" to the CommonJS "module.exports" for node compatibility.
42
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
43
- mod
44
- ));
45
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
46
-
47
- // src/index.ts
48
- var index_exports = {};
49
- __export(index_exports, {
50
- APIKeyService: () => APIKeyService,
51
- GEMINI_API_KEY_SERVICE_NAME: () => GEMINI_API_KEY_SERVICE_NAME,
52
- QuizEditorService: () => QuizEditorService,
53
- QuizEngine: () => QuizEngine,
54
- SCORMService: () => SCORMService,
55
- cn: () => cn,
56
- emptyQuiz: () => emptyQuiz,
57
- exportQuizAsSCORMZip: () => exportQuizAsSCORMZip,
58
- generateFillInTheBlanksQuestion: () => generateFillInTheBlanksQuestion,
59
- generateLauncherHTML: () => generateLauncherHTML,
60
- generateMCQQuestion: () => generateMCQQuestion,
61
- generateMRQQuestion: () => generateMRQQuestion,
62
- generateMatchingQuestion: () => generateMatchingQuestion,
63
- generateNumericQuestion: () => generateNumericQuestion,
64
- generateQuestionsFromQuizPlan: () => generateQuestionsFromQuizPlan,
65
- generateQuizPlan: () => generateQuizPlan,
66
- generateSCORMManifest: () => generateSCORMManifest,
67
- generateSequenceQuestion: () => generateSequenceQuestion,
68
- generateShortAnswerQuestion: () => generateShortAnswerQuestion,
69
- generateTrueFalseQuestion: () => generateTrueFalseQuestion,
70
- generateUniqueId: () => generateUniqueId,
71
- sampleQuiz: () => sampleQuiz
72
- });
73
- module.exports = __toCommonJS(index_exports);
74
-
75
- // src/services/SCORMService.ts
76
- var SCORM_TRUE = "true";
77
- var SCORM_NO_ERROR = "0";
78
- var CMI_CORE_LESSON_STATUS_PASSED = "passed";
79
- var CMI_CORE_LESSON_STATUS_FAILED = "failed";
80
- var CMI_CORE_LESSON_STATUS_COMPLETED = "completed";
81
- var CMI_CORE_LESSON_STATUS_INCOMPLETE = "incomplete";
82
- var CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED = "not attempted";
83
- var CMI_COMPLETION_STATUS_COMPLETED = "completed";
84
- var CMI_COMPLETION_STATUS_INCOMPLETE = "incomplete";
85
- var CMI_SUCCESS_STATUS_PASSED = "passed";
86
- var CMI_SUCCESS_STATUS_FAILED = "failed";
87
- var SCORMService = class {
88
- constructor(settings) {
89
- this.scormAPI = null;
90
- this.scormVersionFound = null;
91
- this.isInitialized = false;
92
- this.isTerminated = false;
93
- this.studentName = null;
94
- this.settings = __spreadValues({
95
- setCompletionOnFinish: true,
96
- setSuccessOnPass: true,
97
- autoCommit: true
98
- }, settings);
99
- if (typeof window !== "undefined") {
100
- this._findAPI();
101
- }
102
- }
103
- _findAPIRecursive(win) {
104
- if (win === null) return null;
105
- if (win.API_1484_11) {
106
- this.scormVersionFound = "2004";
107
- return win.API_1484_11;
108
- }
109
- if (win.API) {
110
- this.scormVersionFound = "1.2";
111
- return win.API;
112
- }
113
- if (win.parent && win.parent !== win) {
114
- return this._findAPIRecursive(win.parent);
115
- }
116
- if (win.opener && typeof win.opener !== "undefined" && win.opener !== win && win.opener !== win.parent) {
117
- try {
118
- if (win.opener.document) {
119
- return this._findAPIRecursive(win.opener);
120
- }
121
- } catch (e) {
122
- console.warn("SCORMService: Could not access win.opener for API search due to cross-origin restrictions.");
123
- }
124
- }
125
- return null;
126
- }
127
- _findAPI() {
128
- try {
129
- this.scormAPI = this._findAPIRecursive(window);
130
- if (this.scormAPI) {
131
- if (!this.scormVersionFound) this.scormVersionFound = this.settings.version;
132
- console.log(`SCORMService: API Found. Version determined: ${this.scormVersionFound}`);
133
- } else {
134
- console.warn("SCORMService: SCORM API not found in window hierarchy.");
135
- }
136
- } catch (e) {
137
- console.error("SCORMService: Error finding SCORM API", e);
138
- this.scormAPI = null;
139
- }
140
- }
141
- hasAPI() {
142
- return this.scormAPI !== null;
143
- }
144
- getSCORMVersion() {
145
- return this.scormVersionFound;
146
- }
147
- initialize() {
148
- if (!this.hasAPI()) return { success: false, error: "SCORM API not found." };
149
- if (this.isInitialized) return { success: true, studentName: this.studentName || void 0 };
150
- const result = this.scormVersionFound === "2004" ? this.scormAPI.Initialize("") : this.scormAPI.LMSInitialize("");
151
- if (result.toString() === SCORM_TRUE || result === true) {
152
- this.isInitialized = true;
153
- this.isTerminated = false;
154
- const studentNameVar = this.settings.studentNameVar || (this.scormVersionFound === "2004" ? "cmi.learner_name" : "cmi.core.student_name");
155
- this.studentName = this.getValue(studentNameVar);
156
- if (this.scormVersionFound === "2004") {
157
- const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
158
- if (this.getValue(completionStatusVar) === "not attempted") {
159
- this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
160
- }
161
- } else {
162
- const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
163
- if (this.getValue(lessonStatusVar) === CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED) {
164
- this.setValue(lessonStatusVar, CMI_CORE_LESSON_STATUS_INCOMPLETE);
165
- }
166
- }
167
- if (this.settings.autoCommit) this.commit();
168
- return { success: true, studentName: this.studentName || void 0 };
169
- } else {
170
- const error = this.getLastError();
171
- return { success: false, error: `Initialization failed: ${error.message}` };
172
- }
173
- }
174
- terminate() {
175
- if (!this.hasAPI() || !this.isInitialized || this.isTerminated) {
176
- const reason = !this.hasAPI() ? "API not found" : !this.isInitialized ? "Not initialized" : "Already terminated";
177
- return { success: !this.hasAPI() || this.isTerminated, error: this.isTerminated ? void 0 : reason };
178
- }
179
- const result = this.scormVersionFound === "2004" ? this.scormAPI.Terminate("") : this.scormAPI.LMSFinish("");
180
- if (result.toString() === SCORM_TRUE || result === true) {
181
- this.isTerminated = true;
182
- this.isInitialized = false;
183
- return { success: true };
184
- } else {
185
- const error = this.getLastError();
186
- return { success: false, error: `Termination failed: ${error.message}` };
187
- }
188
- }
189
- setValue(element, value) {
190
- if (!this.hasAPI() || !this.isInitialized) {
191
- return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
192
- }
193
- const valStr = value.toString();
194
- const result = this.scormVersionFound === "2004" ? this.scormAPI.SetValue(element, valStr) : this.scormAPI.LMSSetValue(element, valStr);
195
- if (result.toString() === SCORM_TRUE || result === true) {
196
- if (this.settings.autoCommit) this.commit();
197
- return { success: true };
198
- } else {
199
- const error = this.getLastError();
200
- return { success: false, error: `SetValue failed for ${element}: ${error.message}` };
201
- }
202
- }
203
- getValue(element) {
204
- var _a;
205
- if (!this.hasAPI() || !this.isInitialized) return null;
206
- const value = this.scormVersionFound === "2004" ? this.scormAPI.GetValue(element) : this.scormAPI.LMSGetValue(element);
207
- const error = this.getLastError();
208
- if (error.code !== SCORM_NO_ERROR && error.code !== "403" && error.code !== "0") {
209
- console.warn(`SCORMService: GetValue for ${element} produced an error ${error.code}: ${error.message}. Returning raw value:`, value);
210
- }
211
- return (_a = value == null ? void 0 : value.toString()) != null ? _a : null;
212
- }
213
- commit() {
214
- if (!this.hasAPI() || !this.isInitialized) {
215
- return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
216
- }
217
- const result = this.scormVersionFound === "2004" ? this.scormAPI.Commit("") : this.scormAPI.LMSCommit("");
218
- if (result.toString() === SCORM_TRUE || result === true) {
219
- return { success: true };
220
- } else {
221
- const error = this.getLastError();
222
- return { success: false, error: `Commit failed: ${error.message}` };
223
- }
224
- }
225
- setScore(rawScore, maxScore, minScore = 0) {
226
- if (!this.hasAPI() || !this.isInitialized) return;
227
- if (this.scormVersionFound === "2004") {
228
- const scoreRawVar = this.settings.scoreRawVar_2004 || this.settings.scoreRawVar || "cmi.score.raw";
229
- const scoreMaxVar = this.settings.scoreMaxVar_2004 || this.settings.scoreMaxVar || "cmi.score.max";
230
- const scoreMinVar = this.settings.scoreMinVar_2004 || this.settings.scoreMinVar || "cmi.score.min";
231
- const scoreScaledVar = this.settings.scoreScaledVar_2004 || "cmi.score.scaled";
232
- this.setValue(scoreMinVar, minScore);
233
- this.setValue(scoreMaxVar, maxScore);
234
- this.setValue(scoreRawVar, rawScore);
235
- if (maxScore > minScore) {
236
- const scaledScore = (rawScore - minScore) / (maxScore - minScore);
237
- this.setValue(scoreScaledVar, parseFloat(scaledScore.toFixed(4)));
238
- } else if (maxScore === minScore && maxScore !== 0) {
239
- this.setValue(scoreScaledVar, rawScore >= maxScore ? 1 : 0);
240
- } else {
241
- this.setValue(scoreScaledVar, 0);
242
- }
243
- } else {
244
- const scoreRawVar = this.settings.scoreRawVar_1_2 || this.settings.scoreRawVar || "cmi.core.score.raw";
245
- const scoreMaxVar = this.settings.scoreMaxVar_1_2 || this.settings.scoreMaxVar || "cmi.core.score.max";
246
- const scoreMinVar = this.settings.scoreMinVar_1_2 || this.settings.scoreMinVar || "cmi.core.score.min";
247
- this.setValue(scoreMinVar, minScore);
248
- this.setValue(scoreMaxVar, maxScore);
249
- this.setValue(scoreRawVar, rawScore);
250
- }
251
- }
252
- setLessonStatus(status, passed) {
253
- if (!this.hasAPI() || !this.isInitialized) return;
254
- if (this.scormVersionFound === "2004") {
255
- const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
256
- const successStatusVar = this.settings.successStatusVar_2004 || "cmi.success_status";
257
- if (this.settings.setCompletionOnFinish && (status === "completed" || status === "passed" || status === "failed")) {
258
- this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_COMPLETED);
259
- } else if (status === "incomplete" || status === "browsed") {
260
- this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
261
- }
262
- if (this.settings.setSuccessOnPass && passed !== void 0) {
263
- this.setValue(successStatusVar, passed ? CMI_SUCCESS_STATUS_PASSED : CMI_SUCCESS_STATUS_FAILED);
264
- }
265
- } else {
266
- const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
267
- let finalStatus = status;
268
- if (this.settings.setCompletionOnFinish) {
269
- if (this.settings.setSuccessOnPass && passed !== void 0) {
270
- finalStatus = passed ? CMI_CORE_LESSON_STATUS_PASSED : CMI_CORE_LESSON_STATUS_FAILED;
271
- } else {
272
- finalStatus = CMI_CORE_LESSON_STATUS_COMPLETED;
273
- }
274
- } else {
275
- if (status === CMI_CORE_LESSON_STATUS_PASSED || status === CMI_CORE_LESSON_STATUS_FAILED) {
276
- } else {
277
- finalStatus = CMI_CORE_LESSON_STATUS_INCOMPLETE;
278
- }
279
- }
280
- this.setValue(lessonStatusVar, finalStatus);
281
- }
282
- }
283
- getLastError() {
284
- var _a, _b;
285
- if (!this.hasAPI()) return { code: "-1", message: "SCORM API not found." };
286
- const errorCode = this.scormVersionFound === "2004" ? this.scormAPI.GetLastError() : this.scormAPI.LMSGetLastError();
287
- if (errorCode === SCORM_NO_ERROR || errorCode === 0 || errorCode === "0") {
288
- return { code: SCORM_NO_ERROR, message: "No error." };
289
- }
290
- const errorMessage = this.scormVersionFound === "2004" ? this.scormAPI.GetErrorString(errorCode.toString()) : this.scormAPI.LMSGetErrorString(errorCode.toString());
291
- const diagnostic = this.scormVersionFound === "2004" ? this.scormAPI.GetDiagnostic(errorCode.toString()) : this.scormAPI.LMSGetDiagnostic(errorCode.toString());
292
- return {
293
- code: errorCode.toString(),
294
- message: (_a = errorMessage == null ? void 0 : errorMessage.toString()) != null ? _a : "Unknown error.",
295
- diagnostic: (_b = diagnostic == null ? void 0 : diagnostic.toString()) != null ? _b : void 0
296
- };
297
- }
298
- formatCMITime(totalSeconds) {
299
- const pad = (num, size = 2) => num.toString().padStart(size, "0");
300
- if (this.scormVersionFound === "2004") {
301
- const hours = Math.floor(totalSeconds / 3600);
302
- const minutes = Math.floor(totalSeconds % 3600 / 60);
303
- const seconds = parseFloat((totalSeconds % 60).toFixed(2));
304
- let timeString = "PT";
305
- if (hours > 0) timeString += `${hours}H`;
306
- if (minutes > 0 || hours > 0 && seconds > 0) {
307
- timeString += `${minutes}M`;
308
- }
309
- if (seconds > 0 || timeString === "PT") {
310
- timeString += `${seconds}S`;
311
- }
312
- return timeString === "PT" ? "PT0S" : timeString;
313
- } else {
314
- const hours = Math.floor(totalSeconds / 3600);
315
- const minutes = Math.floor(totalSeconds % 3600 / 60);
316
- const secondsOnly = Math.floor(totalSeconds % 60);
317
- const centiseconds = Math.floor((totalSeconds - Math.floor(totalSeconds)) * 100);
318
- return `${pad(hours, 4)}:${pad(minutes)}:${pad(secondsOnly)}.${pad(centiseconds)}`;
319
- }
320
- }
321
- };
322
-
323
- // src/services/QuizEngine.ts
324
- var QuizEngine = class {
325
- // Stores time in seconds
326
- constructor(options) {
327
- this.userAnswers = /* @__PURE__ */ new Map();
328
- this.currentQuestionIndex = 0;
329
- this.timerId = null;
330
- this.timeLeftInSeconds = null;
331
- this.scormService = null;
332
- this.quizResultState = { scormStatus: "idle" };
333
- this.questionStartTime = null;
334
- this.questionTimings = /* @__PURE__ */ new Map();
335
- var _a, _b, _c, _d, _e;
336
- this.config = options.config;
337
- this.callbacks = options.callbacks || {};
338
- this.questions = ((_a = this.config.settings) == null ? void 0 : _a.shuffleQuestions) ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
339
- this.overallStartTime = Date.now();
340
- if (((_b = this.config.settings) == null ? void 0 : _b.timeLimitMinutes) && this.config.settings.timeLimitMinutes > 0) {
341
- this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
342
- }
343
- if ((_c = this.config.settings) == null ? void 0 : _c.scorm) {
344
- this.quizResultState.scormStatus = "initializing";
345
- this.scormService = new SCORMService(this.config.settings.scorm);
346
- if (this.scormService.hasAPI()) {
347
- const initResult = this.scormService.initialize();
348
- if (initResult.success) {
349
- this.quizResultState.scormStatus = "initialized";
350
- this.quizResultState.studentName = initResult.studentName;
351
- } else {
352
- this.quizResultState.scormStatus = "error";
353
- this.quizResultState.scormError = initResult.error || "SCORM initialization failed.";
354
- }
355
- } else {
356
- this.quizResultState.scormStatus = "no_api";
357
- }
358
- }
359
- const initialQ = this.getCurrentQuestion();
360
- if (initialQ) {
361
- this.questionStartTime = Date.now();
362
- }
363
- if (this.callbacks.onQuizStart) {
364
- this.callbacks.onQuizStart({
365
- initialQuestion: initialQ,
366
- currentQuestionNumber: this.getCurrentQuestionNumber(),
367
- totalQuestions: this.getTotalQuestions(),
368
- timeLimitInSeconds: this.timeLeftInSeconds,
369
- scormStatus: this.quizResultState.scormStatus,
370
- studentName: this.quizResultState.studentName
371
- });
372
- }
373
- if (this.timeLeftInSeconds !== null) {
374
- this.startTimer();
375
- }
376
- (_e = (_d = this.callbacks).onQuestionChange) == null ? void 0 : _e.call(_d, initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
377
- }
378
- _recordCurrentQuestionTime() {
379
- if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
380
- const currentQId = this.questions[this.currentQuestionIndex].id;
381
- const elapsedMs = Date.now() - this.questionStartTime;
382
- const currentTotalTime = this.questionTimings.get(currentQId) || 0;
383
- this.questionTimings.set(currentQId, currentTotalTime + elapsedMs / 1e3);
384
- }
385
- this.questionStartTime = null;
386
- }
387
- startTimer() {
388
- if (this.timerId !== null) {
389
- clearInterval(this.timerId);
390
- }
391
- this.timerId = setInterval(() => this.handleTick(), 1e3);
392
- }
393
- stopTimer() {
394
- if (this.timerId !== null) {
395
- clearInterval(this.timerId);
396
- this.timerId = null;
397
- }
398
- }
399
- handleTick() {
400
- var _a, _b, _c, _d;
401
- if (this.timeLeftInSeconds === null) return;
402
- if (this.timeLeftInSeconds > 0) {
403
- this.timeLeftInSeconds--;
404
- (_b = (_a = this.callbacks).onTimeTick) == null ? void 0 : _b.call(_a, this.timeLeftInSeconds);
405
- }
406
- if (this.timeLeftInSeconds <= 0) {
407
- this.stopTimer();
408
- (_d = (_c = this.callbacks).onQuizTimeUp) == null ? void 0 : _d.call(_c);
409
- this.calculateResults().then((results) => {
410
- });
411
- }
412
- }
413
- getTimeLeftInSeconds() {
414
- return this.timeLeftInSeconds;
415
- }
416
- getCurrentQuestion() {
417
- return this.questions[this.currentQuestionIndex] || null;
418
- }
419
- getCurrentQuestionNumber() {
420
- return this.currentQuestionIndex + 1;
421
- }
422
- getTotalQuestions() {
423
- return this.questions.length;
424
- }
425
- submitAnswer(questionId, answer) {
426
- var _a, _b;
427
- this.userAnswers.set(questionId, answer);
428
- const question = this.questions.find((q) => q.id === questionId);
429
- if (question) {
430
- (_b = (_a = this.callbacks).onAnswerSubmit) == null ? void 0 : _b.call(_a, question, answer);
431
- } else {
432
- console.warn(`QuizEngine: Question with id ${questionId} not found for onAnswerSubmit.`);
433
- }
434
- }
435
- getUserAnswer(questionId) {
436
- return this.userAnswers.get(questionId);
437
- }
438
- nextQuestion() {
439
- var _a, _b;
440
- this._recordCurrentQuestionTime();
441
- if (this.currentQuestionIndex < this.questions.length - 1) {
442
- this.currentQuestionIndex++;
443
- const currentQ = this.getCurrentQuestion();
444
- this.questionStartTime = Date.now();
445
- (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
446
- return currentQ;
447
- }
448
- return null;
449
- }
450
- previousQuestion() {
451
- var _a, _b;
452
- this._recordCurrentQuestionTime();
453
- if (this.currentQuestionIndex > 0) {
454
- this.currentQuestionIndex--;
455
- const currentQ = this.getCurrentQuestion();
456
- this.questionStartTime = Date.now();
457
- (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
458
- return currentQ;
459
- }
460
- return null;
461
- }
462
- goToQuestion(index) {
463
- var _a, _b;
464
- if (index >= 0 && index < this.questions.length && index !== this.currentQuestionIndex) {
465
- this._recordCurrentQuestionTime();
466
- this.currentQuestionIndex = index;
467
- const currentQ = this.getCurrentQuestion();
468
- this.questionStartTime = Date.now();
469
- (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
470
- return currentQ;
471
- }
472
- return this.getCurrentQuestion();
473
- }
474
- isQuizFinished() {
475
- return this.quizResultState.score !== void 0;
476
- }
477
- async _sendResultsToWebhook(results) {
478
- var _a;
479
- if (!((_a = this.config.settings) == null ? void 0 : _a.webhookUrl)) {
480
- results.webhookStatus = "idle";
481
- return;
482
- }
483
- results.webhookStatus = "sending";
484
- try {
485
- const response = await fetch(this.config.settings.webhookUrl, {
486
- method: "POST",
487
- headers: { "Content-Type": "application/json" },
488
- body: JSON.stringify(results)
489
- });
490
- if (response.ok) {
491
- results.webhookStatus = "success";
492
- } else {
493
- results.webhookStatus = "error";
494
- results.webhookError = `Webhook returned status: ${response.status} ${response.statusText}`;
495
- try {
496
- const errorBody = await response.text();
497
- results.webhookError += ` - Body: ${errorBody.substring(0, 200)}`;
498
- } catch (e) {
499
- }
500
- }
501
- } catch (error) {
502
- results.webhookStatus = "error";
503
- results.webhookError = error instanceof Error ? `Fetch error: ${error.message}` : "Unknown webhook error.";
504
- }
505
- }
506
- _sendResultsToSCORM(results) {
507
- var _a, _b, _c, _d, _e, _f, _g;
508
- if (!this.scormService || !this.scormService.hasAPI() || this.quizResultState.scormStatus === "no_api") {
509
- results.scormStatus = this.quizResultState.scormStatus || "idle";
510
- return;
511
- }
512
- if (this.quizResultState.scormStatus === "error" && ((_a = this.quizResultState.scormError) == null ? void 0 : _a.includes("initialization failed"))) {
513
- results.scormStatus = "error";
514
- results.scormError = this.quizResultState.scormError;
515
- return;
516
- }
517
- results.scormStatus = "sending_data";
518
- try {
519
- this.scormService.setScore(results.score, results.maxScore, 0);
520
- let lessonStatusSetting = "completed";
521
- if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) !== void 0 && ((_c = this.config.settings) == null ? void 0 : _c.passingScorePercent) !== null) {
522
- lessonStatusSetting = results.passed ? "passed" : "failed";
523
- } else if ((_e = (_d = this.config.settings) == null ? void 0 : _d.scorm) == null ? void 0 : _e.setCompletionOnFinish) {
524
- lessonStatusSetting = "completed";
525
- }
526
- this.scormService.setLessonStatus(lessonStatusSetting, results.passed);
527
- if (results.totalTimeSpentSeconds !== void 0 && this.scormService.formatCMITime) {
528
- const cmiTime = this.scormService.formatCMITime(results.totalTimeSpentSeconds);
529
- const sessionTimeVar = ((_g = (_f = this.config.settings) == null ? void 0 : _f.scorm) == null ? void 0 : _g.sessionTimeVar) || (this.scormService.getSCORMVersion() === "2004" ? "cmi.session_time" : "cmi.core.session_time");
530
- if (sessionTimeVar) this.scormService.setValue(sessionTimeVar, cmiTime);
531
- }
532
- const commitResult = this.scormService.commit();
533
- if (commitResult.success) {
534
- results.scormStatus = "committed";
535
- } else {
536
- results.scormStatus = "error";
537
- results.scormError = commitResult.error || "SCORM commit failed.";
538
- }
539
- } catch (e) {
540
- results.scormStatus = "error";
541
- results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
542
- }
543
- }
544
- _calculateMetadataPerformance() {
545
- const loPerformanceMap = /* @__PURE__ */ new Map();
546
- const categoryPerformanceMap = /* @__PURE__ */ new Map();
547
- const topicPerformanceMap = /* @__PURE__ */ new Map();
548
- const difficultyPerformanceMap = /* @__PURE__ */ new Map();
549
- const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
550
- const updateMap = (map, key, points, isCorrect) => {
551
- if (key === void 0 || key === null || key.trim() === "") return;
552
- const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
553
- current.totalQuestions++;
554
- current.maxPoints += points;
555
- if (isCorrect) {
556
- current.correctQuestions++;
557
- current.pointsEarned += points;
558
- }
559
- map.set(key, current);
560
- };
561
- this.questions.forEach((q) => {
562
- const qResult = this.userAnswers.get(q.id);
563
- const { isCorrect } = this.evaluateQuestion(q, qResult || null);
564
- const pointsForThisQuestion = q.points !== void 0 ? q.points : 0;
565
- updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
566
- updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
567
- updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
568
- updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
569
- updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
570
- });
571
- const formatPerformanceArray = (map, keyName) => {
572
- return Array.from(map.entries()).map(([key, data]) => ({
573
- [keyName]: key,
574
- totalQuestions: data.totalQuestions,
575
- correctQuestions: data.correctQuestions,
576
- pointsEarned: data.pointsEarned,
577
- maxPoints: data.maxPoints,
578
- percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
579
- }));
580
- };
581
- return {
582
- performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
583
- performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
584
- performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
585
- performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
586
- performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
587
- };
588
- }
589
- async calculateResults() {
590
- var _a, _b, _c, _d;
591
- this.stopTimer();
592
- this._recordCurrentQuestionTime();
593
- let totalScore = 0;
594
- let maxScore = 0;
595
- const questionResultsArray = [];
596
- let accumulatedTotalTimeSpent = 0;
597
- for (const question of this.questions) {
598
- const userAnswer = this.userAnswers.get(question.id) || null;
599
- const questionPoints = question.points !== void 0 ? question.points : 0;
600
- maxScore += questionPoints;
601
- const { isCorrect, correctAnswer, pointsEarned } = this.evaluateQuestion(question, userAnswer);
602
- totalScore += pointsEarned;
603
- const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
604
- accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
605
- questionResultsArray.push({
606
- questionId: question.id,
607
- isCorrect,
608
- pointsEarned,
609
- userAnswer,
610
- correctAnswer,
611
- timeSpentSeconds: timeSpentOnThisQuestion
612
- });
613
- }
614
- const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
615
- let passed = void 0;
616
- if (((_a = this.config.settings) == null ? void 0 : _a.passingScorePercent) !== void 0 && this.config.settings.passingScorePercent !== null) {
617
- passed = percentage >= this.config.settings.passingScorePercent;
618
- }
619
- const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
620
- const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
621
- const metadataPerformance = this._calculateMetadataPerformance();
622
- const finalResults = __spreadValues({
623
- score: totalScore,
624
- maxScore,
625
- percentage,
626
- answers: this.userAnswers,
627
- questionResults: questionResultsArray,
628
- passed,
629
- webhookStatus: "idle",
630
- scormStatus: this.quizResultState.scormStatus || "idle",
631
- scormError: this.quizResultState.scormError,
632
- studentName: this.quizResultState.studentName,
633
- totalTimeSpentSeconds: totalQuizTimeSpentSeconds,
634
- averageTimePerQuestionSeconds
635
- }, metadataPerformance);
636
- this.quizResultState = __spreadValues(__spreadValues({}, this.quizResultState), finalResults);
637
- if ((_b = this.config.settings) == null ? void 0 : _b.scorm) {
638
- this._sendResultsToSCORM(finalResults);
639
- }
640
- await this._sendResultsToWebhook(finalResults);
641
- (_d = (_c = this.callbacks).onQuizFinish) == null ? void 0 : _d.call(_c, finalResults);
642
- return finalResults;
643
- }
644
- evaluateQuestion(question, answer) {
645
- let isCorrect = false;
646
- let correctAnswerValue = null;
647
- const points = question.points !== void 0 ? question.points : 0;
648
- switch (question.questionType) {
649
- case "multiple_choice":
650
- correctAnswerValue = question.correctAnswerId;
651
- isCorrect = answer === correctAnswerValue;
652
- break;
653
- case "multiple_response":
654
- const mrq = question;
655
- correctAnswerValue = mrq.correctAnswerIds;
656
- if (Array.isArray(answer) && Array.isArray(correctAnswerValue)) {
657
- const userAnswerSet = new Set(answer);
658
- const correctAnswerSet = new Set(correctAnswerValue);
659
- isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
660
- }
661
- break;
662
- case "fill_in_the_blanks":
663
- const fitbq = question;
664
- correctAnswerValue = {};
665
- fitbq.answers.forEach((ans) => {
666
- correctAnswerValue[ans.blankId] = ans.acceptedValues;
667
- });
668
- if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
669
- const userAnswerMap = answer;
670
- isCorrect = fitbq.answers.every((correctAnsDef) => {
671
- var _a;
672
- const userValForBlank = (_a = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a.trim();
673
- const acceptedValsForBlank = correctAnsDef.acceptedValues.map((v) => v.trim());
674
- const caseSensitive = fitbq.isCaseSensitive === void 0 ? false : fitbq.isCaseSensitive;
675
- if (userValForBlank === void 0) return false;
676
- return caseSensitive ? acceptedValsForBlank.some((accVal) => accVal === userValForBlank) : acceptedValsForBlank.some((accVal) => accVal.toLowerCase() === userValForBlank.toLowerCase());
677
- });
678
- }
679
- break;
680
- case "drag_and_drop":
681
- const dndq = question;
682
- correctAnswerValue = {};
683
- dndq.answerMap.forEach((map) => {
684
- correctAnswerValue[map.draggableId] = map.dropZoneId;
685
- });
686
- if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
687
- const userAnswerMap = answer;
688
- const correctPairsCount = dndq.answerMap.length;
689
- isCorrect = dndq.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId) && Object.keys(userAnswerMap).length === correctPairsCount;
690
- }
691
- break;
692
- case "true_false":
693
- const tfq = question;
694
- correctAnswerValue = tfq.correctAnswer;
695
- let tfAnswer = answer;
696
- if (typeof answer === "string") tfAnswer = answer.toLowerCase() === "true";
697
- if (typeof tfAnswer === "boolean") isCorrect = tfAnswer === tfq.correctAnswer;
698
- break;
699
- case "short_answer":
700
- const saq = question;
701
- correctAnswerValue = saq.acceptedAnswers;
702
- if (typeof answer === "string") {
703
- const userAnswerTrimmed = answer.trim();
704
- const caseSensitive = saq.isCaseSensitive === void 0 ? false : saq.isCaseSensitive;
705
- isCorrect = saq.acceptedAnswers.some((accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase());
706
- }
707
- break;
708
- case "numeric":
709
- const nq = question;
710
- correctAnswerValue = { answer: nq.answer, tolerance: nq.tolerance };
711
- if (typeof answer === "string" || typeof answer === "number") {
712
- const userAnswerNum = parseFloat(String(answer));
713
- if (!isNaN(userAnswerNum)) {
714
- isCorrect = nq.tolerance !== void 0 && nq.tolerance !== null ? Math.abs(userAnswerNum - nq.answer) <= nq.tolerance : userAnswerNum === nq.answer;
715
- }
716
- }
717
- break;
718
- case "sequence":
719
- const seqQ = question;
720
- correctAnswerValue = seqQ.correctOrder;
721
- if (Array.isArray(answer) && Array.isArray(seqQ.correctOrder) && answer.length === seqQ.correctOrder.length) {
722
- isCorrect = answer.every((itemId, index) => itemId === seqQ.correctOrder[index]);
723
- }
724
- break;
725
- case "matching":
726
- const matQ = question;
727
- correctAnswerValue = matQ.correctAnswerMap.reduce((acc, curr) => {
728
- acc[curr.promptId] = curr.optionId;
729
- return acc;
730
- }, {});
731
- if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
732
- const userAnswerMap = answer;
733
- const correctMapPairsCount = matQ.correctAnswerMap.length;
734
- isCorrect = matQ.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId) && Object.keys(userAnswerMap).length === correctMapPairsCount;
735
- }
736
- break;
737
- case "hotspot":
738
- const hsQ = question;
739
- correctAnswerValue = hsQ.correctHotspotIds;
740
- if (typeof answer === "string") {
741
- isCorrect = hsQ.correctHotspotIds.includes(answer);
742
- } else if (Array.isArray(answer)) {
743
- const userAnswerSet = new Set(answer);
744
- const correctAnswerSet = new Set(hsQ.correctHotspotIds);
745
- isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
746
- }
747
- break;
748
- case "blockly_programming":
749
- case "scratch_programming":
750
- const progQ = question;
751
- correctAnswerValue = progQ.solutionGeneratedCode;
752
- if (typeof answer === "string" && typeof correctAnswerValue === "string") {
753
- if (typeof window !== "undefined" && window.Blockly) {
754
- const LocalBlockly = window.Blockly;
755
- let generatedUserCode = "";
756
- try {
757
- const tempWorkspace = new LocalBlockly.Workspace();
758
- const dom = LocalBlockly.Xml.textToDom(answer);
759
- LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
760
- generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
761
- generatedUserCode = generatedUserCode.split("\\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\\n");
762
- tempWorkspace.dispose();
763
- } catch (e) {
764
- console.error(`Error generating code from user's ${progQ.questionType} XML for evaluation:`, e);
765
- generatedUserCode = `Error generating code: ${e}`;
766
- }
767
- isCorrect = generatedUserCode === correctAnswerValue;
768
- } else {
769
- console.warn(`Blockly library not available in QuizEngine for ${progQ.questionType} code generation during evaluation. XML will be stored, but code comparison skipped.`);
770
- isCorrect = false;
771
- }
772
- } else {
773
- isCorrect = false;
774
- }
775
- break;
776
- default:
777
- const _exhaustiveCheck = question;
778
- console.warn("Unsupported question type in QuizEngine evaluation:", _exhaustiveCheck);
779
- isCorrect = false;
780
- }
781
- return { isCorrect, correctAnswer: correctAnswerValue, pointsEarned: isCorrect ? points : 0 };
782
- }
783
- getElapsedTime() {
784
- return Date.now() - this.overallStartTime;
785
- }
786
- destroy() {
787
- this.stopTimer();
788
- this._recordCurrentQuestionTime();
789
- if (this.scormService && this.scormService.hasAPI()) {
790
- if (this.quizResultState.scormStatus === "initialized" || this.quizResultState.scormStatus === "committed" || this.quizResultState.scormStatus === "sending_data") {
791
- const termResult = this.scormService.terminate();
792
- if (termResult.success) {
793
- this.quizResultState.scormStatus = "terminated";
794
- } else {
795
- this.quizResultState.scormStatus = "error";
796
- this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
797
- }
798
- }
799
- }
800
- this.scormService = null;
801
- }
802
- };
803
-
804
- // src/services/HTMLLauncherGenerator.ts
805
- var escapeAttribute = (unsafe) => {
806
- if (typeof unsafe !== "string") return "";
807
- return unsafe.replace(/"/g, "&quot;");
808
- };
809
- var generateLauncherHTML = (quizConfig, libraryJSPath = "lib/interactive-quiz-kit.esm.js", quizDataPath = "quiz_data.json", blocklyCSSPath = "blockly-styles.css", title) => {
810
- const pageTitle = escapeAttribute(title || quizConfig.title || "Interactive Quiz");
811
- const relLibraryJSPath = libraryJSPath.startsWith("./") ? libraryJSPath : `./${libraryJSPath}`;
812
- const relQuizDataPath = quizDataPath.startsWith("./") ? quizDataPath : `./${quizDataPath}`;
813
- const relBlocklyCSSPath = blocklyCSSPath.startsWith("./") ? blocklyCSSPath : `./${blocklyCSSPath}`;
814
- return `<!DOCTYPE html>
815
- <html lang="en">
816
- <head>
817
- <meta charset="UTF-8">
818
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
819
- <title>${pageTitle}</title>
820
- <style>
821
- body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; background-color: #f0f2f5; color: #1f2937; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding-top: 20px; box-sizing: border-box; }
822
- #root { width: 100%; max-width: 900px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); padding: 20px; }
823
- .loading-spinner {
824
- border: 4px solid #e5e7eb; border-top: 4px solid #3b82f6;
825
- border-radius: 50%; width: 40px; height: 40px;
826
- animation: spin 1s linear infinite;
827
- margin: 60px auto 20px auto;
828
- }
829
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
830
- .status-message { text-align: center; padding: 20px; margin-top: 10px; color: #4b5563; }
831
- /* Basic button styling for exit (if QuizPlayer doesn't provide one post-completion) */
832
- .exit-button { display: block; margin: 20px auto; padding: 10px 20px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
833
- .exit-button:hover { background-color: #2563eb; }
834
- </style>
835
- <link rel="stylesheet" href="${escapeAttribute(relBlocklyCSSPath)}">
836
- <script type="importmap">
837
- {
838
- "imports": {
839
- "react": "https://esm.sh/react@18.3.1",
840
- "react-dom/client": "https://esm.sh/react-dom@18.3.1/client"
841
- }
842
- }
843
- </script>
844
- </head>
845
- <body>
846
- <div id="root">
847
- <div class="loading-spinner" aria-label="Loading quiz content"></div>
848
- <p class="status-message" role="status">Loading Quiz...</p>
849
- </div>
850
-
851
- <script type="module">
852
- import React from 'react';
853
- import ReactDOM from 'react-dom/client';
854
- // The library path must be relative to this HTML file within the SCORM package
855
- import { QuizPlayer } from '${escapeAttribute(relLibraryJSPath)}';
856
-
857
- async function loadQuizData() {
858
- try {
859
- // quizDataPath is relative to this HTML file
860
- const response = await fetch('${escapeAttribute(relQuizDataPath)}');
861
- if (!response.ok) {
862
- throw new Error('Failed to load quiz data: Status ' + response.status + ' - ' + response.statusText + '. Ensure quiz_data.json is in the same directory as the launcher HTML or the path is correct.');
863
- }
864
- return await response.json();
865
- } catch (error) {
866
- console.error("Error loading quiz data:", error);
867
- showStatusMessage('Error: Could not load quiz configuration. ' + error.message, true);
868
- return null;
869
- }
870
- }
871
-
872
- function showStatusMessage(message, isError = false) {
873
- const rootEl = document.getElementById('root');
874
- if (rootEl) {
875
- rootEl.innerHTML = ''; // Clear previous content (like spinner)
876
- const messageEl = document.createElement('p');
877
- messageEl.textContent = message;
878
- messageEl.className = 'status-message';
879
- if(isError) {
880
- messageEl.style.color = '#ef4444'; // Red for errors
881
- messageEl.setAttribute('role', 'alert');
882
- } else {
883
- messageEl.setAttribute('role', 'status');
884
- }
885
- rootEl.appendChild(messageEl);
886
- }
887
- }
888
-
889
- function showCompletionScreen(message) {
890
- const rootEl = document.getElementById('root');
891
- if (rootEl) {
892
- rootEl.innerHTML = ''; // Clear quiz player
893
- const messageEl = document.createElement('p');
894
- messageEl.textContent = message;
895
- messageEl.className = 'status-message';
896
- rootEl.appendChild(messageEl);
897
-
898
- // Optional: Add a manual close button if LMS doesn't handle it or for testing
899
- // const closeButton = document.createElement('button');
900
- // closeButton.textContent = 'Close Quiz';
901
- // closeButton.className = 'exit-button';
902
- // closeButton.onclick = () => {
903
- // try { window.close(); } catch(e) { console.warn("Could not close window programmatically."); }
904
- // // SCORM termination should have happened in QuizPlayer/QuizEngine
905
- // };
906
- // rootEl.appendChild(closeButton);
907
- }
908
- }
909
-
910
- async function main() {
911
- const quizConfigData = await loadQuizData();
912
- if (!quizConfigData) {
913
- return; // Error message already shown by loadQuizData
914
- }
915
-
916
- // Check if QuizPlayer is available
917
- if (typeof QuizPlayer !== 'function') {
918
- showStatusMessage('Error: QuizPlayer component not found. Check library bundle.', true);
919
- return;
920
- }
921
-
922
- const App = () => {
923
- const handleQuizComplete = (result) => {
924
- console.log("Quiz Complete (SCORM Launcher):", result);
925
- let completionMessage = 'Quiz completed. ';
926
- if (result.passed !== undefined) {
927
- completionMessage += result.passed ? 'You passed!' : 'You did not pass.';
928
- }
929
- completionMessage += ' You may now close this window, or your Learning Management System will manage navigation.';
930
- showCompletionScreen(completionMessage);
931
- // SCORM termination logic should be handled by QuizEngine/QuizPlayer via SCORMService.destroy()
932
- };
933
-
934
- const handleExitQuiz = () => {
935
- console.log("Quiz Exited (SCORM Launcher)");
936
- showCompletionScreen('Quiz exited. You may close this window or your Learning Management System will manage navigation.');
937
- // SCORM termination logic should be handled by QuizEngine/QuizPlayer via SCORMService.destroy()
938
- };
939
-
940
- // Fallback if quizConfigData is malformed but loaded
941
- if (!quizConfigData.questions || !Array.isArray(quizConfigData.questions)) {
942
- showStatusMessage('Error: Quiz data is malformed. "questions" array is missing.', true);
943
- return React.createElement('div', null, 'Error rendering quiz.');
944
- }
945
-
946
-
947
- return React.createElement(QuizPlayer, {
948
- quizConfig: quizConfigData,
949
- onQuizComplete: handleQuizComplete,
950
- onExitQuiz: handleExitQuiz,
951
- // You might pass isScormContext=true if QuizPlayer needs it
952
- });
953
- };
954
-
955
- const rootElement = document.getElementById('root');
956
- if (rootElement) {
957
- rootElement.innerHTML = ''; // Clear loading message before rendering React app
958
- const reactRoot = ReactDOM.createRoot(rootElement);
959
- reactRoot.render(React.createElement(React.StrictMode, null, React.createElement(App)));
960
- } else {
961
- console.error('Root element (#root) not found for SCORM launcher.');
962
- document.body.innerHTML = '<p style="color: red; text-align: center; padding: 20px;">Critical Error: Root HTML element not found.</p>';
963
- }
964
- }
965
-
966
- // Ensure DOM is ready before trying to find #root
967
- if (document.readyState === 'loading') {
968
- document.addEventListener('DOMContentLoaded', main);
969
- } else {
970
- main();
971
- }
972
- </script>
973
- </body>
974
- </html>
975
- `;
976
- };
977
-
978
- // src/services/SCORMManifestGenerator.ts
979
- var escapeXML = (unsafe) => {
980
- if (typeof unsafe !== "string") return "";
981
- return unsafe.replace(/[<>&'"]/g, (c) => {
982
- switch (c) {
983
- case "<":
984
- return "&lt;";
985
- case ">":
986
- return "&gt;";
987
- case "&":
988
- return "&amp;";
989
- case "'":
990
- return "&apos;";
991
- case '"':
992
- return "&quot;";
993
- default:
994
- return c;
995
- }
996
- });
997
- };
998
- var generateSCORMManifest = (quizConfig, scormVersion, launcherFile = "index.html", libraryJSPath = "lib/interactive-quiz-kit.esm.js", quizDataPath = "quiz_data.json", blocklyCSSPath = "blockly-styles.css") => {
999
- var _a;
1000
- const uniqueId = `iqk_${quizConfig.id.replace(/[^a-zA-Z0-9_]/g, "_")}`;
1001
- const organizationId = `ORG-${uniqueId}`;
1002
- const itemId = `ITEM-${uniqueId}`;
1003
- const resourceId = `RES-${uniqueId}`;
1004
- const quizTitle = escapeXML(quizConfig.title);
1005
- const passingScore = (_a = quizConfig.settings) == null ? void 0 : _a.passingScorePercent;
1006
- const effectiveScormVersion = scormVersion;
1007
- const schemaVersion = effectiveScormVersion === "2004" ? "2004 4th Edition" : "1.2";
1008
- const adlcpNamespace = effectiveScormVersion === "2004" ? "http://www.adlnet.org/xsd/adlcp_v1p3" : "http://www.adlnet.org/xsd/adlcp_rootv1p2";
1009
- const imsmdNamespace = effectiveScormVersion === "2004" ? "http://www.imsglobal.org/xsd/imsmd_v1p2" : "http://www.imsglobal.org/xsd/imsmd_rootv1p2p1";
1010
- const xsiSchemaLocation = effectiveScormVersion === "2004" ? "http://www.imsglobal.org/xsd/imscp_v1p1 imscp_v1p1.xsd http://www.adlnet.org/xsd/adlcp_v1p3 adlcp_v1p3.xsd http://www.imsglobal.org/xsd/imsmd_v1p2 imsmd_v1p2p2.xsd" : "http://www.imsglobal.org/xsd/imscp_v1p1 imscp_v1p1.xsd http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd http://www.imsglobal.org/xsd/imsmd_rootv1p2p1 imsmd_rootv1p2p1.xsd";
1011
- const files = [
1012
- launcherFile,
1013
- libraryJSPath,
1014
- quizDataPath,
1015
- blocklyCSSPath
1016
- ].map((file) => `<file href="${escapeXML(file)}"/>`).join("\n ");
1017
- const manifestHeader = effectiveScormVersion === "2004" ? `<?xml version="1.0" encoding="UTF-8"?>
1018
- <manifest identifier="${uniqueId}-MANIFEST" version="1.0"
1019
- xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
1020
- xmlns:adlcp="${adlcpNamespace}"
1021
- xmlns:imsmd="${imsmdNamespace}"
1022
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1023
- xsi:schemaLocation="${xsiSchemaLocation}">` : (
1024
- // SCORM 1.2
1025
- `<?xml version="1.0" encoding="UTF-8"?>
1026
- <manifest identifier="${uniqueId}-MANIFEST" version="1.2"
1027
- xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
1028
- xmlns:adlcp="${adlcpNamespace}"
1029
- xmlns:imsmd="${imsmdNamespace}"
1030
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1031
- xsi:schemaLocation="${xsiSchemaLocation}">`
1032
- );
1033
- const organizationStructure = effectiveScormVersion === "2004" ? `<organizations default="${organizationId}">
1034
- <organization identifier="${organizationId}" structure="hierarchical">
1035
- <title>${quizTitle}</title>
1036
- <item identifier="${itemId}" identifierref="${resourceId}">
1037
- <title>${quizTitle}</title>
1038
- ${passingScore !== void 0 ? `<adlcp:masteryscore>${passingScore}</adlcp:masteryscore>` : ""}
1039
- </item>
1040
- </organization>
1041
- </organizations>` : (
1042
- // SCORM 1.2
1043
- `<organizations default="${organizationId}">
1044
- <organization identifier="${organizationId}">
1045
- <title>${quizTitle}</title>
1046
- <item identifier="${itemId}" identifierref="${resourceId}" isvisible="true">
1047
- <title>${quizTitle}</title>
1048
- ${passingScore !== void 0 ? `<adlcp:masteryscore>${passingScore}</adlcp:masteryscore>` : ""}
1049
- </item>
1050
- </organization>
1051
- </organizations>`
1052
- );
1053
- const resourceScormType = effectiveScormVersion === "2004" ? "sco" : "sco";
1054
- return `${manifestHeader}
1055
- <metadata>
1056
- <schema>ADL SCORM</schema>
1057
- <schemaversion>${schemaVersion}</schemaversion>
1058
- <imsmd:lom>
1059
- <imsmd:general>
1060
- <imsmd:title>
1061
- <imsmd:langstring xml:lang="en">${quizTitle}</imsmd:langstring>
1062
- </imsmd:title>
1063
- ${quizConfig.description ? `<imsmd:description><imsmd:langstring xml:lang="en">${escapeXML(quizConfig.description)}</imsmd:langstring></imsmd:description>` : ""}
1064
- </imsmd:general>
1065
- </imsmd:lom>
1066
- </metadata>
1067
- ${organizationStructure}
1068
- <resources>
1069
- <resource identifier="${resourceId}" type="webcontent" adlcp:scormtype="${resourceScormType}" href="${escapeXML(launcherFile)}">
1070
- ${files}
1071
- </resource>
1072
- </resources>
1073
- </manifest>`;
1074
- };
1075
-
1076
- // src/utils/idGenerators.ts
1077
- function generateUniqueId(prefix = "id_") {
1078
- return prefix + Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
1079
- }
1080
-
1081
- // src/services/sampleQuiz.ts
1082
- var trueFalseQ1 = {
1083
- id: generateUniqueId("tfq_"),
1084
- questionType: "true_false",
1085
- prompt: "B\u1EA7u tr\u1EDDi c\xF3 m\xE0u xanh do hi\u1EC7n t\u01B0\u1EE3ng t\xE1n x\u1EA1 Rayleigh.",
1086
- correctAnswer: true,
1087
- points: 10,
1088
- explanation: "T\xE1n x\u1EA1 Rayleigh khi\u1EBFn \xE1nh s\xE1ng xanh t\xE1n x\u1EA1 nhi\u1EC1u h\u01A1n c\xE1c m\xE0u kh\xE1c v\xEC n\xF3 truy\u1EC1n \u0111i d\u01B0\u1EDBi d\u1EA1ng s\xF3ng ng\u1EAFn h\u01A1n, nh\u1ECF h\u01A1n.",
1089
- difficulty: "easy",
1090
- topic: "V\u1EADt l\xFD",
1091
- category: "Khoa h\u1ECDc",
1092
- learningObjective: "Hi\u1EC3u v\u1EC1 quang h\u1ECDc kh\xED quy\u1EC3n c\u01A1 b\u1EA3n."
1093
- };
1094
- var mcq1 = {
1095
- id: generateUniqueId("mcq_"),
1096
- questionType: "multiple_choice",
1097
- prompt: "Th\u1EE7 \u0111\xF4 c\u1EE7a Ph\xE1p l\xE0 g\xEC?",
1098
- options: [
1099
- { id: generateUniqueId("opt_"), text: "Berlin" },
1100
- { id: generateUniqueId("opt_"), text: "Madrid" },
1101
- { id: generateUniqueId("opt_"), text: "Paris" },
1102
- { id: generateUniqueId("opt_"), text: "Rome" }
1103
- ],
1104
- correctAnswerId: "",
1105
- points: 15,
1106
- difficulty: "easy",
1107
- topic: "\u0110\u1ECBa l\xFD",
1108
- category: "Khoa h\u1ECDc X\xE3 h\u1ED9i"
1109
- };
1110
- var parisOption = mcq1.options.find((opt) => opt.text === "Paris");
1111
- if (parisOption) {
1112
- mcq1.correctAnswerId = parisOption.id;
1113
- }
1114
- var mrq1_opt1_id = generateUniqueId("opt_");
1115
- var mrq1_opt2_id = generateUniqueId("opt_");
1116
- var mrq1_opt3_id = generateUniqueId("opt_");
1117
- var mrq1_opt4_id = generateUniqueId("opt_");
1118
- var mrq1_opt5_id = generateUniqueId("opt_");
1119
- var mrq1 = {
1120
- id: generateUniqueId("mrq_"),
1121
- questionType: "multiple_response",
1122
- prompt: "Nh\u1EEFng h\xE0nh tinh n\xE0o sau \u0111\xE2y thu\u1ED9c H\u1EC7 M\u1EB7t Tr\u1EDDi c\xF3 v\xE0nh \u0111ai (rings)?",
1123
- options: [
1124
- { id: mrq1_opt1_id, text: "Sao Th\u1ED5 (Saturn)" },
1125
- { id: mrq1_opt2_id, text: "Sao M\u1ED9c (Jupiter)" },
1126
- { id: mrq1_opt3_id, text: "Sao Thi\xEAn V\u01B0\u01A1ng (Uranus)" },
1127
- { id: mrq1_opt4_id, text: "Sao H\u1EA3i V\u01B0\u01A1ng (Neptune)" },
1128
- { id: mrq1_opt5_id, text: "Tr\xE1i \u0110\u1EA5t (Earth)" }
1129
- ],
1130
- correctAnswerIds: [mrq1_opt1_id, mrq1_opt2_id, mrq1_opt3_id, mrq1_opt4_id],
1131
- points: 20,
1132
- explanation: "Sao Th\u1ED5 n\u1ED5i ti\u1EBFng v\u1EDBi h\u1EC7 th\u1ED1ng v\xE0nh \u0111ai ph\u1EE9c t\u1EA1p. Sao M\u1ED9c, Sao Thi\xEAn V\u01B0\u01A1ng v\xE0 Sao H\u1EA3i V\u01B0\u01A1ng c\u0169ng c\xF3 v\xE0nh \u0111ai, m\u1EB7c d\xF9 ch\xFAng m\u1EDD h\u01A1n v\xE0 kh\xF3 quan s\xE1t h\u01A1n nhi\u1EC1u so v\u1EDBi v\xE0nh \u0111ai c\u1EE7a Sao Th\u1ED5.",
1133
- difficulty: "medium",
1134
- topic: "Thi\xEAn v\u0103n h\u1ECDc",
1135
- category: "Khoa h\u1ECDc"
1136
- };
1137
- var shortAnswerQ1 = {
1138
- id: generateUniqueId("saq_"),
1139
- questionType: "short_answer",
1140
- prompt: "Ng\xF4n ng\u1EEF l\u1EADp tr\xECnh n\xE0o th\u01B0\u1EDDng \u0111\u01B0\u1EE3c s\u1EED d\u1EE5ng ch\u1EE7 y\u1EBFu cho ph\xE1t tri\u1EC3n web ph\xEDa client-side \u0111\u1EC3 l\xE0m cho c\xE1c trang web tr\u1EDF n\xEAn t\u01B0\u01A1ng t\xE1c?",
1141
- acceptedAnswers: ["JavaScript", "Javascript", "javascript", "JS", "js"],
1142
- points: 10,
1143
- explanation: "JavaScript l\xE0 ng\xF4n ng\u1EEF k\u1ECBch b\u1EA3n ch\xEDnh ch\u1EA1y tr\xEAn tr\xECnh duy\u1EC7t c\u1EE7a ng\u01B0\u1EDDi d\xF9ng \u0111\u1EC3 t\u1EA1o ra c\xE1c trang web t\u01B0\u01A1ng t\xE1c.",
1144
- difficulty: "easy",
1145
- topic: "Ph\xE1t tri\u1EC3n Web",
1146
- category: "C\xF4ng ngh\u1EC7",
1147
- isCaseSensitive: false
1148
- };
1149
- var numericQ1 = {
1150
- id: generateUniqueId("nq_"),
1151
- questionType: "numeric",
1152
- prompt: "Nhi\u1EC7t \u0111\u1ED9 s\xF4i c\u1EE7a n\u01B0\u1EDBc \u1EDF \xE1p su\u1EA5t kh\xED quy\u1EC3n ti\xEAu chu\u1EA9n l\xE0 bao nhi\xEAu \u0111\u1ED9 C?",
1153
- answer: 100,
1154
- tolerance: 1,
1155
- points: 10,
1156
- explanation: "N\u01B0\u1EDBc s\xF4i \u1EDF 100 \u0111\u1ED9 C (212 \u0111\u1ED9 F) \u1EDF \xE1p su\u1EA5t kh\xED quy\u1EC3n ti\xEAu chu\u1EA9n.",
1157
- difficulty: "easy",
1158
- topic: "H\xF3a h\u1ECDc",
1159
- category: "Khoa h\u1ECDc"
1160
- };
1161
- var fillInTheBlanksQ1 = {
1162
- id: generateUniqueId("fitb_"),
1163
- questionType: "fill_in_the_blanks",
1164
- prompt: "\u0110i\u1EC1n v\xE0o ch\u1ED7 tr\u1ED1ng \u0111\u1EC3 ho\xE0n th\xE0nh c\xE2u sau:",
1165
- segments: [
1166
- { type: "text", content: "N\u01B0\u1EDBc \u0111\u01B0\u1EE3c c\u1EA5u t\u1EA1o t\u1EEB hai nguy\xEAn t\u1ED1 l\xE0 " },
1167
- { type: "blank", id: "fitb_h" },
1168
- { type: "text", content: " v\xE0 " },
1169
- { type: "blank", id: "fitb_o" },
1170
- { type: "text", content: "." }
1171
- ],
1172
- answers: [
1173
- { blankId: "fitb_h", acceptedValues: ["Hydro", "Hydrogen", "H"] },
1174
- { blankId: "fitb_o", acceptedValues: ["Oxy", "Oxygen", "O"] }
1175
- ],
1176
- isCaseSensitive: false,
1177
- points: 15,
1178
- explanation: "N\u01B0\u1EDBc (H\u2082O) \u0111\u01B0\u1EE3c t\u1EA1o th\xE0nh t\u1EEB hai nguy\xEAn t\u1EED Hydro v\xE0 m\u1ED9t nguy\xEAn t\u1EED Oxy.",
1179
- difficulty: "easy",
1180
- topic: "H\xF3a h\u1ECDc C\u01A1 b\u1EA3n",
1181
- category: "Khoa h\u1ECDc"
1182
- };
1183
- var sequenceQ1_item1_id = generateUniqueId("seqi_");
1184
- var sequenceQ1_item2_id = generateUniqueId("seqi_");
1185
- var sequenceQ1_item3_id = generateUniqueId("seqi_");
1186
- var sequenceQ1_item4_id = generateUniqueId("seqi_");
1187
- var sequenceQ1 = {
1188
- id: generateUniqueId("seqq_"),
1189
- questionType: "sequence",
1190
- prompt: "S\u1EAFp x\u1EBFp c\xE1c h\xE0nh tinh sau theo th\u1EE9 t\u1EF1 t\u1EEB g\u1EA7n M\u1EB7t Tr\u1EDDi nh\u1EA5t \u0111\u1EBFn xa nh\u1EA5t:",
1191
- items: [
1192
- { id: sequenceQ1_item1_id, content: "Sao H\u1ECFa (Mars)" },
1193
- { id: sequenceQ1_item2_id, content: "Tr\xE1i \u0110\u1EA5t (Earth)" },
1194
- { id: sequenceQ1_item3_id, content: "Sao Th\u1EE7y (Mercury)" },
1195
- { id: sequenceQ1_item4_id, content: "Sao Kim (Venus)" }
1196
- ],
1197
- correctOrder: [sequenceQ1_item3_id, sequenceQ1_item4_id, sequenceQ1_item2_id, sequenceQ1_item1_id],
1198
- points: 20,
1199
- explanation: "Th\u1EE9 t\u1EF1 \u0111\xFAng c\u1EE7a c\xE1c h\xE0nh tinh t\u1EEB g\u1EA7n M\u1EB7t Tr\u1EDDi nh\u1EA5t l\xE0: Sao Th\u1EE7y, Sao Kim, Tr\xE1i \u0110\u1EA5t, Sao H\u1ECFa.",
1200
- difficulty: "medium",
1201
- topic: "Thi\xEAn v\u0103n h\u1ECDc",
1202
- category: "Khoa h\u1ECDc"
1203
- };
1204
- var matchingQ1_prompt_vn = generateUniqueId("matp_");
1205
- var matchingQ1_prompt_jp = generateUniqueId("matp_");
1206
- var matchingQ1_prompt_us = generateUniqueId("matp_");
1207
- var matchingQ1_opt_hanoi = generateUniqueId("mato_");
1208
- var matchingQ1_opt_tokyo = generateUniqueId("mato_");
1209
- var matchingQ1_opt_dc = generateUniqueId("mato_");
1210
- var matchingQ1 = {
1211
- id: generateUniqueId("matq_"),
1212
- questionType: "matching",
1213
- prompt: "H\xE3y gh\xE9p m\u1ED7i qu\u1ED1c gia v\u1EDBi th\u1EE7 \u0111\xF4 t\u01B0\u01A1ng \u1EE9ng.",
1214
- prompts: [
1215
- { id: matchingQ1_prompt_vn, content: "Vi\u1EC7t Nam" },
1216
- { id: matchingQ1_prompt_jp, content: "Nh\u1EADt B\u1EA3n" },
1217
- { id: matchingQ1_prompt_us, content: "Hoa K\u1EF3" }
1218
- ],
1219
- options: [
1220
- { id: matchingQ1_opt_tokyo, content: "Tokyo" },
1221
- { id: matchingQ1_opt_hanoi, content: "H\xE0 N\u1ED9i" },
1222
- { id: matchingQ1_opt_dc, content: "Washington D.C." }
1223
- ],
1224
- correctAnswerMap: [
1225
- { promptId: matchingQ1_prompt_vn, optionId: matchingQ1_opt_hanoi },
1226
- { promptId: matchingQ1_prompt_jp, optionId: matchingQ1_opt_tokyo },
1227
- { promptId: matchingQ1_prompt_us, optionId: matchingQ1_opt_dc }
1228
- ],
1229
- points: 15,
1230
- explanation: "H\xE0 N\u1ED9i l\xE0 th\u1EE7 \u0111\xF4 c\u1EE7a Vi\u1EC7t Nam, Tokyo l\xE0 c\u1EE7a Nh\u1EADt B\u1EA3n, v\xE0 Washington D.C. l\xE0 c\u1EE7a Hoa K\u1EF3.",
1231
- difficulty: "easy",
1232
- topic: "\u0110\u1ECBa l\xFD Th\u1EBF gi\u1EDBi",
1233
- shuffleOptions: true
1234
- };
1235
- var dndQ1_drag_apple = generateUniqueId("dndi_");
1236
- var dndQ1_drag_banana = generateUniqueId("dndi_");
1237
- var dndQ1_drag_orange = generateUniqueId("dndi_");
1238
- var dndQ1_drop_red = generateUniqueId("dndz_");
1239
- var dndQ1_drop_yellow = generateUniqueId("dndz_");
1240
- var dndQ1_drop_orange_color = generateUniqueId("dndz_");
1241
- var dragAndDropQ1 = {
1242
- id: generateUniqueId("dndq_"),
1243
- questionType: "drag_and_drop",
1244
- prompt: "K\xE9o c\xE1c lo\u1EA1i tr\xE1i c\xE2y v\xE0o \u0111\xFAng gi\u1ECF m\xE0u c\u1EE7a ch\xFAng (theo logic gh\xE9p n\u1ED1i \u0111\u01A1n gi\u1EA3n).",
1245
- draggableItems: [
1246
- { id: dndQ1_drag_apple, content: "T\xE1o" },
1247
- { id: dndQ1_drag_banana, content: "Chu\u1ED1i" },
1248
- { id: dndQ1_drag_orange, content: "Cam" }
1249
- ],
1250
- dropZones: [
1251
- { id: dndQ1_drop_red, label: "Gi\u1ECF \u0110\u1ECF" },
1252
- { id: dndQ1_drop_yellow, label: "Gi\u1ECF V\xE0ng" },
1253
- { id: dndQ1_drop_orange_color, label: "Gi\u1ECF Cam" }
1254
- ],
1255
- answerMap: [
1256
- { draggableId: dndQ1_drag_apple, dropZoneId: dndQ1_drop_red },
1257
- { draggableId: dndQ1_drag_banana, dropZoneId: dndQ1_drop_yellow },
1258
- { draggableId: dndQ1_drag_orange, dropZoneId: dndQ1_drop_orange_color }
1259
- ],
1260
- points: 15,
1261
- explanation: "T\xE1o th\u01B0\u1EDDng c\xF3 m\xE0u \u0111\u1ECF (gi\u1ECF \u0111\u1ECF), chu\u1ED1i m\xE0u v\xE0ng (gi\u1ECF v\xE0ng), v\xE0 cam c\xF3 m\xE0u cam (gi\u1ECF cam).",
1262
- difficulty: "easy",
1263
- topic: "M\xE0u s\u1EAFc v\xE0 V\u1EADt th\u1EC3",
1264
- backgroundImageUrl: "https://placehold.co/600x200.png",
1265
- imageAltText: "colored baskets"
1266
- };
1267
- var hotspotQ1_engine_left = generateUniqueId("hs_");
1268
- var hotspotQ1_engine_right = generateUniqueId("hs_");
1269
- var hotspotQ1_cockpit = generateUniqueId("hs_");
1270
- var hotspotQ1 = {
1271
- id: generateUniqueId("hsq_"),
1272
- questionType: "hotspot",
1273
- prompt: "Nh\u1EA5p v\xE0o (c\xE1c) \u0111\u1ED9ng c\u01A1 c\u1EE7a m\xE1y bay trong h\xECnh.",
1274
- imageUrl: "https://placehold.co/600x400.png",
1275
- imageAltText: "airplane diagram",
1276
- hotspots: [
1277
- { id: hotspotQ1_engine_left, shape: "rect", coords: [150, 200, 80, 60], description: "\u0110\u1ED9ng c\u01A1 b\xEAn tr\xE1i" },
1278
- { id: hotspotQ1_engine_right, shape: "rect", coords: [370, 200, 80, 60], description: "\u0110\u1ED9ng c\u01A1 b\xEAn ph\u1EA3i" },
1279
- { id: hotspotQ1_cockpit, shape: "rect", coords: [250, 120, 100, 70], description: "Bu\u1ED3ng l\xE1i" }
1280
- ],
1281
- correctHotspotIds: [hotspotQ1_engine_left, hotspotQ1_engine_right],
1282
- points: 15,
1283
- explanation: "M\xE1y bay n\xE0y c\xF3 hai \u0111\u1ED9ng c\u01A1 ch\xEDnh, n\u1EB1m d\u01B0\u1EDBi c\xE1nh.",
1284
- difficulty: "medium",
1285
- topic: "H\xE0ng kh\xF4ng",
1286
- category: "K\u1EF9 thu\u1EADt"
1287
- };
1288
- var blocklyQ1 = {
1289
- id: generateUniqueId("blkq_"),
1290
- questionType: "blockly_programming",
1291
- prompt: "S\u1EED d\u1EE5ng c\xE1c kh\u1ED1i l\u1EC7nh \u0111\u1EC3 t\u1EA1o m\u1ED9t ch\u01B0\u01A1ng tr\xECnh in ra d\xF2ng ch\u1EEF 'Hello, World!' v\xE0o console.",
1292
- points: 25,
1293
- difficulty: "easy",
1294
- topic: "L\u1EADp tr\xECnh C\u01A1 b\u1EA3n",
1295
- category: "C\xF4ng ngh\u1EC7 Th\xF4ng tin",
1296
- toolboxDefinition: `
1297
- <xml xmlns="https://developers.google.com/blockly/xml">
1298
- <category name="Text" colour="%{BKY_TEXTS_HUE}">
1299
- <block type="text"></block>
1300
- <block type="text_print"></block>
1301
- </category>
1302
- </xml>
1303
- `,
1304
- initialWorkspace: `
1305
- <xml xmlns="https://developers.google.com/blockly/xml">
1306
- <block type="text_print" id="${generateUniqueId("blki_")}" x="70" y="70">
1307
- <value name="TEXT">
1308
- <shadow type="text" id="${generateUniqueId("blki_")}">
1309
- <field name="TEXT">abc</field>
1310
- </shadow>
1311
- </value>
1312
- </block>
1313
- </xml>
1314
- `,
1315
- solutionWorkspaceXML: `
1316
- <xml xmlns="https://developers.google.com/blockly/xml">
1317
- <block type="text_print" id="${generateUniqueId("blki_solution_")}" x="70" y="70">
1318
- <value name="TEXT">
1319
- <block type="text" id="${generateUniqueId("blki_text_solution_")}">
1320
- <field name="TEXT">Hello, World!</field>
1321
- </block>
1322
- </value>
1323
- </block>
1324
- </xml>
1325
- `,
1326
- solutionGeneratedCode: "window.alert('Hello, World!');",
1327
- // Normalized JS code
1328
- explanation: "Ch\u01B0\u01A1ng tr\xECnh c\u1EA7n s\u1EED d\u1EE5ng kh\u1ED1i 'print' v\u1EDBi \u0111\u1EA7u v\xE0o l\xE0 kh\u1ED1i v\u0103n b\u1EA3n ch\u1EE9a 'Hello, World!'."
1329
- };
1330
- var scratchQ1 = {
1331
- id: generateUniqueId("scrq_"),
1332
- questionType: "scratch_programming",
1333
- prompt: "D\xF9ng kh\u1ED1i l\u1EC7nh Scratch \u0111\u1EC3 di chuy\u1EC3n nh\xE2n v\u1EADt v\u1EC1 ph\xEDa tr\u01B0\u1EDBc 10 b\u01B0\u1EDBc khi c\u1EDD xanh \u0111\u01B0\u1EE3c click.",
1334
- points: 20,
1335
- difficulty: "easy",
1336
- topic: "L\u1EADp tr\xECnh Scratch",
1337
- category: "C\xF4ng ngh\u1EC7 Th\xF4ng tin",
1338
- toolboxDefinition: `
1339
- <xml xmlns="https://developers.google.com/blockly/xml">
1340
- <category name="Motion" colour="#4C97FF">
1341
- <block type="motion_movesteps"></block>
1342
- </category>
1343
- <category name="Events" colour="#FFBF00">
1344
- <block type="event_whenflagclicked"></block>
1345
- </category>
1346
- </xml>
1347
- `,
1348
- initialWorkspace: `
1349
- <xml xmlns="https://developers.google.com/blockly/xml"></xml>
1350
- `,
1351
- solutionWorkspaceXML: `
1352
- <xml xmlns="https://developers.google.com/blockly/xml">
1353
- <block type="event_whenflagclicked" id="${generateUniqueId("scr_event_")}" x="50" y="50">
1354
- <next>
1355
- <block type="motion_movesteps" id="${generateUniqueId("scr_motion_")}">
1356
- <value name="STEPS">
1357
- <shadow type="math_number">
1358
- <field name="NUM">10</field>
1359
- </shadow>
1360
- </value>
1361
- </block>
1362
- </next>
1363
- </block>
1364
- </xml>
1365
- `,
1366
- solutionGeneratedCode: "whenGreenFlagClicked(() => { move(10); });",
1367
- // Example pseudo-code or JS representation
1368
- explanation: "S\u1EED d\u1EE5ng kh\u1ED1i 'when green flag clicked' t\u1EEB Events v\xE0 kh\u1ED1i 'move 10 steps' t\u1EEB Motion."
1369
- };
1370
- var sampleQuiz = {
1371
- id: "sample-quiz-001",
1372
- title: "Sample Quiz for Testers",
1373
- description: "A short quiz with a few different question types to test the QuizKit functionality.",
1374
- questions: [
1375
- trueFalseQ1,
1376
- mcq1,
1377
- mrq1,
1378
- shortAnswerQ1,
1379
- numericQ1,
1380
- fillInTheBlanksQ1,
1381
- sequenceQ1,
1382
- matchingQ1,
1383
- dragAndDropQ1,
1384
- hotspotQ1,
1385
- blocklyQ1,
1386
- scratchQ1
1387
- // Added Scratch question
1388
- ],
1389
- settings: {
1390
- shuffleQuestions: true,
1391
- shuffleOptions: true,
1392
- showCorrectAnswers: "end_of_quiz",
1393
- passingScorePercent: 70,
1394
- timeLimitMinutes: 25
1395
- }
1396
- };
1397
- var emptyQuiz = {
1398
- id: generateUniqueId("quiz_"),
1399
- title: "New Quiz",
1400
- description: "",
1401
- questions: [],
1402
- settings: {
1403
- language: "English",
1404
- // <-- ĐÃ THÊM
1405
- shuffleQuestions: false,
1406
- shuffleOptions: false,
1407
- showCorrectAnswers: "end_of_quiz",
1408
- passingScorePercent: 0,
1409
- timeLimitMinutes: 0
1410
- }
1411
- };
1412
-
1413
- // src/services/scormPackaging.ts
1414
- var import_jszip = __toESM(require("jszip"));
1415
- var sanitizeFilename = (name) => {
1416
- return name.replace(/[^a-z0-9_.-]/gi, "_").toLowerCase();
1417
- };
1418
- var exportQuizAsSCORMZip = async (quiz, options) => {
1419
- try {
1420
- const zip = new import_jszip.default();
1421
- const quizDataString = JSON.stringify(quiz, null, 2);
1422
- zip.file("quiz_data.json", quizDataString);
1423
- const libraryJSPath = "lib/interactive-quiz-kit.esm.js";
1424
- const blocklyCSSPath = "blockly-styles.css";
1425
- const manifestContent = generateSCORMManifest(
1426
- quiz,
1427
- options.scormVersion,
1428
- "index.html",
1429
- // Launcher file name in ZIP
1430
- libraryJSPath,
1431
- // Expected path for lib JS in ZIP
1432
- "quiz_data.json",
1433
- // Path to quiz data in ZIP
1434
- blocklyCSSPath
1435
- // Expected path for Blockly CSS in ZIP
1436
- );
1437
- zip.file("imsmanifest.xml", manifestContent);
1438
- const launcherContent = generateLauncherHTML(
1439
- quiz,
1440
- // Pass full quizConfig for title and other potential uses
1441
- libraryJSPath,
1442
- // Path to library JS, relative within ZIP
1443
- "quiz_data.json",
1444
- // Path to quiz data, relative within ZIP
1445
- blocklyCSSPath,
1446
- // Path to Blockly CSS, relative within ZIP
1447
- quiz.title
1448
- // Explicitly pass title for the HTML page
1449
- );
1450
- zip.file("index.html", launcherContent);
1451
- const blob = await zip.generateAsync({ type: "blob" });
1452
- const fileName = `${sanitizeFilename(quiz.title || "quiz")}_scorm_${options.scormVersion.replace(".", "_")}.zip`;
1453
- const link = document.createElement("a");
1454
- link.href = URL.createObjectURL(blob);
1455
- link.download = fileName;
1456
- document.body.appendChild(link);
1457
- link.click();
1458
- document.body.removeChild(link);
1459
- URL.revokeObjectURL(link.href);
1460
- return { success: true, fileName };
1461
- } catch (err) {
1462
- console.error("Error creating SCORM ZIP:", err);
1463
- return { success: false, error: err instanceof Error ? err.message : "Unknown error during ZIP creation." };
1464
- }
1465
- };
1466
-
1467
- // src/services/APIKeyService.ts
1468
- var GEMINI_API_KEY_SERVICE_NAME = "gemini";
1469
- var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
1470
- function _encode(data) {
1471
- if (typeof window !== "undefined" && typeof window.btoa === "function") {
1472
- try {
1473
- return window.btoa(data);
1474
- } catch (e) {
1475
- console.error("Base64 encoding (btoa) failed:", e);
1476
- return data;
1477
- }
1478
- }
1479
- return data;
1480
- }
1481
- function _decode(data) {
1482
- if (typeof window !== "undefined" && typeof window.atob === "function") {
1483
- try {
1484
- return window.atob(data);
1485
- } catch (e) {
1486
- console.error("Base64 decoding (atob) failed:", e);
1487
- return data;
1488
- }
1489
- }
1490
- return data;
1491
- }
1492
- var APIKeyService = class {
1493
- static getStorageKey(serviceName) {
1494
- return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
1495
- }
1496
- /**
1497
- * Saves an API key to localStorage. The key is mildly obfuscated using Base64.
1498
- * @param serviceName - The name of the service (e.g., 'gemini').
1499
- * @param apiKey - The API key to save.
1500
- */
1501
- static saveAPIKey(serviceName, apiKey) {
1502
- if (typeof window !== "undefined" && window.localStorage) {
1503
- try {
1504
- const encodedKey = _encode(apiKey);
1505
- localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
1506
- } catch (e) {
1507
- console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
1508
- }
1509
- } else {
1510
- console.warn("localStorage is not available. APIKeyService cannot save keys.");
1511
- }
1512
- }
1513
- /**
1514
- * Retrieves an API key from localStorage.
1515
- * @param serviceName - The name of the service.
1516
- * @returns The decoded API key, or null if not found or if localStorage is unavailable.
1517
- */
1518
- static getAPIKey(serviceName) {
1519
- if (typeof window !== "undefined" && window.localStorage) {
1520
- try {
1521
- const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
1522
- if (storedKey) {
1523
- return _decode(storedKey);
1524
- }
1525
- } catch (e) {
1526
- console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
1527
- }
1528
- } else {
1529
- }
1530
- return null;
1531
- }
1532
- /**
1533
- * Removes an API key from localStorage.
1534
- * @param serviceName - The name of the service.
1535
- */
1536
- static removeAPIKey(serviceName) {
1537
- if (typeof window !== "undefined" && window.localStorage) {
1538
- try {
1539
- localStorage.removeItem(this.getStorageKey(serviceName));
1540
- } catch (e) {
1541
- console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
1542
- }
1543
- } else {
1544
- }
1545
- }
1546
- /**
1547
- * Checks if an API key exists in localStorage for the given service.
1548
- * @param serviceName - The name of the service.
1549
- * @returns True if a key exists, false otherwise.
1550
- */
1551
- static hasAPIKey(serviceName) {
1552
- return this.getAPIKey(serviceName) !== null;
1553
- }
1554
- };
1555
-
1556
- // src/services/QuizEditorService.ts
1557
- var QuizEditorService = class {
1558
- constructor(initialQuiz) {
1559
- this.quiz = JSON.parse(JSON.stringify(initialQuiz));
1560
- }
1561
- /**
1562
- * Returns the current state of the quiz configuration.
1563
- * @returns The current QuizConfig object.
1564
- */
1565
- getQuiz() {
1566
- return this.quiz;
1567
- }
1568
- /**
1569
- * Creates a new, "empty" question object based on the specified question type.
1570
- * @param type The type of question to create.
1571
- * @returns A new QuizQuestion object with default values.
1572
- */
1573
- static createNewQuestionTemplate(type) {
1574
- const baseNewQuestion = {
1575
- id: generateUniqueId(`new_${type}_`),
1576
- // 'new_' prefix indicates it's a new, unsaved question
1577
- questionType: type,
1578
- prompt: "",
1579
- points: 10,
1580
- difficulty: "medium"
1581
- };
1582
- switch (type) {
1583
- case "true_false":
1584
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "true_false", correctAnswer: true });
1585
- case "multiple_choice":
1586
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_choice", options: [], correctAnswerId: "" });
1587
- case "multiple_response":
1588
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_response", options: [], correctAnswerIds: [] });
1589
- case "short_answer":
1590
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "short_answer", acceptedAnswers: [""], isCaseSensitive: false });
1591
- case "numeric":
1592
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "numeric", answer: 0 });
1593
- case "fill_in_the_blanks": {
1594
- const blankId = generateUniqueId("blank_");
1595
- return __spreadProps(__spreadValues({}, baseNewQuestion), {
1596
- questionType: "fill_in_the_blanks",
1597
- segments: [
1598
- { type: "text", content: "Your text before " },
1599
- { type: "blank", id: blankId },
1600
- { type: "text", content: " and after." }
1601
- ],
1602
- answers: [{ blankId, acceptedValues: [""] }],
1603
- isCaseSensitive: false
1604
- });
1605
- }
1606
- case "sequence":
1607
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "sequence", items: [], correctOrder: [] });
1608
- case "matching":
1609
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "matching", prompts: [], options: [], correctAnswerMap: [], shuffleOptions: true });
1610
- case "drag_and_drop":
1611
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "drag_and_drop", draggableItems: [], dropZones: [], answerMap: [] });
1612
- case "hotspot":
1613
- return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "hotspot", imageUrl: "", hotspots: [], correctHotspotIds: [] });
1614
- case "blockly_programming":
1615
- return __spreadProps(__spreadValues({}, baseNewQuestion), {
1616
- questionType: "blockly_programming",
1617
- toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
1618
- initialWorkspace: "",
1619
- solutionWorkspaceXML: "",
1620
- solutionGeneratedCode: ""
1621
- });
1622
- case "scratch_programming":
1623
- return __spreadProps(__spreadValues({}, baseNewQuestion), {
1624
- questionType: "scratch_programming",
1625
- toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
1626
- initialWorkspace: "",
1627
- solutionWorkspaceXML: "",
1628
- solutionGeneratedCode: ""
1629
- });
1630
- default:
1631
- const _exhaustiveCheck = type;
1632
- throw new Error(`Question type "${_exhaustiveCheck}" is not supported for creation.`);
1633
- }
1634
- }
1635
- /**
1636
- * Adds a new question to the quiz. If the question ID is temporary, a new permanent ID is generated.
1637
- * @param question The question object to add.
1638
- * @returns The updated QuizConfig.
1639
- */
1640
- addQuestion(question) {
1641
- const newQuestion = __spreadValues({}, question);
1642
- if (newQuestion.id.startsWith("new_")) {
1643
- newQuestion.id = generateUniqueId(`${newQuestion.questionType}_`);
1644
- }
1645
- this.quiz.questions.push(newQuestion);
1646
- return this.quiz;
1647
- }
1648
- /**
1649
- * Updates an existing question in the quiz.
1650
- * @param updatedQuestion The full question object with changes.
1651
- * @returns The updated QuizConfig.
1652
- */
1653
- updateQuestion(updatedQuestion) {
1654
- const questionIndex = this.quiz.questions.findIndex((q) => q.id === updatedQuestion.id);
1655
- if (questionIndex === -1) {
1656
- throw new Error(`Question with ID "${updatedQuestion.id}" not found.`);
1657
- }
1658
- this.quiz.questions[questionIndex] = updatedQuestion;
1659
- return this.quiz;
1660
- }
1661
- /**
1662
- * Deletes a question from the quiz by its index.
1663
- * @param index The index of the question to delete.
1664
- * @returns The updated QuizConfig.
1665
- */
1666
- deleteQuestionByIndex(index) {
1667
- if (index < 0 || index >= this.quiz.questions.length) {
1668
- throw new Error(`Invalid index ${index} for question deletion.`);
1669
- }
1670
- this.quiz.questions.splice(index, 1);
1671
- return this.quiz;
1672
- }
1673
- /**
1674
- * Moves a question from one position to another.
1675
- * @param fromIndex The current index of the question.
1676
- * @param toIndex The target index for the question.
1677
- * @returns The updated QuizConfig.
1678
- */
1679
- moveQuestion(fromIndex, toIndex) {
1680
- if (fromIndex < 0 || fromIndex >= this.quiz.questions.length || toIndex < 0 || toIndex >= this.quiz.questions.length) {
1681
- throw new Error("Invalid index for moving question.");
1682
- }
1683
- const [movedItem] = this.quiz.questions.splice(fromIndex, 1);
1684
- this.quiz.questions.splice(toIndex, 0, movedItem);
1685
- return this.quiz;
1686
- }
1687
- };
1688
-
1689
- // src/ai/flows/generate-fitb-question.ts
1690
- var import_zod = require("zod");
1691
- var import_genkit = require("genkit");
1692
- var import_googleai = require("@genkit-ai/googleai");
1693
- function extractJsonFromMarkdown(text) {
1694
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1695
- return match ? match[1].trim() : text.trim();
1696
- }
1697
- var GenerateFillInTheBlanksQuestionClientInputSchema = import_zod.z.object({
1698
- topic: import_zod.z.string().describe("The topic for the question."),
1699
- language: import_zod.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
1700
- // <-- ĐÃ THÊM
1701
- difficulty: import_zod.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
1702
- numberOfBlanks: import_zod.z.number().int().min(1).max(5).optional().default(1).describe("Number of blanks to include (1-5)."),
1703
- isCaseSensitive: import_zod.z.boolean().optional().default(false).describe("Whether answers should be case-sensitive."),
1704
- contextDescription: import_zod.z.string().optional().describe("A specific context or scenario for the question."),
1705
- selectedContextId: import_zod.z.string().optional().describe("The ID of the selected context.")
1706
- });
1707
- var AIFillInTheBlanksOutputFieldsSchema = import_zod.z.object({
1708
- prompt: import_zod.z.string().describe("The overall instruction for the question."),
1709
- sentenceWithPlaceholders: import_zod.z.string().describe("The sentence containing placeholders like {{placeholder_name}}."),
1710
- blanks: import_zod.z.array(
1711
- import_zod.z.object({
1712
- placeholder: import_zod.z.string().describe("The placeholder name (without curly braces)."),
1713
- acceptedAnswers: import_zod.z.array(import_zod.z.string().min(1)).min(1).describe("Array of acceptable answers.")
1714
- })
1715
- ).min(1),
1716
- isCaseSensitive: import_zod.z.boolean().optional(),
1717
- explanation: import_zod.z.string().optional(),
1718
- points: import_zod.z.number().optional().default(10),
1719
- difficulty: import_zod.z.enum(["easy", "medium", "hard"]).optional(),
1720
- topic: import_zod.z.string().optional()
1721
- });
1722
- var FillInTheBlanksQuestionZodSchema = import_zod.z.object({
1723
- id: import_zod.z.string(),
1724
- questionType: import_zod.z.literal("fill_in_the_blanks"),
1725
- prompt: import_zod.z.string().min(1),
1726
- segments: import_zod.z.array(import_zod.z.object({
1727
- type: import_zod.z.enum(["text", "blank"]),
1728
- content: import_zod.z.string().optional(),
1729
- // Only for 'text' type
1730
- id: import_zod.z.string().optional()
1731
- // Only for 'blank' type
1732
- })).min(1),
1733
- answers: import_zod.z.array(import_zod.z.object({
1734
- blankId: import_zod.z.string(),
1735
- acceptedValues: import_zod.z.array(import_zod.z.string().min(1)).min(1)
1736
- })).min(1),
1737
- isCaseSensitive: import_zod.z.boolean().optional(),
1738
- points: import_zod.z.number().min(0).optional(),
1739
- explanation: import_zod.z.string().optional()
1740
- // ... other fields
1741
- }).refine((data) => {
1742
- const segmentBlankIds = new Set(data.segments.filter((s) => s.type === "blank").map((s) => s.id));
1743
- const answerBlankIds = new Set(data.answers.map((a) => a.blankId));
1744
- if (segmentBlankIds.size !== answerBlankIds.size) return false;
1745
- for (const id of segmentBlankIds) {
1746
- if (!answerBlankIds.has(id || "")) return false;
1747
- }
1748
- return true;
1749
- }, {
1750
- message: "There must be a 1-to-1 correspondence between blank segments and answer definitions.",
1751
- path: ["answers"]
1752
- });
1753
- var GenerateFillInTheBlanksQuestionOutputSchema = import_zod.z.object({
1754
- question: FillInTheBlanksQuestionZodSchema.optional().describe("The generated question.")
1755
- });
1756
- async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
1757
- var _a;
1758
- try {
1759
- const ai = (0, import_genkit.genkit)({
1760
- plugins: [(0, import_googleai.googleAI)({ apiKey })],
1761
- model: import_googleai.gemini20Flash
1762
- });
1763
- const promptText = `You are an expert quiz question writer.
1764
- Generate a single Fill-In-The-Blanks question in ${clientInput.language} with approximately ${clientInput.numberOfBlanks} blank(s).
1765
-
1766
- IMPORTANT: Return the response as JSON with this EXACT format:
1767
- {
1768
- "prompt": "Complete the famous saying.",
1769
- "sentenceWithPlaceholders": "Roses are {{color1}}, violets are {{color2}}.",
1770
- "blanks": [
1771
- { "placeholder": "color1", "acceptedAnswers": ["red"] },
1772
- { "placeholder": "color2", "acceptedAnswers": ["blue"] }
1773
- ],
1774
- "isCaseSensitive": false,
1775
- "explanation": "This is a classic nursery rhyme.",
1776
- "points": 10,
1777
- "difficulty": "easy",
1778
- "topic": "Nursery Rhymes"
1779
- }
1780
-
1781
- Requirements:
1782
- - Use double curly braces for placeholders, like {{placeholder_name}}.
1783
- - Each placeholder name inside the braces must be unique.
1784
- - The 'blanks' array must define the accepted answers for every unique placeholder in the sentence.
1785
- - The 'placeholder' value in the 'blanks' array should NOT include the curly braces.
1786
- - The content of 'prompt', 'sentenceWithPlaceholders', 'blanks.acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
1787
-
1788
- Topic: ${clientInput.topic}
1789
- Language: ${clientInput.language}
1790
- Difficulty: ${clientInput.difficulty}
1791
- Target Number of Blanks: ${clientInput.numberOfBlanks}
1792
- Case Sensitive: ${clientInput.isCaseSensitive}
1793
-
1794
- Return only the JSON response.`;
1795
- const response = await ai.generate(promptText);
1796
- const rawText = response.text;
1797
- const jsonText = extractJsonFromMarkdown(rawText);
1798
- console.log("AI Response:", jsonText);
1799
- const aiGeneratedContent = JSON.parse(jsonText);
1800
- if (aiGeneratedContent) {
1801
- const segments = [];
1802
- const answers = [];
1803
- const placeholderToBlankIdMap = {};
1804
- aiGeneratedContent.blanks.forEach((blankInfo) => {
1805
- const blankId = generateUniqueId("blank_");
1806
- placeholderToBlankIdMap[blankInfo.placeholder] = blankId;
1807
- answers.push({
1808
- blankId,
1809
- acceptedValues: blankInfo.acceptedAnswers
1810
- });
1811
- });
1812
- const placeholderRegex = /\{\{([^}]+)\}\}/g;
1813
- let lastIndex = 0;
1814
- let match;
1815
- while ((match = placeholderRegex.exec(aiGeneratedContent.sentenceWithPlaceholders)) !== null) {
1816
- const placeholderName = match[1];
1817
- const blankId = placeholderToBlankIdMap[placeholderName];
1818
- if (match.index > lastIndex) {
1819
- segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex, match.index) });
1820
- }
1821
- if (blankId) {
1822
- segments.push({ type: "blank", id: blankId });
1823
- } else {
1824
- console.warn(`Placeholder {{${placeholderName}}} found in sentence but not defined in blanks array. Treating as literal text.`);
1825
- segments.push({ type: "text", content: match[0] });
1826
- }
1827
- lastIndex = placeholderRegex.lastIndex;
1828
- }
1829
- if (lastIndex < aiGeneratedContent.sentenceWithPlaceholders.length) {
1830
- segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex) });
1831
- }
1832
- const completeQuestion = {
1833
- id: generateUniqueId("fitb_ai_"),
1834
- questionType: "fill_in_the_blanks",
1835
- prompt: aiGeneratedContent.prompt,
1836
- segments,
1837
- answers,
1838
- isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
1839
- explanation: aiGeneratedContent.explanation,
1840
- points: aiGeneratedContent.points,
1841
- topic: aiGeneratedContent.topic || clientInput.topic,
1842
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
1843
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1844
- };
1845
- try {
1846
- const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
1847
- return { question: validatedQuestion };
1848
- } catch (validationError) {
1849
- console.error("Question validation failed:", validationError);
1850
- throw new Error(`Generated question failed validation: ${validationError}`);
1851
- }
1852
- } else {
1853
- throw new Error("AI did not return content for the Fill-In-The-Blanks question.");
1854
- }
1855
- } catch (error) {
1856
- console.error("Error generating Fill-In-The-Blanks question:", error);
1857
- throw new Error(`Failed to generate Fill-In-The-Blanks question: ${error.message}`);
1858
- }
1859
- }
1860
-
1861
- // src/ai/flows/generate-matching-question.ts
1862
- var import_zod2 = require("zod");
1863
- var import_genkit2 = require("genkit");
1864
- var import_googleai2 = require("@genkit-ai/googleai");
1865
- function extractJsonFromMarkdown2(text) {
1866
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1867
- return match ? match[1].trim() : text.trim();
1868
- }
1869
- var GenerateMatchingQuestionClientInputSchema = import_zod2.z.object({
1870
- topic: import_zod2.z.string().describe("The topic for the question."),
1871
- language: import_zod2.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
1872
- // <-- ĐÃ THÊM
1873
- difficulty: import_zod2.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
1874
- numberOfPairs: import_zod2.z.number().int().min(2).max(8).optional().default(4).describe("Number of pairs to match (2-8)."),
1875
- shuffleOptions: import_zod2.z.boolean().optional().default(true).describe("Whether the options should be shuffled."),
1876
- contextDescription: import_zod2.z.string().optional().describe("A specific context or scenario for the question."),
1877
- selectedContextId: import_zod2.z.string().optional().describe("The ID of the selected context.")
1878
- });
1879
- var AIMatchingOutputFieldsSchema = import_zod2.z.object({
1880
- prompt: import_zod2.z.string().describe("The overall instruction (e.g., 'Match the terms to their definitions.')."),
1881
- correctPairs: import_zod2.z.array(
1882
- import_zod2.z.object({
1883
- promptText: import_zod2.z.string().min(1).describe("The text for a prompt item (e.g., a term)."),
1884
- optionText: import_zod2.z.string().min(1).describe("The text for the corresponding option item (e.g., its definition).")
1885
- })
1886
- ).min(2),
1887
- explanation: import_zod2.z.string().optional(),
1888
- points: import_zod2.z.number().optional().default(10),
1889
- difficulty: import_zod2.z.enum(["easy", "medium", "hard"]).optional(),
1890
- topic: import_zod2.z.string().optional()
1891
- });
1892
- var MatchingQuestionZodSchema = import_zod2.z.object({
1893
- id: import_zod2.z.string(),
1894
- questionType: import_zod2.z.literal("matching"),
1895
- prompt: import_zod2.z.string().min(1),
1896
- prompts: import_zod2.z.array(import_zod2.z.object({ id: import_zod2.z.string(), content: import_zod2.z.string().min(1) })).min(2),
1897
- options: import_zod2.z.array(import_zod2.z.object({ id: import_zod2.z.string(), content: import_zod2.z.string().min(1) })).min(2),
1898
- correctAnswerMap: import_zod2.z.array(import_zod2.z.object({ promptId: import_zod2.z.string(), optionId: import_zod2.z.string() })).min(2),
1899
- shuffleOptions: import_zod2.z.boolean().optional(),
1900
- points: import_zod2.z.number().min(0).optional(),
1901
- explanation: import_zod2.z.string().optional()
1902
- // ... other fields
1903
- }).refine((data) => {
1904
- const promptIds = new Set(data.prompts.map((p) => p.id));
1905
- const optionIds = new Set(data.options.map((o) => o.id));
1906
- return data.correctAnswerMap.every(
1907
- (map) => promptIds.has(map.promptId) && optionIds.has(map.optionId)
1908
- );
1909
- }, {
1910
- message: "All IDs in correctAnswerMap must exist in the prompts and options arrays.",
1911
- path: ["correctAnswerMap"]
1912
- }).refine((data) => {
1913
- const mappedPromptIds = new Set(data.correctAnswerMap.map((m) => m.promptId));
1914
- return mappedPromptIds.size === data.prompts.length && data.correctAnswerMap.length === data.prompts.length;
1915
- }, {
1916
- message: "Each prompt must be mapped exactly once in the correctAnswerMap.",
1917
- path: ["correctAnswerMap"]
1918
- });
1919
- var GenerateMatchingQuestionOutputSchema = import_zod2.z.object({
1920
- question: MatchingQuestionZodSchema.optional().describe("The generated Matching question.")
1921
- });
1922
- async function generateMatchingQuestion(clientInput, apiKey) {
1923
- try {
1924
- const ai = (0, import_genkit2.genkit)({
1925
- plugins: [(0, import_googleai2.googleAI)({ apiKey })],
1926
- model: import_googleai2.gemini20Flash
1927
- });
1928
- const promptText = `You are an expert quiz question writer.
1929
- Generate a single Matching question in ${clientInput.language} with exactly ${clientInput.numberOfPairs} correct pairs.
1930
-
1931
- IMPORTANT: Return the response as JSON with this EXACT format:
1932
- {
1933
- "prompt": "Match each country to its capital city.",
1934
- "correctPairs": [
1935
- { "promptText": "France", "optionText": "Paris" },
1936
- { "promptText": "Japan", "optionText": "Tokyo" },
1937
- { "promptText": "Egypt", "optionText": "Cairo" },
1938
- { "promptText": "Brazil", "optionText": "Bras\xEDlia" }
1939
- ],
1940
- "explanation": "These are the capital cities for the respective countries.",
1941
- "points": 10,
1942
- "difficulty": "medium",
1943
- "topic": "World Geography"
1944
- }
1945
-
1946
- Requirements:
1947
- - The 'correctPairs' array must contain exactly ${clientInput.numberOfPairs} objects.
1948
- - Each object in 'correctPairs' must have a 'promptText' and its corresponding 'optionText'.
1949
- - Ensure the content of all 'promptText' values are unique.
1950
- - Ensure the content of all 'optionText' values are unique.
1951
- - The content of 'prompt', 'correctPairs.promptText', 'correctPairs.optionText', 'explanation', and 'topic' must be in ${clientInput.language}.
1952
-
1953
- Topic: ${clientInput.topic}
1954
- Language: ${clientInput.language}
1955
- Difficulty: ${clientInput.difficulty}
1956
- Number of Pairs: ${clientInput.numberOfPairs}
1957
-
1958
- Return only the JSON response.`;
1959
- const response = await ai.generate(promptText);
1960
- const rawText = response.text;
1961
- const jsonText = extractJsonFromMarkdown2(rawText);
1962
- console.log("AI Response:", jsonText);
1963
- const aiGeneratedContent = JSON.parse(jsonText);
1964
- if (aiGeneratedContent) {
1965
- if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
1966
- throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were requested.`);
1967
- }
1968
- const finalPrompts = [];
1969
- const finalOptions = [];
1970
- const finalCorrectAnswerMap = [];
1971
- aiGeneratedContent.correctPairs.forEach((pair) => {
1972
- const promptId = generateUniqueId("m_p_");
1973
- const optionId = generateUniqueId("m_o_");
1974
- finalPrompts.push({ id: promptId, content: pair.promptText });
1975
- finalOptions.push({ id: optionId, content: pair.optionText });
1976
- finalCorrectAnswerMap.push({ promptId, optionId });
1977
- });
1978
- const completeQuestion = {
1979
- id: generateUniqueId("match_ai_"),
1980
- questionType: "matching",
1981
- prompt: aiGeneratedContent.prompt,
1982
- prompts: finalPrompts,
1983
- options: finalOptions,
1984
- correctAnswerMap: finalCorrectAnswerMap,
1985
- shuffleOptions: clientInput.shuffleOptions,
1986
- explanation: aiGeneratedContent.explanation,
1987
- points: aiGeneratedContent.points,
1988
- topic: aiGeneratedContent.topic || clientInput.topic,
1989
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
1990
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1991
- };
1992
- try {
1993
- const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
1994
- return { question: validatedQuestion };
1995
- } catch (validationError) {
1996
- console.error("Question validation failed:", validationError);
1997
- throw new Error(`Generated question failed validation: ${validationError}`);
1998
- }
1999
- } else {
2000
- throw new Error("AI did not return content for the Matching question.");
2001
- }
2002
- } catch (error) {
2003
- console.error("Error generating Matching question:", error);
2004
- throw new Error(`Failed to generate Matching question: ${error.message}`);
2005
- }
2006
- }
2007
-
2008
- // src/ai/flows/generate-mcq-question.ts
2009
- var import_zod3 = require("zod");
2010
- var import_genkit3 = require("genkit");
2011
- var import_googleai3 = require("@genkit-ai/googleai");
2012
- function extractJsonFromMarkdown3(text) {
2013
- return text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
2014
- }
2015
- var GenerateMCQQuestionClientInputSchema = import_zod3.z.object({
2016
- topic: import_zod3.z.string().describe("The topic for the question."),
2017
- language: import_zod3.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2018
- // <-- ĐÃ THÊM
2019
- difficulty: import_zod3.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
2020
- numberOfOptions: import_zod3.z.number().int().min(2).max(6).optional().default(4).describe("Number of answer options to generate (2-6)."),
2021
- contextDescription: import_zod3.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
2022
- selectedContextId: import_zod3.z.string().optional().describe("The ID of the selected context, if any.")
2023
- });
2024
- var AIMCQOutputFieldsSchema = import_zod3.z.object({
2025
- prompt: import_zod3.z.string().describe("The question statement itself."),
2026
- options: import_zod3.z.array(
2027
- import_zod3.z.object({
2028
- tempId: import_zod3.z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B', '1', '2')."),
2029
- text: import_zod3.z.string().describe("The text content of this answer option.")
2030
- })
2031
- ).min(2).max(6),
2032
- correctTempOptionId: import_zod3.z.string().describe("The temporary ID of the correct option from the generated options array."),
2033
- explanation: import_zod3.z.string().optional().describe("A brief explanation of why the answer is correct."),
2034
- points: import_zod3.z.number().optional().default(10).describe("Points for a correct answer."),
2035
- difficulty: import_zod3.z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
2036
- topic: import_zod3.z.string().optional().describe("Refined topic.")
2037
- });
2038
- var MultipleChoiceQuestionZodSchema = import_zod3.z.object({
2039
- id: import_zod3.z.string(),
2040
- questionType: import_zod3.z.literal("multiple_choice"),
2041
- prompt: import_zod3.z.string().min(1),
2042
- options: import_zod3.z.array(import_zod3.z.object({ id: import_zod3.z.string(), text: import_zod3.z.string().min(1) })).min(2).max(10),
2043
- correctAnswerId: import_zod3.z.string(),
2044
- points: import_zod3.z.number().min(0).optional(),
2045
- explanation: import_zod3.z.string().optional(),
2046
- learningObjective: import_zod3.z.string().optional(),
2047
- glossary: import_zod3.z.array(import_zod3.z.string()).optional(),
2048
- bloomLevel: import_zod3.z.string().optional(),
2049
- difficulty: import_zod3.z.enum(["easy", "medium", "hard"]).optional(),
2050
- contextCode: import_zod3.z.string().optional(),
2051
- gradeBand: import_zod3.z.string().optional(),
2052
- course: import_zod3.z.string().optional(),
2053
- category: import_zod3.z.string().optional(),
2054
- topic: import_zod3.z.string().optional()
2055
- }).refine((data) => {
2056
- return data.options.some((option) => option.id === data.correctAnswerId);
2057
- }, {
2058
- message: "correctAnswerId must match one of the option IDs",
2059
- path: ["correctAnswerId"]
2060
- }).refine((data) => {
2061
- const optionIds = data.options.map((opt) => opt.id);
2062
- return optionIds.length === new Set(optionIds).size;
2063
- }, {
2064
- message: "All option IDs must be unique",
2065
- path: ["options"]
2066
- });
2067
- var GenerateMCQQuestionOutputSchema = import_zod3.z.object({
2068
- question: MultipleChoiceQuestionZodSchema.optional().describe("The generated Multiple Choice question.")
2069
- });
2070
- async function generateMCQQuestion(clientInput, apiKey) {
2071
- try {
2072
- const ai = (0, import_genkit3.genkit)({
2073
- plugins: [(0, import_googleai3.googleAI)({ apiKey })],
2074
- model: import_googleai3.gemini20Flash
2075
- });
2076
- const promptText = `You are an expert quiz question writer.
2077
- Generate a single Multiple Choice question in ${clientInput.language} based on the following inputs.
2078
-
2079
- IMPORTANT: Return the response as JSON with this EXACT format:
2080
- {
2081
- "prompt": "Your question here",
2082
- "options": [
2083
- { "tempId": "A", "text": "First option text" },
2084
- { "tempId": "B", "text": "Second option text" },
2085
- { "tempId": "C", "text": "Third option text" },
2086
- { "tempId": "D", "text": "Fourth option text" }
2087
- ],
2088
- "correctTempOptionId": "C",
2089
- "explanation": "Brief explanation",
2090
- "points": 10,
2091
- "difficulty": "medium",
2092
- "topic": "refined topic"
2093
- }
2094
-
2095
- Requirements:
2096
- - Generate exactly ${clientInput.numberOfOptions} options.
2097
- - Use tempId values like "A", "B", "C", "D" or "option_1", "option_2", etc.
2098
- - Make sure correctTempOptionId matches one of the tempId values in options array.
2099
- - Each option must have both "tempId" and "text" fields.
2100
- - The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
2101
-
2102
- Topic: ${clientInput.topic}
2103
- Language: ${clientInput.language}
2104
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2105
- Difficulty: ${clientInput.difficulty}
2106
- Number of Options: ${clientInput.numberOfOptions}
2107
-
2108
- Return only the JSON response.`;
2109
- const response = await ai.generate(promptText);
2110
- const rawText = response.text;
2111
- const jsonText = extractJsonFromMarkdown3(rawText);
2112
- console.log("AI Response:", jsonText);
2113
- let aiGeneratedContent = JSON.parse(jsonText);
2114
- if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
2115
- const normalizedOptions = aiGeneratedContent.options.map((option, index) => {
2116
- if (typeof option === "object" && !option.tempId) {
2117
- const key = Object.keys(option)[0];
2118
- const text = option[key];
2119
- return { tempId: key, text };
2120
- }
2121
- return option;
2122
- });
2123
- aiGeneratedContent.options = normalizedOptions;
2124
- }
2125
- console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
2126
- if (aiGeneratedContent) {
2127
- const finalOptions = [];
2128
- const tempIdToFinalIdMap = {};
2129
- aiGeneratedContent.options.forEach((aiOption) => {
2130
- const finalId = generateUniqueId("opt_");
2131
- finalOptions.push({ id: finalId, text: aiOption.text });
2132
- tempIdToFinalIdMap[aiOption.tempId] = finalId;
2133
- });
2134
- const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
2135
- if (!finalCorrectAnswerId) {
2136
- throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid or does not match any generated option tempId.`);
2137
- }
2138
- const completeQuestion = {
2139
- id: generateUniqueId("mcq_ai_"),
2140
- questionType: "multiple_choice",
2141
- prompt: aiGeneratedContent.prompt,
2142
- options: finalOptions,
2143
- correctAnswerId: finalCorrectAnswerId,
2144
- explanation: aiGeneratedContent.explanation,
2145
- points: aiGeneratedContent.points,
2146
- topic: aiGeneratedContent.topic || clientInput.topic,
2147
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2148
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2149
- };
2150
- try {
2151
- const validatedQuestion = MultipleChoiceQuestionZodSchema.parse(completeQuestion);
2152
- return { question: validatedQuestion };
2153
- } catch (validationError) {
2154
- console.error("Question validation failed:", validationError);
2155
- throw new Error(`Generated question failed validation: ${validationError}`);
2156
- }
2157
- } else {
2158
- throw new Error("AI did not return content for the MCQ question.");
2159
- }
2160
- } catch (error) {
2161
- console.error("Error generating MCQ question:", error);
2162
- throw new Error(`Failed to generate MCQ question: ${error.message}`);
2163
- }
2164
- }
2165
-
2166
- // src/ai/flows/generate-mrq-question.ts
2167
- var import_zod4 = require("zod");
2168
- var import_genkit4 = require("genkit");
2169
- var import_googleai4 = require("@genkit-ai/googleai");
2170
- function extractJsonFromMarkdown4(text) {
2171
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2172
- return match ? match[1].trim() : text.trim();
2173
- }
2174
- var GenerateMRQQuestionClientInputSchema = import_zod4.z.object({
2175
- topic: import_zod4.z.string().describe("The topic for the question."),
2176
- language: import_zod4.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2177
- // <-- ĐÃ THÊM
2178
- difficulty: import_zod4.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
2179
- numberOfOptions: import_zod4.z.number().int().min(2).max(8).optional().default(5).describe("Number of answer options to generate (2-8)."),
2180
- minCorrectAnswers: import_zod4.z.number().int().min(1).optional().default(2).describe("Minimum number of correct answers among the options."),
2181
- maxCorrectAnswers: import_zod4.z.number().int().min(1).optional().default(3).describe("Maximum number of correct answers (must be <= numberOfOptions)."),
2182
- contextDescription: import_zod4.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
2183
- selectedContextId: import_zod4.z.string().optional().describe("The ID of the selected context, if any.")
2184
- });
2185
- var AIMRQOutputFieldsSchema = import_zod4.z.object({
2186
- prompt: import_zod4.z.string().describe("The question statement itself."),
2187
- options: import_zod4.z.array(
2188
- import_zod4.z.object({
2189
- tempId: import_zod4.z.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B')."),
2190
- text: import_zod4.z.string().describe("The text content of this answer option.")
2191
- })
2192
- ).min(2).max(8),
2193
- correctTempOptionIds: import_zod4.z.array(import_zod4.z.string()).min(1).describe("An array of temporary IDs of the correct options."),
2194
- explanation: import_zod4.z.string().optional().describe("A brief explanation of why the answers are correct."),
2195
- points: import_zod4.z.number().optional().default(10).describe("Points for a correct answer."),
2196
- difficulty: import_zod4.z.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
2197
- topic: import_zod4.z.string().optional().describe("Refined topic.")
2198
- });
2199
- var MultipleResponseQuestionZodSchema = import_zod4.z.object({
2200
- id: import_zod4.z.string(),
2201
- questionType: import_zod4.z.literal("multiple_response"),
2202
- prompt: import_zod4.z.string().min(1),
2203
- options: import_zod4.z.array(import_zod4.z.object({ id: import_zod4.z.string(), text: import_zod4.z.string().min(1) })).min(2).max(10),
2204
- correctAnswerIds: import_zod4.z.array(import_zod4.z.string()).min(1),
2205
- points: import_zod4.z.number().min(0).optional(),
2206
- explanation: import_zod4.z.string().optional(),
2207
- learningObjective: import_zod4.z.string().optional(),
2208
- glossary: import_zod4.z.array(import_zod4.z.string()).optional(),
2209
- bloomLevel: import_zod4.z.string().optional(),
2210
- difficulty: import_zod4.z.enum(["easy", "medium", "hard"]).optional(),
2211
- contextCode: import_zod4.z.string().optional(),
2212
- gradeBand: import_zod4.z.string().optional(),
2213
- course: import_zod4.z.string().optional(),
2214
- category: import_zod4.z.string().optional(),
2215
- topic: import_zod4.z.string().optional()
2216
- }).refine((data) => {
2217
- const optionIds = new Set(data.options.map((option) => option.id));
2218
- return data.correctAnswerIds.every((correctId) => optionIds.has(correctId));
2219
- }, {
2220
- message: "All correctAnswerIds must match one of the option IDs",
2221
- path: ["correctAnswerIds"]
2222
- }).refine((data) => {
2223
- const optionIds = data.options.map((opt) => opt.id);
2224
- return optionIds.length === new Set(optionIds).size;
2225
- }, {
2226
- message: "All option IDs must be unique",
2227
- path: ["options"]
2228
- });
2229
- var GenerateMRQQuestionOutputSchema = import_zod4.z.object({
2230
- question: MultipleResponseQuestionZodSchema.optional().describe("The generated Multiple Response question.")
2231
- });
2232
- async function generateMRQQuestion(clientInput, apiKey) {
2233
- try {
2234
- if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
2235
- throw new Error("Minimum correct answers cannot exceed maximum correct answers.");
2236
- }
2237
- if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
2238
- throw new Error("Maximum correct answers must be less than the total number of options.");
2239
- }
2240
- const ai = (0, import_genkit4.genkit)({
2241
- plugins: [(0, import_googleai4.googleAI)({ apiKey })],
2242
- model: import_googleai4.gemini20Flash
2243
- });
2244
- const promptText = `You are an expert quiz question writer specializing in Multiple Response questions.
2245
- Generate a single Multiple Response question in ${clientInput.language} based on the following inputs.
2246
-
2247
- IMPORTANT: Return the response as JSON with this EXACT format:
2248
- {
2249
- "prompt": "Your question here (e.g., 'Which of the following are primary colors?')",
2250
- "options": [
2251
- { "tempId": "A", "text": "First option" },
2252
- { "tempId": "B", "text": "Second option" },
2253
- { "tempId": "C", "text": "Third option" },
2254
- { "tempId": "D", "text": "Fourth option" },
2255
- { "tempId": "E", "text": "Fifth option" }
2256
- ],
2257
- "correctTempOptionIds": ["A", "C"],
2258
- "explanation": "Brief explanation of all correct answers.",
2259
- "points": 10,
2260
- "difficulty": "medium",
2261
- "topic": "refined topic"
2262
- }
2263
-
2264
- Requirements:
2265
- - Generate exactly ${clientInput.numberOfOptions} options.
2266
- - Generate between ${clientInput.minCorrectAnswers} and ${clientInput.maxCorrectAnswers} correct answers.
2267
- - Use tempId values like "A", "B", "C", etc.
2268
- - Make sure all IDs in correctTempOptionIds match a tempId in the options array.
2269
- - Each option must have both "tempId" and "text" fields.
2270
- - The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
2271
-
2272
- Topic: ${clientInput.topic}
2273
- Language: ${clientInput.language}
2274
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2275
- Difficulty: ${clientInput.difficulty}
2276
- Number of Options: ${clientInput.numberOfOptions}
2277
- Min Correct Answers: ${clientInput.minCorrectAnswers}
2278
- Max Correct Answers: ${clientInput.maxCorrectAnswers}
2279
-
2280
- Return only the JSON response.`;
2281
- const response = await ai.generate(promptText);
2282
- const rawText = response.text;
2283
- const jsonText = extractJsonFromMarkdown4(rawText);
2284
- console.log("AI Response:", jsonText);
2285
- let aiGeneratedContent = JSON.parse(jsonText);
2286
- if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
2287
- const normalizedOptions = aiGeneratedContent.options.map((option) => {
2288
- if (typeof option === "object" && !option.tempId && Object.keys(option).length === 1) {
2289
- const key = Object.keys(option)[0];
2290
- return { tempId: key, text: option[key] };
2291
- }
2292
- return option;
2293
- });
2294
- aiGeneratedContent.options = normalizedOptions;
2295
- }
2296
- console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
2297
- if (aiGeneratedContent) {
2298
- const finalOptions = [];
2299
- const tempIdToFinalIdMap = {};
2300
- aiGeneratedContent.options.forEach((aiOption) => {
2301
- const finalId = generateUniqueId("opt_mr_");
2302
- finalOptions.push({ id: finalId, text: aiOption.text });
2303
- tempIdToFinalIdMap[aiOption.tempId] = finalId;
2304
- });
2305
- const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
2306
- const finalId = tempIdToFinalIdMap[tempId];
2307
- if (!finalId) {
2308
- throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') that does not map to any generated option.`);
2309
- }
2310
- return finalId;
2311
- });
2312
- if (finalCorrectAnswerIds.length < clientInput.minCorrectAnswers || finalCorrectAnswerIds.length > clientInput.maxCorrectAnswers) {
2313
- throw new Error(`AI generated ${finalCorrectAnswerIds.length} correct answers, which is outside the requested range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
2314
- }
2315
- if (finalOptions.length !== clientInput.numberOfOptions) {
2316
- throw new Error(`AI generated ${finalOptions.length} options, but ${clientInput.numberOfOptions} were requested.`);
2317
- }
2318
- const completeQuestion = {
2319
- id: generateUniqueId("mrq_ai_"),
2320
- questionType: "multiple_response",
2321
- prompt: aiGeneratedContent.prompt,
2322
- options: finalOptions,
2323
- correctAnswerIds: finalCorrectAnswerIds,
2324
- explanation: aiGeneratedContent.explanation,
2325
- points: aiGeneratedContent.points,
2326
- topic: aiGeneratedContent.topic || clientInput.topic,
2327
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2328
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2329
- };
2330
- try {
2331
- const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
2332
- return { question: validatedQuestion };
2333
- } catch (validationError) {
2334
- console.error("Question validation failed:", validationError);
2335
- throw new Error(`Generated question failed validation: ${validationError}`);
2336
- }
2337
- } else {
2338
- throw new Error("AI did not return content for the MRQ question.");
2339
- }
2340
- } catch (error) {
2341
- console.error("Error generating MRQ question:", error);
2342
- throw new Error(`Failed to generate MRQ question: ${error.message}`);
2343
- }
2344
- }
2345
-
2346
- // src/ai/flows/generate-numeric-question.ts
2347
- var import_zod5 = require("zod");
2348
- var import_genkit5 = require("genkit");
2349
- var import_googleai5 = require("@genkit-ai/googleai");
2350
- function extractJsonFromMarkdown5(text) {
2351
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2352
- return match ? match[1].trim() : text.trim();
2353
- }
2354
- var GenerateNumericQuestionClientInputSchema = import_zod5.z.object({
2355
- topic: import_zod5.z.string().describe("The topic for the question."),
2356
- language: import_zod5.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2357
- // <-- ĐÃ THÊM
2358
- difficulty: import_zod5.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
2359
- allowDecimals: import_zod5.z.boolean().optional().default(true).describe("Whether the answer can be a decimal."),
2360
- minRange: import_zod5.z.number().optional().describe("Optional minimum value for the answer."),
2361
- maxRange: import_zod5.z.number().optional().describe("Optional maximum value for the answer."),
2362
- contextDescription: import_zod5.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
2363
- selectedContextId: import_zod5.z.string().optional().describe("The ID of the selected context, if any.")
2364
- });
2365
- var AINumericOutputFieldsSchema = import_zod5.z.object({
2366
- prompt: import_zod5.z.string().describe("The question statement that expects a numerical answer."),
2367
- answer: import_zod5.z.number().describe("The precise numerical correct answer."),
2368
- tolerance: import_zod5.z.number().min(0).optional().default(0).describe("The acceptable range of error (plus or minus). Default is 0 for exact match."),
2369
- explanation: import_zod5.z.string().optional().describe("Explanation for the correct answer."),
2370
- points: import_zod5.z.number().optional().default(10),
2371
- difficulty: import_zod5.z.enum(["easy", "medium", "hard"]).optional(),
2372
- topic: import_zod5.z.string().optional()
2373
- });
2374
- var NumericQuestionZodSchema = import_zod5.z.object({
2375
- id: import_zod5.z.string(),
2376
- questionType: import_zod5.z.literal("numeric"),
2377
- prompt: import_zod5.z.string().min(1),
2378
- answer: import_zod5.z.number(),
2379
- tolerance: import_zod5.z.number().min(0).optional(),
2380
- points: import_zod5.z.number().min(0).optional(),
2381
- explanation: import_zod5.z.string().optional(),
2382
- learningObjective: import_zod5.z.string().optional(),
2383
- glossary: import_zod5.z.array(import_zod5.z.string()).optional(),
2384
- bloomLevel: import_zod5.z.string().optional(),
2385
- difficulty: import_zod5.z.enum(["easy", "medium", "hard"]).optional(),
2386
- contextCode: import_zod5.z.string().optional(),
2387
- gradeBand: import_zod5.z.string().optional(),
2388
- course: import_zod5.z.string().optional(),
2389
- category: import_zod5.z.string().optional(),
2390
- topic: import_zod5.z.string().optional()
2391
- });
2392
- var GenerateNumericQuestionOutputSchema = import_zod5.z.object({
2393
- question: NumericQuestionZodSchema.optional().describe("The generated Numeric question.")
2394
- });
2395
- async function generateNumericQuestion(clientInput, apiKey) {
2396
- try {
2397
- if (clientInput.minRange !== void 0 && clientInput.maxRange !== void 0 && clientInput.minRange > clientInput.maxRange) {
2398
- throw new Error("minRange cannot be greater than maxRange.");
2399
- }
2400
- const ai = (0, import_genkit5.genkit)({
2401
- plugins: [(0, import_googleai5.googleAI)({ apiKey })],
2402
- model: import_googleai5.gemini20Flash
2403
- });
2404
- const promptText = `You are an expert quiz question writer.
2405
- Generate a single Numeric question in ${clientInput.language} based on the following inputs. The question must require a numerical answer.
2406
-
2407
- IMPORTANT: Return the response as JSON with this EXACT format:
2408
- {
2409
- "prompt": "Your question here (e.g., 'What is the value of Pi rounded to two decimal places?')",
2410
- "answer": 3.14,
2411
- "tolerance": 0.01,
2412
- "explanation": "Pi is an irrational number, approximately 3.14159. Rounding to two decimal places gives 3.14.",
2413
- "points": 10,
2414
- "difficulty": "easy",
2415
- "topic": "Mathematics"
2416
- }
2417
-
2418
- Requirements:
2419
- - The 'answer' must be a number.
2420
- - If 'allowDecimals' is false, the 'answer' must be an integer.
2421
- - 'tolerance' is the acceptable error range (+/-). A tolerance of 0 means the answer must be exact.
2422
- - The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
2423
- ${clientInput.minRange !== void 0 ? `- The answer should ideally be >= ${clientInput.minRange}.` : ""}
2424
- ${clientInput.maxRange !== void 0 ? `- The answer should ideally be <= ${clientInput.maxRange}.` : ""}
2425
-
2426
- Topic: ${clientInput.topic}
2427
- Language: ${clientInput.language}
2428
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2429
- Difficulty: ${clientInput.difficulty}
2430
- Allow Decimals: ${clientInput.allowDecimals}
2431
-
2432
- Return only the JSON response.`;
2433
- const response = await ai.generate(promptText);
2434
- const rawText = response.text;
2435
- const jsonText = extractJsonFromMarkdown5(rawText);
2436
- console.log("AI Response:", jsonText);
2437
- const aiGeneratedContent = JSON.parse(jsonText);
2438
- if (aiGeneratedContent) {
2439
- let finalAnswer = aiGeneratedContent.answer;
2440
- if (!clientInput.allowDecimals) {
2441
- finalAnswer = Math.round(finalAnswer);
2442
- }
2443
- if (clientInput.minRange !== void 0 && finalAnswer < clientInput.minRange) {
2444
- console.warn(`AI generated answer ${finalAnswer} is below the requested minRange of ${clientInput.minRange}.`);
2445
- }
2446
- if (clientInput.maxRange !== void 0 && finalAnswer > clientInput.maxRange) {
2447
- console.warn(`AI generated answer ${finalAnswer} is above the requested maxRange of ${clientInput.maxRange}.`);
2448
- }
2449
- const completeQuestion = {
2450
- id: generateUniqueId("num_ai_"),
2451
- questionType: "numeric",
2452
- prompt: aiGeneratedContent.prompt,
2453
- answer: finalAnswer,
2454
- tolerance: aiGeneratedContent.tolerance,
2455
- explanation: aiGeneratedContent.explanation,
2456
- points: aiGeneratedContent.points,
2457
- topic: aiGeneratedContent.topic || clientInput.topic,
2458
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2459
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2460
- };
2461
- try {
2462
- const validatedQuestion = NumericQuestionZodSchema.parse(completeQuestion);
2463
- return { question: validatedQuestion };
2464
- } catch (validationError) {
2465
- console.error("Question validation failed:", validationError);
2466
- throw new Error(`Generated question failed validation: ${validationError}`);
2467
- }
2468
- } else {
2469
- throw new Error("AI did not return content for the Numeric question.");
2470
- }
2471
- } catch (error) {
2472
- console.error("Error generating Numeric question:", error);
2473
- throw new Error(`Failed to generate Numeric question: ${error.message}`);
2474
- }
2475
- }
2476
-
2477
- // src/ai/flows/generate-quiz-plan.ts
2478
- var import_zod6 = require("zod");
2479
- var import_genkit6 = require("genkit");
2480
- var import_googleai6 = require("@genkit-ai/googleai");
2481
- function extractJsonFromMarkdown6(text) {
2482
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2483
- return match ? match[1].trim() : text.trim();
2484
- }
2485
- var fullQuizSupportedQuestionTypesArray = [
2486
- "true_false",
2487
- "multiple_choice",
2488
- "multiple_response",
2489
- "short_answer",
2490
- "numeric",
2491
- "fill_in_the_blanks",
2492
- "sequence",
2493
- "matching"
2494
- ];
2495
- var BloomLevelStringsEnum = import_zod6.z.enum(["remembering", "understanding", "applying"]);
2496
- var GenerateQuizPlanClientInputSchema = import_zod6.z.object({
2497
- language: import_zod6.z.string().optional().default("English").describe('The language for the quiz plan (e.g., "Vietnamese", "English").'),
2498
- // <-- ĐÃ THÊM
2499
- totalQuestions: import_zod6.z.number().int().min(1).max(50),
2500
- topics: import_zod6.z.array(import_zod6.z.object({
2501
- topic: import_zod6.z.string().min(1),
2502
- ratio: import_zod6.z.number().min(0).max(100)
2503
- })).min(1),
2504
- bloomLevels: import_zod6.z.array(import_zod6.z.object({
2505
- level: BloomLevelStringsEnum,
2506
- ratio: import_zod6.z.number().min(0).max(100)
2507
- })).min(1),
2508
- selectedContextIds: import_zod6.z.array(import_zod6.z.string()).optional(),
2509
- selectedQuestionTypes: import_zod6.z.array(import_zod6.z.enum(fullQuizSupportedQuestionTypesArray)).min(1)
2510
- });
2511
- var PlannedQuestionSchema = import_zod6.z.object({
2512
- plannedTopic: import_zod6.z.string().min(1).describe("The specific topic for this question."),
2513
- plannedQuestionType: import_zod6.z.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
2514
- plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned.")
2515
- });
2516
- var GenerateQuizPlanOutputSchema = import_zod6.z.object({
2517
- quizPlan: import_zod6.z.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz.")
2518
- });
2519
- async function generateQuizPlan(clientInput, apiKey) {
2520
- try {
2521
- const totalTopicRatio = clientInput.topics.reduce((sum, t) => sum + t.ratio, 0);
2522
- if (Math.abs(totalTopicRatio - 100) > 1) {
2523
- throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
2524
- }
2525
- const totalBloomRatio = clientInput.bloomLevels.reduce((sum, b) => sum + b.ratio, 0);
2526
- if (Math.abs(totalBloomRatio - 100) > 1) {
2527
- throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
2528
- }
2529
- const ai = (0, import_genkit6.genkit)({
2530
- plugins: [(0, import_googleai6.googleAI)({ apiKey })],
2531
- model: import_googleai6.gemini20Flash
2532
- });
2533
- const topicsDistribution = clientInput.topics.map((t) => `- Topic: "${t.topic}", Ratio: ${t.ratio}%`).join("\n ");
2534
- const bloomDistribution = clientInput.bloomLevels.map((b) => `- Level: "${b.level}", Ratio: ${b.ratio}%`).join("\n ");
2535
- const allowedQuestionTypes = clientInput.selectedQuestionTypes.map((t) => `'${t}'`).join(", ");
2536
- const promptText = `You are an expert educational content planner. Your task is to generate a detailed plan for a quiz in ${clientInput.language}.
2537
-
2538
- IMPORTANT: Return the response as JSON with this EXACT format:
2539
- {
2540
- "quizPlan": [
2541
- { "plannedTopic": "Specific Topic A", "plannedQuestionType": "multiple_choice", "plannedBloomLevel": "remembering" },
2542
- { "plannedTopic": "Specific Topic B", "plannedQuestionType": "true_false", "plannedBloomLevel": "understanding" }
2543
- ]
2544
- }
2545
-
2546
- Constraints and Guidelines:
2547
- 1. **Total Questions**: The output 'quizPlan' array must contain exactly ${clientInput.totalQuestions} elements.
2548
- 2. **Topic Distribution**: Distribute the questions across the following topics according to their specified ratios. Match these ratios as closely as possible.
2549
- ${topicsDistribution}
2550
- 3. **Bloom Level Distribution**: Distribute the questions across the following Bloom's Taxonomy levels according to their specified ratios.
2551
- ${bloomDistribution}
2552
- 4. **Allowed Question Types**: For each planned question, 'plannedQuestionType' must be one of these types: ${allowedQuestionTypes}. Use a variety of these types.
2553
- 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}.
2554
-
2555
- 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}.
2556
- The final 'quizPlan' array must have exactly ${clientInput.totalQuestions} elements.
2557
-
2558
- Return only the JSON response.`;
2559
- const response = await ai.generate(promptText);
2560
- const rawText = response.text;
2561
- const jsonText = extractJsonFromMarkdown6(rawText);
2562
- console.log("AI Response:", jsonText);
2563
- const aiGeneratedContent = JSON.parse(jsonText);
2564
- if (!aiGeneratedContent || !aiGeneratedContent.quizPlan) {
2565
- throw new Error("AI did not return a valid quiz plan structure.");
2566
- }
2567
- if (aiGeneratedContent.quizPlan.length !== clientInput.totalQuestions) {
2568
- throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested. The plan must match the total number of questions.`);
2569
- }
2570
- aiGeneratedContent.quizPlan.forEach((item, index) => {
2571
- if (!clientInput.selectedQuestionTypes.includes(item.plannedQuestionType)) {
2572
- throw new Error(`AI planned question ${index + 1} with a disallowed question type: '${item.plannedQuestionType}'`);
2573
- }
2574
- });
2575
- try {
2576
- const validatedPlan = GenerateQuizPlanOutputSchema.parse(aiGeneratedContent);
2577
- return validatedPlan;
2578
- } catch (validationError) {
2579
- console.error("Quiz plan validation failed:", validationError);
2580
- throw new Error(`Generated quiz plan failed validation: ${validationError}`);
2581
- }
2582
- } catch (error) {
2583
- console.error("Error generating Quiz Plan:", error);
2584
- throw new Error(`Failed to generate Quiz Plan: ${error.message}`);
2585
- }
2586
- }
2587
-
2588
- // src/ai/flows/generate-sequence-question.ts
2589
- var import_zod7 = require("zod");
2590
- var import_genkit7 = require("genkit");
2591
- var import_googleai7 = require("@genkit-ai/googleai");
2592
- function extractJsonFromMarkdown7(text) {
2593
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2594
- return match ? match[1].trim() : text.trim();
2595
- }
2596
- var GenerateSequenceQuestionClientInputSchema = import_zod7.z.object({
2597
- topic: import_zod7.z.string().describe("The topic for the question."),
2598
- language: import_zod7.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2599
- // <-- ĐÃ THÊM
2600
- difficulty: import_zod7.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
2601
- numberOfItems: import_zod7.z.number().int().min(2).max(10).optional().default(4).describe("Number of items to sequence (2-10)."),
2602
- contextDescription: import_zod7.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
2603
- selectedContextId: import_zod7.z.string().optional().describe("The ID of the selected context, if any.")
2604
- });
2605
- var AISequenceOutputFieldsSchema = import_zod7.z.object({
2606
- prompt: import_zod7.z.string().describe("The instruction for sequencing."),
2607
- itemsContent: import_zod7.z.array(import_zod7.z.string().min(1)).min(2).describe("An array of strings for each item to be sequenced."),
2608
- correctOrderContent: import_zod7.z.array(import_zod7.z.string().min(1)).min(2).describe("An array of the same strings from 'itemsContent', but in the correct sequence."),
2609
- explanation: import_zod7.z.string().optional().describe("Explanation for the correct sequence."),
2610
- points: import_zod7.z.number().optional().default(10),
2611
- difficulty: import_zod7.z.enum(["easy", "medium", "hard"]).optional(),
2612
- topic: import_zod7.z.string().optional()
2613
- });
2614
- var SequenceQuestionZodSchema = import_zod7.z.object({
2615
- id: import_zod7.z.string(),
2616
- questionType: import_zod7.z.literal("sequence"),
2617
- prompt: import_zod7.z.string().min(1),
2618
- items: import_zod7.z.array(import_zod7.z.object({ id: import_zod7.z.string(), content: import_zod7.z.string().min(1) })).min(2),
2619
- correctOrder: import_zod7.z.array(import_zod7.z.string()).min(2),
2620
- // Array of item IDs
2621
- points: import_zod7.z.number().min(0).optional(),
2622
- explanation: import_zod7.z.string().optional(),
2623
- learningObjective: import_zod7.z.string().optional(),
2624
- glossary: import_zod7.z.array(import_zod7.z.string()).optional(),
2625
- bloomLevel: import_zod7.z.string().optional(),
2626
- difficulty: import_zod7.z.enum(["easy", "medium", "hard"]).optional(),
2627
- contextCode: import_zod7.z.string().optional(),
2628
- gradeBand: import_zod7.z.string().optional(),
2629
- course: import_zod7.z.string().optional(),
2630
- category: import_zod7.z.string().optional(),
2631
- topic: import_zod7.z.string().optional()
2632
- }).refine((data) => {
2633
- const itemIds = new Set(data.items.map((item) => item.id));
2634
- return data.correctOrder.every((id) => itemIds.has(id));
2635
- }, {
2636
- message: "Every ID in correctOrder must correspond to an item in the items array.",
2637
- path: ["correctOrder"]
2638
- }).refine((data) => {
2639
- return new Set(data.correctOrder).size === data.items.length && data.correctOrder.length === data.items.length;
2640
- }, {
2641
- message: "The correctOrder array must contain all item IDs exactly once.",
2642
- path: ["correctOrder"]
2643
- }).refine((data) => {
2644
- const itemIds = data.items.map((item) => item.id);
2645
- return itemIds.length === new Set(itemIds).size;
2646
- }, {
2647
- message: "All item IDs must be unique.",
2648
- path: ["items"]
2649
- });
2650
- var GenerateSequenceQuestionOutputSchema = import_zod7.z.object({
2651
- question: SequenceQuestionZodSchema.optional().describe("The generated Sequence question.")
2652
- });
2653
- async function generateSequenceQuestion(clientInput, apiKey) {
2654
- try {
2655
- const ai = (0, import_genkit7.genkit)({
2656
- plugins: [(0, import_googleai7.googleAI)({ apiKey })],
2657
- model: import_googleai7.gemini20Flash
2658
- });
2659
- const promptText = `You are an expert quiz question writer specializing in Sequence questions.
2660
- Generate a single Sequence question in ${clientInput.language} based on the following inputs.
2661
-
2662
- IMPORTANT: Return the response as JSON with this EXACT format:
2663
- {
2664
- "prompt": "Arrange the following events of World War II in chronological order.",
2665
- "itemsContent": [
2666
- "D-Day (Normandy Landings)",
2667
- "Invasion of Poland",
2668
- "Attack on Pearl Harbor",
2669
- "Battle of Stalingrad"
2670
- ],
2671
- "correctOrderContent": [
2672
- "Invasion of Poland",
2673
- "Attack on Pearl Harbor",
2674
- "Battle of Stalingrad",
2675
- "D-Day (Normandy Landings)"
2676
- ],
2677
- "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.",
2678
- "points": 10,
2679
- "difficulty": "medium",
2680
- "topic": "World War II History"
2681
- }
2682
-
2683
- Requirements:
2684
- - Generate exactly ${clientInput.numberOfItems} items to be sequenced.
2685
- - The 'itemsContent' array should contain the text for each item.
2686
- - The 'correctOrderContent' array must contain the exact same strings as 'itemsContent', but arranged in the correct sequence.
2687
- - Both arrays must have the same number of elements.
2688
- - The content of 'prompt', 'itemsContent', 'correctOrderContent', 'explanation', and 'topic' must be in ${clientInput.language}.
2689
-
2690
- Topic: ${clientInput.topic}
2691
- Language: ${clientInput.language}
2692
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2693
- Difficulty: ${clientInput.difficulty}
2694
- Number of Items: ${clientInput.numberOfItems}
2695
-
2696
- Return only the JSON response.`;
2697
- const response = await ai.generate(promptText);
2698
- const rawText = response.text;
2699
- const jsonText = extractJsonFromMarkdown7(rawText);
2700
- console.log("AI Response:", jsonText);
2701
- const aiGeneratedContent = JSON.parse(jsonText);
2702
- if (aiGeneratedContent) {
2703
- if (aiGeneratedContent.itemsContent.length !== clientInput.numberOfItems || aiGeneratedContent.correctOrderContent.length !== clientInput.numberOfItems) {
2704
- 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.`);
2705
- }
2706
- if (new Set(aiGeneratedContent.itemsContent).size !== new Set(aiGeneratedContent.correctOrderContent).size) {
2707
- throw new Error("The set of items in 'itemsContent' and 'correctOrderContent' do not match.");
2708
- }
2709
- const contentToIdMap = {};
2710
- const finalItems = aiGeneratedContent.itemsContent.map((content) => {
2711
- const id = generateUniqueId("seqi_");
2712
- contentToIdMap[content] = id;
2713
- return { id, content };
2714
- });
2715
- const finalCorrectOrder = aiGeneratedContent.correctOrderContent.map((content) => {
2716
- const id = contentToIdMap[content];
2717
- if (!id) {
2718
- throw new Error(`Content "${content}" from 'correctOrderContent' was not found in 'itemsContent'.`);
2719
- }
2720
- return id;
2721
- });
2722
- const completeQuestion = {
2723
- id: generateUniqueId("seq_ai_"),
2724
- questionType: "sequence",
2725
- prompt: aiGeneratedContent.prompt,
2726
- items: finalItems,
2727
- correctOrder: finalCorrectOrder,
2728
- explanation: aiGeneratedContent.explanation,
2729
- points: aiGeneratedContent.points,
2730
- topic: aiGeneratedContent.topic || clientInput.topic,
2731
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2732
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2733
- };
2734
- try {
2735
- const validatedQuestion = SequenceQuestionZodSchema.parse(completeQuestion);
2736
- return { question: validatedQuestion };
2737
- } catch (validationError) {
2738
- console.error("Question validation failed:", validationError);
2739
- throw new Error(`Generated question failed validation: ${validationError}`);
2740
- }
2741
- } else {
2742
- throw new Error("AI did not return content for the Sequence question.");
2743
- }
2744
- } catch (error) {
2745
- console.error("Error generating Sequence question:", error);
2746
- throw new Error(`Failed to generate Sequence question: ${error.message}`);
2747
- }
2748
- }
2749
-
2750
- // src/ai/flows/generate-short-answer-question.ts
2751
- var import_zod8 = require("zod");
2752
- var import_genkit8 = require("genkit");
2753
- var import_googleai8 = require("@genkit-ai/googleai");
2754
- function extractJsonFromMarkdown8(text) {
2755
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2756
- return match ? match[1].trim() : text.trim();
2757
- }
2758
- var GenerateShortAnswerQuestionClientInputSchema = import_zod8.z.object({
2759
- topic: import_zod8.z.string().describe("The topic for the question."),
2760
- language: import_zod8.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2761
- // <-- ĐÃ THÊM
2762
- difficulty: import_zod8.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
2763
- isCaseSensitive: import_zod8.z.boolean().optional().default(false).describe("Whether the answer should be case-sensitive."),
2764
- contextDescription: import_zod8.z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
2765
- selectedContextId: import_zod8.z.string().optional().describe("The ID of the selected context, if any.")
2766
- });
2767
- var AIShortAnswerOutputFieldsSchema = import_zod8.z.object({
2768
- prompt: import_zod8.z.string().describe("The question statement."),
2769
- acceptedAnswers: import_zod8.z.array(import_zod8.z.string().min(1)).min(1).describe("An array of acceptable short answers."),
2770
- isCaseSensitive: import_zod8.z.boolean().optional().describe("Should the answer evaluation be case sensitive?"),
2771
- explanation: import_zod8.z.string().optional().describe("Explanation for the correct answer(s)."),
2772
- points: import_zod8.z.number().optional().default(10),
2773
- difficulty: import_zod8.z.enum(["easy", "medium", "hard"]).optional(),
2774
- topic: import_zod8.z.string().optional()
2775
- });
2776
- var ShortAnswerQuestionZodSchema = import_zod8.z.object({
2777
- id: import_zod8.z.string(),
2778
- questionType: import_zod8.z.literal("short_answer"),
2779
- prompt: import_zod8.z.string().min(1),
2780
- acceptedAnswers: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
2781
- isCaseSensitive: import_zod8.z.boolean().optional(),
2782
- points: import_zod8.z.number().min(0).optional(),
2783
- explanation: import_zod8.z.string().optional(),
2784
- learningObjective: import_zod8.z.string().optional(),
2785
- glossary: import_zod8.z.array(import_zod8.z.string()).optional(),
2786
- bloomLevel: import_zod8.z.string().optional(),
2787
- difficulty: import_zod8.z.enum(["easy", "medium", "hard"]).optional(),
2788
- contextCode: import_zod8.z.string().optional(),
2789
- gradeBand: import_zod8.z.string().optional(),
2790
- course: import_zod8.z.string().optional(),
2791
- category: import_zod8.z.string().optional(),
2792
- topic: import_zod8.z.string().optional()
2793
- });
2794
- var GenerateShortAnswerQuestionOutputSchema = import_zod8.z.object({
2795
- question: ShortAnswerQuestionZodSchema.optional().describe("The generated Short Answer question.")
2796
- });
2797
- async function generateShortAnswerQuestion(clientInput, apiKey) {
2798
- var _a;
2799
- try {
2800
- const ai = (0, import_genkit8.genkit)({
2801
- plugins: [(0, import_googleai8.googleAI)({ apiKey })],
2802
- model: import_googleai8.gemini20Flash
2803
- });
2804
- const promptText = `You are an expert quiz question writer.
2805
- Generate a single Short Answer question in ${clientInput.language} based on the following inputs.
2806
-
2807
- IMPORTANT: Return the response as JSON with this EXACT format:
2808
- {
2809
- "prompt": "What is the capital of France?",
2810
- "acceptedAnswers": ["Paris"],
2811
- "isCaseSensitive": false,
2812
- "explanation": "Paris has been the capital of France since the 10th century.",
2813
- "points": 5,
2814
- "difficulty": "easy",
2815
- "topic": "World Capitals"
2816
- }
2817
-
2818
- Requirements:
2819
- - The 'acceptedAnswers' array must contain at least one possible correct answer.
2820
- - If there are multiple common ways to phrase the answer (e.g., "USA", "United States"), include them in the array.
2821
- - Set 'isCaseSensitive' to true or false based on the nature of the answer. Default to the input value.
2822
- - The content of 'prompt', 'acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
2823
-
2824
- Topic: ${clientInput.topic}
2825
- Language: ${clientInput.language}
2826
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2827
- Difficulty: ${clientInput.difficulty}
2828
- Case Sensitive: ${clientInput.isCaseSensitive}
2829
-
2830
- Return only the JSON response.`;
2831
- const response = await ai.generate(promptText);
2832
- const rawText = response.text;
2833
- const jsonText = extractJsonFromMarkdown8(rawText);
2834
- console.log("AI Response:", jsonText);
2835
- const aiGeneratedContent = JSON.parse(jsonText);
2836
- if (aiGeneratedContent) {
2837
- const completeQuestion = {
2838
- id: generateUniqueId("saq_ai_"),
2839
- questionType: "short_answer",
2840
- prompt: aiGeneratedContent.prompt,
2841
- acceptedAnswers: aiGeneratedContent.acceptedAnswers,
2842
- // Ưu tiên giá trị từ AI, nếu không có thì dùng giá trị đầu vào
2843
- isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
2844
- explanation: aiGeneratedContent.explanation,
2845
- points: aiGeneratedContent.points,
2846
- topic: aiGeneratedContent.topic || clientInput.topic,
2847
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2848
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2849
- };
2850
- try {
2851
- const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
2852
- return { question: validatedQuestion };
2853
- } catch (validationError) {
2854
- console.error("Question validation failed:", validationError);
2855
- throw new Error(`Generated question failed validation: ${validationError}`);
2856
- }
2857
- } else {
2858
- throw new Error("AI did not return content for the Short Answer question.");
2859
- }
2860
- } catch (error) {
2861
- console.error("Error generating Short Answer question:", error);
2862
- throw new Error(`Failed to generate Short Answer question: ${error.message}`);
2863
- }
2864
- }
2865
-
2866
- // src/ai/flows/generate-true-false-question.ts
2867
- var import_zod9 = require("zod");
2868
- var import_genkit9 = require("genkit");
2869
- var import_googleai9 = require("@genkit-ai/googleai");
2870
- function extractJsonFromMarkdown9(text) {
2871
- const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2872
- return match ? match[1].trim() : text.trim();
2873
- }
2874
- var GenerateTrueFalseQuestionClientInputSchema = import_zod9.z.object({
2875
- topic: import_zod9.z.string().describe("The topic for the question."),
2876
- language: import_zod9.z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2877
- // <-- ĐÃ THÊM
2878
- difficulty: import_zod9.z.enum(["easy", "medium", "hard"]).optional().default("medium"),
2879
- contextDescription: import_zod9.z.string().optional().describe("A specific context or scenario for the question."),
2880
- selectedContextId: import_zod9.z.string().optional().describe("The ID of the selected context.")
2881
- });
2882
- var AITrueFalseOutputFieldsSchema = import_zod9.z.object({
2883
- prompt: import_zod9.z.string().describe("The statement to be evaluated as true or false."),
2884
- correctAnswer: import_zod9.z.boolean().describe("The correct answer (true or false)."),
2885
- explanation: import_zod9.z.string().optional().describe("A brief explanation of why the answer is correct."),
2886
- points: import_zod9.z.number().optional().default(10),
2887
- difficulty: import_zod9.z.enum(["easy", "medium", "hard"]).optional(),
2888
- topic: import_zod9.z.string().optional()
2889
- });
2890
- var TrueFalseQuestionZodSchema = import_zod9.z.object({
2891
- id: import_zod9.z.string(),
2892
- questionType: import_zod9.z.literal("true_false"),
2893
- prompt: import_zod9.z.string().min(1),
2894
- correctAnswer: import_zod9.z.boolean(),
2895
- points: import_zod9.z.number().min(0).optional(),
2896
- explanation: import_zod9.z.string().optional(),
2897
- learningObjective: import_zod9.z.string().optional(),
2898
- glossary: import_zod9.z.array(import_zod9.z.string()).optional(),
2899
- bloomLevel: import_zod9.z.string().optional(),
2900
- difficulty: import_zod9.z.enum(["easy", "medium", "hard"]).optional(),
2901
- contextCode: import_zod9.z.string().optional(),
2902
- gradeBand: import_zod9.z.string().optional(),
2903
- course: import_zod9.z.string().optional(),
2904
- category: import_zod9.z.string().optional(),
2905
- topic: import_zod9.z.string().optional()
2906
- });
2907
- var GenerateTrueFalseQuestionOutputSchema = import_zod9.z.object({
2908
- question: TrueFalseQuestionZodSchema.optional().describe("The generated True/False question.")
2909
- });
2910
- async function generateTrueFalseQuestion(clientInput, apiKey) {
2911
- try {
2912
- const ai = (0, import_genkit9.genkit)({
2913
- plugins: [(0, import_googleai9.googleAI)({ apiKey })],
2914
- model: import_googleai9.gemini20Flash
2915
- });
2916
- const promptText = `You are an expert quiz question writer.
2917
- Generate a single True/False question in ${clientInput.language} based on the following inputs.
2918
-
2919
- IMPORTANT: Return the response as JSON with this EXACT format:
2920
- {
2921
- "prompt": "The Earth is the fourth planet from the Sun.",
2922
- "correctAnswer": false,
2923
- "explanation": "The Earth is the third planet from the Sun. Mars is the fourth.",
2924
- "points": 10,
2925
- "difficulty": "easy",
2926
- "topic": "Astronomy"
2927
- }
2928
-
2929
- Requirements:
2930
- - The 'prompt' must be a clear statement that is definitively true or false.
2931
- - 'correctAnswer' must be a boolean value (true or false).
2932
- - The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
2933
-
2934
- Topic: ${clientInput.topic}
2935
- Language: ${clientInput.language}
2936
- ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2937
- Difficulty: ${clientInput.difficulty}
2938
-
2939
- Return only the JSON response.`;
2940
- const response = await ai.generate(promptText);
2941
- const rawText = response.text;
2942
- const jsonText = extractJsonFromMarkdown9(rawText);
2943
- console.log("AI Response:", jsonText);
2944
- const aiGeneratedContent = JSON.parse(jsonText);
2945
- if (aiGeneratedContent) {
2946
- const completeQuestion = {
2947
- id: generateUniqueId("tf_ai_"),
2948
- questionType: "true_false",
2949
- prompt: aiGeneratedContent.prompt,
2950
- correctAnswer: aiGeneratedContent.correctAnswer,
2951
- explanation: aiGeneratedContent.explanation,
2952
- points: aiGeneratedContent.points,
2953
- topic: aiGeneratedContent.topic || clientInput.topic,
2954
- difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2955
- contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2956
- };
2957
- try {
2958
- const validatedQuestion = TrueFalseQuestionZodSchema.parse(completeQuestion);
2959
- return { question: validatedQuestion };
2960
- } catch (validationError) {
2961
- console.error("Question validation failed:", validationError);
2962
- throw new Error(`Generated question failed validation: ${validationError}`);
2963
- }
2964
- } else {
2965
- throw new Error("AI did not return content for the True/False question.");
2966
- }
2967
- } catch (error) {
2968
- console.error("Error generating True/False question:", error);
2969
- throw new Error(`Failed to generate True/False question: ${error.message}`);
2970
- }
2971
- }
2972
-
2973
- // src/ai/flows/generate-questions-from-quiz-plan.ts
2974
- var import_zod10 = require("zod");
2975
- var internalContextOptions = [
2976
- // ... (giữ nguyên mảng này)
2977
- { contextId: "THEO_ABS", contextDescription: "L\xFD thuy\u1EBFt/Tr\u1EEBu t\u01B0\u1EE3ng" },
2978
- { contextId: "SPEC_CASE", contextDescription: "V\xED d\u1EE5 C\u1EE5 th\u1EC3/Tr\u01B0\u1EDDng h\u1EE3p Ri\xEAng" },
2979
- { contextId: "NAT_OBS", contextDescription: "Hi\u1EC7n t\u01B0\u1EE3ng T\u1EF1 nhi\xEAn/Quan s\xE1t" },
2980
- { contextId: "TECH_ENG", contextDescription: "\u1EE8ng d\u1EE5ng C\xF4ng ngh\u1EC7/K\u1EF9 thu\u1EADt" },
2981
- { contextId: "EXP_INV", contextDescription: "Th\xED nghi\u1EC7m/\u0110i\u1EC1u tra Khoa h\u1ECDc" },
2982
- { contextId: "REAL_PROB", contextDescription: "V\u1EA5n \u0111\u1EC1 Th\u1EF1c t\u1EBF/X\xE3 h\u1ED9i/M\xF4i tr\u01B0\u1EDDng" },
2983
- { contextId: "DATA_MOD", contextDescription: "Di\u1EC5n gi\u1EA3i D\u1EEF li\u1EC7u/M\xF4 h\xECnh h\xF3a" },
2984
- { contextId: "HIST_SCI", contextDescription: "L\u1ECBch s\u1EED/Ph\xE1t tri\u1EC3n Khoa h\u1ECDc" },
2985
- { contextId: "INTERDISC", contextDescription: "Li\xEAn ng\xE0nh (Interdisciplinary)" },
2986
- { contextId: "HYPO_COMP", contextDescription: "Gi\u1EA3 \u0111\u1ECBnh/So s\xE1nh T\xECnh hu\u1ED1ng" }
2987
- ];
2988
- var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomContextInput) => {
2989
- let contextScore = 1;
2990
- const selectedContext = contextId ? internalContextOptions.find((c) => c.contextId === contextId) : void 0;
2991
- if (selectedContext) {
2992
- if (["THEO_ABS", "HIST_SCI"].includes(selectedContext.contextId)) contextScore = 1;
2993
- else if (["SPEC_CASE", "NAT_OBS", "DATA_MOD", "INTERDISC", "HYPO_COMP"].includes(selectedContext.contextId)) contextScore = 2;
2994
- else if (["TECH_ENG", "EXP_INV", "REAL_PROB"].includes(selectedContext.contextId)) contextScore = 3;
2995
- } else if (generalCustomContextInput == null ? void 0 : generalCustomContextInput.trim()) {
2996
- contextScore = 2;
2997
- }
2998
- let bloomScore = 1;
2999
- if (bloomLevel === "understanding") bloomScore = 2;
3000
- else if (bloomLevel === "applying") bloomScore = 3;
3001
- let questionTypeScore = 1;
3002
- switch (qType) {
3003
- case "true_false":
3004
- case "multiple_choice":
3005
- case "short_answer":
3006
- questionTypeScore = 1;
3007
- break;
3008
- case "matching":
3009
- case "fill_in_the_blanks":
3010
- case "numeric":
3011
- questionTypeScore = 2;
3012
- break;
3013
- case "sequence":
3014
- case "multiple_response":
3015
- case "drag_and_drop":
3016
- questionTypeScore = 3;
3017
- break;
3018
- default:
3019
- questionTypeScore = 1;
3020
- }
3021
- const totalScore = bloomScore + contextScore + questionTypeScore;
3022
- if (totalScore <= 4) return "easy";
3023
- if (totalScore <= 6) return "medium";
3024
- return "hard";
3025
- };
3026
- var PlannedQuestionSchema2 = import_zod10.z.object({
3027
- plannedTopic: import_zod10.z.string().min(1),
3028
- plannedQuestionType: import_zod10.z.string(),
3029
- // Giữ dạng string để linh hoạt, sẽ kiểm tra trong logic
3030
- plannedBloomLevel: import_zod10.z.enum(["remembering", "understanding", "applying"]),
3031
- plannedContextId: import_zod10.z.string().optional()
3032
- });
3033
- var GenerateQuestionsFromQuizPlanClientInputSchema = import_zod10.z.object({
3034
- quizPlan: import_zod10.z.array(PlannedQuestionSchema2).min(1),
3035
- language: import_zod10.z.string().optional().default("English").describe("The language for the generated questions."),
3036
- // <-- ĐÃ THÊM
3037
- selectedContextIds: import_zod10.z.array(import_zod10.z.string()).optional(),
3038
- customContextInput: import_zod10.z.string().optional()
3039
- });
3040
- var GenerationErrorSchema = import_zod10.z.object({
3041
- plannedQuestionIndex: import_zod10.z.number(),
3042
- plannedTopic: import_zod10.z.string(),
3043
- plannedQuestionType: import_zod10.z.string(),
3044
- error: import_zod10.z.string()
3045
- });
3046
- var GenerateQuestionsFromQuizPlanOutputSchema = import_zod10.z.object({
3047
- generatedQuestions: import_zod10.z.array(import_zod10.z.any()),
3048
- // 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
3049
- errors: import_zod10.z.array(GenerationErrorSchema).optional()
3050
- });
3051
- async function generateQuestionsFromQuizPlan(clientInput, apiKey) {
3052
- var _a, _b;
3053
- const { quizPlan, selectedContextIds, customContextInput, language } = clientInput;
3054
- const generatedQuestions = [];
3055
- const errors = [];
3056
- for (let i = 0; i < quizPlan.length; i++) {
3057
- const plannedQ = quizPlan[i];
3058
- let generationError = null;
3059
- try {
3060
- let contextDescriptionForAI;
3061
- let contextIdForDifficultyCalc = plannedQ.plannedContextId;
3062
- if (plannedQ.plannedContextId && plannedQ.plannedContextId !== "__custom__") {
3063
- contextDescriptionForAI = (_a = internalContextOptions.find((c) => c.contextId === plannedQ.plannedContextId)) == null ? void 0 : _a.contextDescription;
3064
- } else if (plannedQ.plannedContextId === "__custom__") {
3065
- contextDescriptionForAI = customContextInput == null ? void 0 : customContextInput.trim();
3066
- } else if ((selectedContextIds == null ? void 0 : selectedContextIds[0]) && selectedContextIds[0] !== "__custom__") {
3067
- contextDescriptionForAI = (_b = internalContextOptions.find((c) => c.contextId === selectedContextIds[0])) == null ? void 0 : _b.contextDescription;
3068
- if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = selectedContextIds[0];
3069
- } else if (customContextInput == null ? void 0 : customContextInput.trim()) {
3070
- contextDescriptionForAI = customContextInput.trim();
3071
- if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = "__custom__";
3072
- }
3073
- const difficultyForAI = calculateCombinedDifficulty(
3074
- contextIdForDifficultyCalc,
3075
- plannedQ.plannedBloomLevel,
3076
- plannedQ.plannedQuestionType,
3077
- contextDescriptionForAI
3078
- );
3079
- const baseClientInput = {
3080
- topic: plannedQ.plannedTopic,
3081
- language,
3082
- // <-- TRUYỀN `language` VÀO
3083
- difficulty: difficultyForAI,
3084
- contextDescription: contextDescriptionForAI,
3085
- selectedContextId: plannedQ.plannedContextId || contextIdForDifficultyCalc
3086
- };
3087
- let result = {};
3088
- switch (plannedQ.plannedQuestionType) {
3089
- case "true_false":
3090
- result = await generateTrueFalseQuestion(baseClientInput, apiKey);
3091
- break;
3092
- case "multiple_choice":
3093
- result = await generateMCQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 4 }), apiKey);
3094
- break;
3095
- case "multiple_response":
3096
- result = await generateMRQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 5, minCorrectAnswers: 2, maxCorrectAnswers: 3 }), apiKey);
3097
- break;
3098
- case "short_answer":
3099
- result = await generateShortAnswerQuestion(__spreadProps(__spreadValues({}, baseClientInput), { isCaseSensitive: false }), apiKey);
3100
- break;
3101
- case "numeric":
3102
- result = await generateNumericQuestion(__spreadProps(__spreadValues({}, baseClientInput), { allowDecimals: true }), apiKey);
3103
- break;
3104
- case "fill_in_the_blanks":
3105
- result = await generateFillInTheBlanksQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfBlanks: 2, isCaseSensitive: false }), apiKey);
3106
- break;
3107
- case "sequence":
3108
- result = await generateSequenceQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfItems: 4 }), apiKey);
3109
- break;
3110
- case "matching":
3111
- result = await generateMatchingQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfPairs: 4, shuffleOptions: true }), apiKey);
3112
- break;
3113
- default:
3114
- generationError = `Question type "${plannedQ.plannedQuestionType}" is not supported for automated generation.`;
3115
- }
3116
- if (result.question) {
3117
- const question = result.question;
3118
- question.topic = plannedQ.plannedTopic;
3119
- question.bloomLevel = plannedQ.plannedBloomLevel;
3120
- question.difficulty = difficultyForAI;
3121
- question.contextCode = baseClientInput.selectedContextId;
3122
- if (question.points === void 0) question.points = 10;
3123
- generatedQuestions.push(question);
3124
- } else if (!generationError) {
3125
- generationError = `AI did not return a question object for type '${plannedQ.plannedQuestionType}'.`;
3126
- }
3127
- } catch (e) {
3128
- generationError = e.message || `An unknown error occurred.`;
3129
- }
3130
- if (generationError) {
3131
- console.error(`Error generating question at index ${i} (Topic: ${plannedQ.plannedTopic}): ${generationError}`);
3132
- errors.push({
3133
- plannedQuestionIndex: i,
3134
- plannedTopic: plannedQ.plannedTopic,
3135
- plannedQuestionType: plannedQ.plannedQuestionType,
3136
- error: generationError
3137
- });
3138
- }
3139
- }
3140
- return { generatedQuestions, errors: errors.length > 0 ? errors : void 0 };
3141
- }
3142
-
3143
- // src/utils/utils.ts
3144
- var import_clsx = require("clsx");
3145
- var import_tailwind_merge = require("tailwind-merge");
3146
- function cn(...inputs) {
3147
- return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
3148
- }
3149
- // Annotate the CommonJS export names for ESM import in node:
3150
- 0 && (module.exports = {
3151
- APIKeyService,
3152
- GEMINI_API_KEY_SERVICE_NAME,
3153
- QuizEditorService,
3154
- QuizEngine,
3155
- SCORMService,
3156
- cn,
3157
- emptyQuiz,
3158
- exportQuizAsSCORMZip,
3159
- generateFillInTheBlanksQuestion,
3160
- generateLauncherHTML,
3161
- generateMCQQuestion,
3162
- generateMRQQuestion,
3163
- generateMatchingQuestion,
3164
- generateNumericQuestion,
3165
- generateQuestionsFromQuizPlan,
3166
- generateQuizPlan,
3167
- generateSCORMManifest,
3168
- generateSequenceQuestion,
3169
- generateShortAnswerQuestion,
3170
- generateTrueFalseQuestion,
3171
- generateUniqueId,
3172
- sampleQuiz
3173
- });