@thanh01.pmt/interactive-quiz-kit 1.0.22 → 1.0.24

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 ADDED
@@ -0,0 +1,3500 @@
1
+ import { z } from 'zod';
2
+ import { genkit } from 'genkit';
3
+ import { gemini20Flash, googleAI } from '@genkit-ai/googleai';
4
+ import JSZip from 'jszip';
5
+ import { clsx } from 'clsx';
6
+ import { twMerge } from 'tailwind-merge';
7
+
8
+ var __defProp = Object.defineProperty;
9
+ var __defProps = Object.defineProperties;
10
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
11
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
15
+ var __spreadValues = (a, b) => {
16
+ for (var prop in b || (b = {}))
17
+ if (__hasOwnProp.call(b, prop))
18
+ __defNormalProp(a, prop, b[prop]);
19
+ if (__getOwnPropSymbols)
20
+ for (var prop of __getOwnPropSymbols(b)) {
21
+ if (__propIsEnum.call(b, prop))
22
+ __defNormalProp(a, prop, b[prop]);
23
+ }
24
+ return a;
25
+ };
26
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
27
+
28
+ // src/services/SCORMService.ts
29
+ var SCORM_TRUE = "true";
30
+ var SCORM_NO_ERROR = "0";
31
+ var CMI_CORE_LESSON_STATUS_PASSED = "passed";
32
+ var CMI_CORE_LESSON_STATUS_FAILED = "failed";
33
+ var CMI_CORE_LESSON_STATUS_COMPLETED = "completed";
34
+ var CMI_CORE_LESSON_STATUS_INCOMPLETE = "incomplete";
35
+ var CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED = "not attempted";
36
+ var CMI_COMPLETION_STATUS_COMPLETED = "completed";
37
+ var CMI_COMPLETION_STATUS_INCOMPLETE = "incomplete";
38
+ var CMI_SUCCESS_STATUS_PASSED = "passed";
39
+ var CMI_SUCCESS_STATUS_FAILED = "failed";
40
+ var SCORMService = class {
41
+ constructor(settings) {
42
+ this.scormAPI = null;
43
+ this.scormVersionFound = null;
44
+ this.isInitialized = false;
45
+ this.isTerminated = false;
46
+ this.studentName = null;
47
+ this.settings = __spreadValues({
48
+ setCompletionOnFinish: true,
49
+ setSuccessOnPass: true,
50
+ autoCommit: true
51
+ }, settings);
52
+ if (typeof window !== "undefined") {
53
+ this._findAPI();
54
+ }
55
+ }
56
+ _findAPIRecursive(win) {
57
+ if (win === null) return null;
58
+ if (win.API_1484_11) {
59
+ this.scormVersionFound = "2004";
60
+ return win.API_1484_11;
61
+ }
62
+ if (win.API) {
63
+ this.scormVersionFound = "1.2";
64
+ return win.API;
65
+ }
66
+ if (win.parent && win.parent !== win) {
67
+ return this._findAPIRecursive(win.parent);
68
+ }
69
+ if (win.opener && typeof win.opener !== "undefined" && win.opener !== win && win.opener !== win.parent) {
70
+ try {
71
+ if (win.opener.document) {
72
+ return this._findAPIRecursive(win.opener);
73
+ }
74
+ } catch (e) {
75
+ console.warn("SCORMService: Could not access win.opener for API search due to cross-origin restrictions.");
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ _findAPI() {
81
+ try {
82
+ this.scormAPI = this._findAPIRecursive(window);
83
+ if (this.scormAPI) {
84
+ if (!this.scormVersionFound) this.scormVersionFound = this.settings.version;
85
+ console.log(`SCORMService: API Found. Version determined: ${this.scormVersionFound}`);
86
+ } else {
87
+ console.warn("SCORMService: SCORM API not found in window hierarchy.");
88
+ }
89
+ } catch (e) {
90
+ console.error("SCORMService: Error finding SCORM API", e);
91
+ this.scormAPI = null;
92
+ }
93
+ }
94
+ hasAPI() {
95
+ return this.scormAPI !== null;
96
+ }
97
+ getSCORMVersion() {
98
+ return this.scormVersionFound;
99
+ }
100
+ initialize() {
101
+ if (!this.hasAPI()) return { success: false, error: "SCORM API not found." };
102
+ if (this.isInitialized) return { success: true, studentName: this.studentName || void 0 };
103
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.Initialize("") : this.scormAPI.LMSInitialize("");
104
+ if (result.toString() === SCORM_TRUE || result === true) {
105
+ this.isInitialized = true;
106
+ this.isTerminated = false;
107
+ const studentNameVar = this.settings.studentNameVar || (this.scormVersionFound === "2004" ? "cmi.learner_name" : "cmi.core.student_name");
108
+ this.studentName = this.getValue(studentNameVar);
109
+ if (this.scormVersionFound === "2004") {
110
+ const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
111
+ if (this.getValue(completionStatusVar) === "not attempted") {
112
+ this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
113
+ }
114
+ } else {
115
+ const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
116
+ if (this.getValue(lessonStatusVar) === CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED) {
117
+ this.setValue(lessonStatusVar, CMI_CORE_LESSON_STATUS_INCOMPLETE);
118
+ }
119
+ }
120
+ if (this.settings.autoCommit) this.commit();
121
+ return { success: true, studentName: this.studentName || void 0 };
122
+ } else {
123
+ const error = this.getLastError();
124
+ return { success: false, error: `Initialization failed: ${error.message}` };
125
+ }
126
+ }
127
+ terminate() {
128
+ if (!this.hasAPI() || !this.isInitialized || this.isTerminated) {
129
+ const reason = !this.hasAPI() ? "API not found" : !this.isInitialized ? "Not initialized" : "Already terminated";
130
+ return { success: !this.hasAPI() || this.isTerminated, error: this.isTerminated ? void 0 : reason };
131
+ }
132
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.Terminate("") : this.scormAPI.LMSFinish("");
133
+ if (result.toString() === SCORM_TRUE || result === true) {
134
+ this.isTerminated = true;
135
+ this.isInitialized = false;
136
+ return { success: true };
137
+ } else {
138
+ const error = this.getLastError();
139
+ return { success: false, error: `Termination failed: ${error.message}` };
140
+ }
141
+ }
142
+ setValue(element, value) {
143
+ if (!this.hasAPI() || !this.isInitialized) {
144
+ return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
145
+ }
146
+ const valStr = value.toString();
147
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.SetValue(element, valStr) : this.scormAPI.LMSSetValue(element, valStr);
148
+ if (result.toString() === SCORM_TRUE || result === true) {
149
+ if (this.settings.autoCommit) this.commit();
150
+ return { success: true };
151
+ } else {
152
+ const error = this.getLastError();
153
+ return { success: false, error: `SetValue failed for ${element}: ${error.message}` };
154
+ }
155
+ }
156
+ getValue(element) {
157
+ var _a;
158
+ if (!this.hasAPI() || !this.isInitialized) return null;
159
+ const value = this.scormVersionFound === "2004" ? this.scormAPI.GetValue(element) : this.scormAPI.LMSGetValue(element);
160
+ const error = this.getLastError();
161
+ if (error.code !== SCORM_NO_ERROR && error.code !== "403" && error.code !== "0") {
162
+ console.warn(`SCORMService: GetValue for ${element} produced an error ${error.code}: ${error.message}. Returning raw value:`, value);
163
+ }
164
+ return (_a = value == null ? void 0 : value.toString()) != null ? _a : null;
165
+ }
166
+ commit() {
167
+ if (!this.hasAPI() || !this.isInitialized) {
168
+ return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
169
+ }
170
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.Commit("") : this.scormAPI.LMSCommit("");
171
+ if (result.toString() === SCORM_TRUE || result === true) {
172
+ return { success: true };
173
+ } else {
174
+ const error = this.getLastError();
175
+ return { success: false, error: `Commit failed: ${error.message}` };
176
+ }
177
+ }
178
+ setScore(rawScore, maxScore, minScore = 0) {
179
+ if (!this.hasAPI() || !this.isInitialized) return;
180
+ if (this.scormVersionFound === "2004") {
181
+ const scoreRawVar = this.settings.scoreRawVar_2004 || this.settings.scoreRawVar || "cmi.score.raw";
182
+ const scoreMaxVar = this.settings.scoreMaxVar_2004 || this.settings.scoreMaxVar || "cmi.score.max";
183
+ const scoreMinVar = this.settings.scoreMinVar_2004 || this.settings.scoreMinVar || "cmi.score.min";
184
+ const scoreScaledVar = this.settings.scoreScaledVar_2004 || "cmi.score.scaled";
185
+ this.setValue(scoreMinVar, minScore);
186
+ this.setValue(scoreMaxVar, maxScore);
187
+ this.setValue(scoreRawVar, rawScore);
188
+ if (maxScore > minScore) {
189
+ const scaledScore = (rawScore - minScore) / (maxScore - minScore);
190
+ this.setValue(scoreScaledVar, parseFloat(scaledScore.toFixed(4)));
191
+ } else if (maxScore === minScore && maxScore !== 0) {
192
+ this.setValue(scoreScaledVar, rawScore >= maxScore ? 1 : 0);
193
+ } else {
194
+ this.setValue(scoreScaledVar, 0);
195
+ }
196
+ } else {
197
+ const scoreRawVar = this.settings.scoreRawVar_1_2 || this.settings.scoreRawVar || "cmi.core.score.raw";
198
+ const scoreMaxVar = this.settings.scoreMaxVar_1_2 || this.settings.scoreMaxVar || "cmi.core.score.max";
199
+ const scoreMinVar = this.settings.scoreMinVar_1_2 || this.settings.scoreMinVar || "cmi.core.score.min";
200
+ this.setValue(scoreMinVar, minScore);
201
+ this.setValue(scoreMaxVar, maxScore);
202
+ this.setValue(scoreRawVar, rawScore);
203
+ }
204
+ }
205
+ setLessonStatus(status, passed) {
206
+ if (!this.hasAPI() || !this.isInitialized) return;
207
+ if (this.scormVersionFound === "2004") {
208
+ const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
209
+ const successStatusVar = this.settings.successStatusVar_2004 || "cmi.success_status";
210
+ if (this.settings.setCompletionOnFinish && (status === "completed" || status === "passed" || status === "failed")) {
211
+ this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_COMPLETED);
212
+ } else if (status === "incomplete" || status === "browsed") {
213
+ this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
214
+ }
215
+ if (this.settings.setSuccessOnPass && passed !== void 0) {
216
+ this.setValue(successStatusVar, passed ? CMI_SUCCESS_STATUS_PASSED : CMI_SUCCESS_STATUS_FAILED);
217
+ }
218
+ } else {
219
+ const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
220
+ let finalStatus = status;
221
+ if (this.settings.setCompletionOnFinish) {
222
+ if (this.settings.setSuccessOnPass && passed !== void 0) {
223
+ finalStatus = passed ? CMI_CORE_LESSON_STATUS_PASSED : CMI_CORE_LESSON_STATUS_FAILED;
224
+ } else {
225
+ finalStatus = CMI_CORE_LESSON_STATUS_COMPLETED;
226
+ }
227
+ } else {
228
+ if (status === CMI_CORE_LESSON_STATUS_PASSED || status === CMI_CORE_LESSON_STATUS_FAILED) ; else {
229
+ finalStatus = CMI_CORE_LESSON_STATUS_INCOMPLETE;
230
+ }
231
+ }
232
+ this.setValue(lessonStatusVar, finalStatus);
233
+ }
234
+ }
235
+ getLastError() {
236
+ var _a, _b;
237
+ if (!this.hasAPI()) return { code: "-1", message: "SCORM API not found." };
238
+ const errorCode = this.scormVersionFound === "2004" ? this.scormAPI.GetLastError() : this.scormAPI.LMSGetLastError();
239
+ if (errorCode === SCORM_NO_ERROR || errorCode === 0 || errorCode === "0") {
240
+ return { code: SCORM_NO_ERROR, message: "No error." };
241
+ }
242
+ const errorMessage = this.scormVersionFound === "2004" ? this.scormAPI.GetErrorString(errorCode.toString()) : this.scormAPI.LMSGetErrorString(errorCode.toString());
243
+ const diagnostic = this.scormVersionFound === "2004" ? this.scormAPI.GetDiagnostic(errorCode.toString()) : this.scormAPI.LMSGetDiagnostic(errorCode.toString());
244
+ return {
245
+ code: errorCode.toString(),
246
+ message: (_a = errorMessage == null ? void 0 : errorMessage.toString()) != null ? _a : "Unknown error.",
247
+ diagnostic: (_b = diagnostic == null ? void 0 : diagnostic.toString()) != null ? _b : void 0
248
+ };
249
+ }
250
+ formatCMITime(totalSeconds) {
251
+ const pad = (num, size = 2) => num.toString().padStart(size, "0");
252
+ if (this.scormVersionFound === "2004") {
253
+ const hours = Math.floor(totalSeconds / 3600);
254
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
255
+ const seconds = parseFloat((totalSeconds % 60).toFixed(2));
256
+ let timeString = "PT";
257
+ if (hours > 0) timeString += `${hours}H`;
258
+ if (minutes > 0 || hours > 0 && seconds > 0) {
259
+ timeString += `${minutes}M`;
260
+ }
261
+ if (seconds > 0 || timeString === "PT") {
262
+ timeString += `${seconds}S`;
263
+ }
264
+ return timeString === "PT" ? "PT0S" : timeString;
265
+ } else {
266
+ const hours = Math.floor(totalSeconds / 3600);
267
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
268
+ const secondsOnly = Math.floor(totalSeconds % 60);
269
+ const centiseconds = Math.floor((totalSeconds - Math.floor(totalSeconds)) * 100);
270
+ return `${pad(hours, 4)}:${pad(minutes)}:${pad(secondsOnly)}.${pad(centiseconds)}`;
271
+ }
272
+ }
273
+ };
274
+
275
+ // src/services/evaluators/multiple-choice-evaluator.ts
276
+ var MultipleChoiceEvaluator = class {
277
+ async evaluate(question, answer) {
278
+ var _a;
279
+ const points = (_a = question.points) != null ? _a : 0;
280
+ const correctAnswerId = question.correctAnswerId;
281
+ const isCorrect = answer === correctAnswerId;
282
+ const correctOption = question.options.find((opt) => opt.id === correctAnswerId);
283
+ const correctAnswerDetail = {
284
+ id: correctAnswerId,
285
+ value: (correctOption == null ? void 0 : correctOption.text) || ""
286
+ };
287
+ return Promise.resolve({
288
+ isCorrect,
289
+ correctAnswer: correctAnswerDetail,
290
+ pointsEarned: isCorrect ? points : 0
291
+ });
292
+ }
293
+ };
294
+
295
+ // src/services/evaluators/multiple-response-evaluator.ts
296
+ var MultipleResponseEvaluator = class {
297
+ async evaluate(question, answer) {
298
+ var _a;
299
+ const points = (_a = question.points) != null ? _a : 0;
300
+ const correctAnswerIds = question.correctAnswerIds;
301
+ let isCorrect = false;
302
+ if (Array.isArray(answer)) {
303
+ const userAnswerSet = new Set(answer);
304
+ const correctAnswerSet = new Set(correctAnswerIds);
305
+ isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
306
+ }
307
+ const correctValues = correctAnswerIds.map(
308
+ (id) => {
309
+ var _a2;
310
+ return ((_a2 = question.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
311
+ }
312
+ );
313
+ const correctAnswerDetail = {
314
+ id: correctAnswerIds,
315
+ value: correctValues
316
+ };
317
+ return Promise.resolve({
318
+ isCorrect,
319
+ correctAnswer: correctAnswerDetail,
320
+ pointsEarned: isCorrect ? points : 0
321
+ });
322
+ }
323
+ };
324
+
325
+ // src/services/evaluators/true-false-evaluator.ts
326
+ var TrueFalseEvaluator = class {
327
+ async evaluate(question, answer) {
328
+ var _a;
329
+ const points = (_a = question.points) != null ? _a : 0;
330
+ const correctAnswer = question.correctAnswer;
331
+ let userAnswer = answer;
332
+ if (typeof answer === "string") {
333
+ userAnswer = answer.toLowerCase() === "true";
334
+ }
335
+ const isCorrect = typeof userAnswer === "boolean" && userAnswer === correctAnswer;
336
+ const correctAnswerDetail = {
337
+ id: null,
338
+ value: correctAnswer
339
+ };
340
+ return Promise.resolve({
341
+ isCorrect,
342
+ correctAnswer: correctAnswerDetail,
343
+ pointsEarned: isCorrect ? points : 0
344
+ });
345
+ }
346
+ };
347
+
348
+ // src/services/evaluators/short-answer-evaluator.ts
349
+ var ShortAnswerEvaluator = class {
350
+ async evaluate(question, answer) {
351
+ var _a, _b;
352
+ const points = (_a = question.points) != null ? _a : 0;
353
+ let isCorrect = false;
354
+ if (typeof answer === "string") {
355
+ const userAnswerTrimmed = answer.trim();
356
+ const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
357
+ isCorrect = question.acceptedAnswers.some(
358
+ (accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
359
+ );
360
+ }
361
+ const correctAnswerDetail = {
362
+ id: null,
363
+ value: question.acceptedAnswers
364
+ };
365
+ return Promise.resolve({
366
+ isCorrect,
367
+ correctAnswer: correctAnswerDetail,
368
+ pointsEarned: isCorrect ? points : 0
369
+ });
370
+ }
371
+ };
372
+
373
+ // src/services/evaluators/numeric-evaluator.ts
374
+ var NumericEvaluator = class {
375
+ async evaluate(question, answer) {
376
+ var _a;
377
+ const points = (_a = question.points) != null ? _a : 0;
378
+ let isCorrect = false;
379
+ if (typeof answer === "string" || typeof answer === "number") {
380
+ const userAnswerNum = parseFloat(String(answer));
381
+ if (!isNaN(userAnswerNum)) {
382
+ isCorrect = question.tolerance != null ? Math.abs(userAnswerNum - question.answer) <= question.tolerance : userAnswerNum === question.answer;
383
+ }
384
+ }
385
+ const correctAnswerDetail = {
386
+ id: null,
387
+ value: question.answer
388
+ };
389
+ return Promise.resolve({
390
+ isCorrect,
391
+ correctAnswer: correctAnswerDetail,
392
+ pointsEarned: isCorrect ? points : 0
393
+ });
394
+ }
395
+ };
396
+
397
+ // src/services/evaluators/sequence-evaluator.ts
398
+ var SequenceEvaluator = class {
399
+ async evaluate(question, answer) {
400
+ var _a;
401
+ const points = (_a = question.points) != null ? _a : 0;
402
+ let isCorrect = false;
403
+ if (Array.isArray(answer) && answer.length === question.correctOrder.length) {
404
+ isCorrect = answer.every((itemId, index) => itemId === question.correctOrder[index]);
405
+ }
406
+ const correctValues = question.correctOrder.map(
407
+ (id) => {
408
+ var _a2;
409
+ return ((_a2 = question.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
410
+ }
411
+ );
412
+ const correctAnswerDetail = {
413
+ id: question.correctOrder,
414
+ value: correctValues
415
+ };
416
+ return Promise.resolve({
417
+ isCorrect,
418
+ correctAnswer: correctAnswerDetail,
419
+ pointsEarned: isCorrect ? points : 0
420
+ });
421
+ }
422
+ };
423
+
424
+ // src/services/evaluators/matching-evaluator.ts
425
+ var MatchingEvaluator = class {
426
+ async evaluate(question, answer) {
427
+ var _a;
428
+ const points = (_a = question.points) != null ? _a : 0;
429
+ let isCorrect = false;
430
+ if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
431
+ const userAnswerMap = answer;
432
+ isCorrect = question.correctAnswerMap.length === Object.keys(userAnswerMap).length && question.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
433
+ }
434
+ const correctMap = question.correctAnswerMap.reduce((acc, curr) => {
435
+ var _a2, _b;
436
+ const promptText = ((_a2 = question.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
437
+ const optionText = ((_b = question.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b.content) || "";
438
+ acc[promptText] = optionText;
439
+ return acc;
440
+ }, {});
441
+ const correctAnswerDetail = {
442
+ id: null,
443
+ value: correctMap
444
+ };
445
+ return Promise.resolve({
446
+ isCorrect,
447
+ correctAnswer: correctAnswerDetail,
448
+ pointsEarned: isCorrect ? points : 0
449
+ });
450
+ }
451
+ };
452
+
453
+ // src/services/evaluators/fill-in-the-blanks-evaluator.ts
454
+ var FillInTheBlanksEvaluator = class {
455
+ async evaluate(question, answer) {
456
+ var _a;
457
+ const points = (_a = question.points) != null ? _a : 0;
458
+ let isCorrect = false;
459
+ if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
460
+ const userAnswerMap = answer;
461
+ isCorrect = question.answers.length > 0 && question.answers.every((correctAnsDef) => {
462
+ var _a2, _b;
463
+ const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
464
+ if (userValForBlank === void 0) return false;
465
+ const caseSensitive = (_b = question.isCaseSensitive) != null ? _b : false;
466
+ return correctAnsDef.acceptedValues.some(
467
+ (accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase()
468
+ );
469
+ });
470
+ }
471
+ const correctMap = question.answers.reduce((acc, curr) => {
472
+ acc[curr.blankId] = curr.acceptedValues.join(" | ");
473
+ return acc;
474
+ }, {});
475
+ const correctAnswerDetail = {
476
+ id: null,
477
+ value: correctMap
478
+ };
479
+ return Promise.resolve({
480
+ isCorrect,
481
+ correctAnswer: correctAnswerDetail,
482
+ pointsEarned: isCorrect ? points : 0
483
+ });
484
+ }
485
+ };
486
+
487
+ // src/services/evaluators/drag-and-drop-evaluator.ts
488
+ var DragAndDropEvaluator = class {
489
+ async evaluate(question, answer) {
490
+ var _a;
491
+ const points = (_a = question.points) != null ? _a : 0;
492
+ let isCorrect = false;
493
+ if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
494
+ const userAnswerMap = answer;
495
+ isCorrect = question.answerMap.length === Object.keys(userAnswerMap).length && question.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
496
+ }
497
+ const correctMap = question.answerMap.reduce((acc, curr) => {
498
+ var _a2, _b;
499
+ const draggableText = ((_a2 = question.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
500
+ const dropZoneText = ((_b = question.dropZones.find((z4) => z4.id === curr.dropZoneId)) == null ? void 0 : _b.label) || "";
501
+ acc[draggableText] = dropZoneText;
502
+ return acc;
503
+ }, {});
504
+ const correctAnswerDetail = {
505
+ id: null,
506
+ value: correctMap
507
+ };
508
+ return Promise.resolve({
509
+ isCorrect,
510
+ correctAnswer: correctAnswerDetail,
511
+ pointsEarned: isCorrect ? points : 0
512
+ });
513
+ }
514
+ };
515
+
516
+ // src/services/evaluators/hotspot-evaluator.ts
517
+ var HotspotEvaluator = class {
518
+ async evaluate(question, answer) {
519
+ var _a;
520
+ const points = (_a = question.points) != null ? _a : 0;
521
+ let isCorrect = false;
522
+ if (Array.isArray(answer)) {
523
+ const userAnswerSet = new Set(answer);
524
+ const correctAnswerSet = new Set(question.correctHotspotIds);
525
+ isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
526
+ }
527
+ const correctValues = question.correctHotspotIds.map(
528
+ (id) => {
529
+ var _a2;
530
+ return ((_a2 = question.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
531
+ }
532
+ );
533
+ const correctAnswerDetail = {
534
+ id: question.correctHotspotIds,
535
+ value: correctValues
536
+ };
537
+ return Promise.resolve({
538
+ isCorrect,
539
+ correctAnswer: correctAnswerDetail,
540
+ pointsEarned: isCorrect ? points : 0
541
+ });
542
+ }
543
+ };
544
+
545
+ // src/services/evaluators/programming-evaluator.ts
546
+ var ProgrammingEvaluator = class {
547
+ async evaluate(question, answer) {
548
+ var _a, _b;
549
+ const points = (_a = question.points) != null ? _a : 0;
550
+ let isCorrect = false;
551
+ if (typeof answer === "string" && typeof question.solutionGeneratedCode === "string") {
552
+ if (typeof window !== "undefined" && ((_b = window.Blockly) == null ? void 0 : _b.JavaScript)) {
553
+ const LocalBlockly = window.Blockly;
554
+ let generatedUserCode = "";
555
+ try {
556
+ const tempWorkspace = new LocalBlockly.Workspace();
557
+ const dom = LocalBlockly.Xml.textToDom(answer);
558
+ LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
559
+ generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
560
+ const normalize = (code) => code.replace(/\s+/g, " ").trim();
561
+ isCorrect = normalize(generatedUserCode) === normalize(question.solutionGeneratedCode);
562
+ tempWorkspace.dispose();
563
+ } catch (e) {
564
+ console.error(`Error generating code from user's ${question.questionType} XML for evaluation:`, e);
565
+ isCorrect = false;
566
+ }
567
+ } else {
568
+ console.warn(`Blockly library not available for ${question.questionType} evaluation. Skipping code comparison.`);
569
+ isCorrect = false;
570
+ }
571
+ }
572
+ const correctAnswerDetail = {
573
+ id: null,
574
+ value: question.solutionGeneratedCode || ""
575
+ };
576
+ return Promise.resolve({
577
+ isCorrect,
578
+ correctAnswer: correctAnswerDetail,
579
+ pointsEarned: isCorrect ? points : 0
580
+ });
581
+ }
582
+ };
583
+
584
+ // src/utils/jsonUtils.ts
585
+ var JsonRepairEngine = class {
586
+ /**
587
+ * Attempts to repair unterminated strings in JSON.
588
+ * NOTE: This is a heuristic approach and may not be perfect for all cases.
589
+ */
590
+ static repairUnterminatedStrings(jsonStr) {
591
+ let repaired = jsonStr;
592
+ let inString = false;
593
+ let escaped = false;
594
+ let lastQuoteIndex = -1;
595
+ for (let i = 0; i < repaired.length; i++) {
596
+ const char = repaired[i];
597
+ if (escaped) {
598
+ escaped = false;
599
+ continue;
600
+ }
601
+ if (char === "\\") {
602
+ escaped = true;
603
+ continue;
604
+ }
605
+ if (char === '"') {
606
+ inString = !inString;
607
+ if (inString) {
608
+ lastQuoteIndex = i;
609
+ }
610
+ }
611
+ }
612
+ if (inString && lastQuoteIndex !== -1) {
613
+ const beforeUnterminated = repaired.substring(0, lastQuoteIndex + 1);
614
+ const afterUnterminated = repaired.substring(lastQuoteIndex + 1);
615
+ const breakPoints = [",", "}", "]", "\n"];
616
+ let breakIndex = -1;
617
+ for (let i = 0; i < afterUnterminated.length; i++) {
618
+ if (breakPoints.includes(afterUnterminated[i])) {
619
+ breakIndex = i;
620
+ break;
621
+ }
622
+ }
623
+ if (breakIndex !== -1) {
624
+ const stringContent = afterUnterminated.substring(0, breakIndex);
625
+ const remainder = afterUnterminated.substring(breakIndex);
626
+ const escapedContent = stringContent.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
627
+ repaired = beforeUnterminated + escapedContent + '"' + remainder;
628
+ } else {
629
+ const escapedContent = afterUnterminated.replace(new RegExp('(?<!\\\\)"', "g"), '\\"');
630
+ repaired = beforeUnterminated + escapedContent + '"';
631
+ }
632
+ }
633
+ return repaired;
634
+ }
635
+ // FIX: Replaced unsafe single quote replacement with a stateful parser.
636
+ /**
637
+ * Safely replaces single quotes with double quotes only for keys and string values,
638
+ * ignoring apostrophes inside already double-quoted strings.
639
+ */
640
+ static safelyFixQuotes(jsonStr) {
641
+ let result = "";
642
+ let inDoubleQuoteString = false;
643
+ let escaped = false;
644
+ for (let i = 0; i < jsonStr.length; i++) {
645
+ const char = jsonStr[i];
646
+ if (escaped) {
647
+ result += char;
648
+ escaped = false;
649
+ continue;
650
+ }
651
+ if (char === "\\") {
652
+ escaped = true;
653
+ result += char;
654
+ continue;
655
+ }
656
+ if (char === '"') {
657
+ inDoubleQuoteString = !inDoubleQuoteString;
658
+ }
659
+ if (char === "'" && !inDoubleQuoteString) {
660
+ result += '"';
661
+ } else {
662
+ result += char;
663
+ }
664
+ }
665
+ return result;
666
+ }
667
+ /**
668
+ * Fixes common JSON formatting issues using more robust methods.
669
+ */
670
+ static applyCommonFixes(jsonStr) {
671
+ let fixed = jsonStr;
672
+ fixed = this.safelyFixQuotes(fixed);
673
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
674
+ fixed = fixed.replace(/("|}|\d|]|true|false|null)\s*\n\s*(")/g, "$1,\n$2");
675
+ fixed = fixed.replace(/"[\s\S]*?"/g, (match) => {
676
+ const content = match.substring(1, match.length - 1);
677
+ const fixedContent = content.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
678
+ return `"${fixedContent}"`;
679
+ });
680
+ fixed = fixed.replace(/"(true|false|null)"/g, "$1");
681
+ return fixed;
682
+ }
683
+ /**
684
+ * Validates JSON by attempting to parse and providing detailed error info.
685
+ */
686
+ static validateAndGetError(jsonStr) {
687
+ try {
688
+ JSON.parse(jsonStr);
689
+ return { isValid: true };
690
+ } catch (error) {
691
+ const errorMessage = error.message || "";
692
+ const positionMatch = errorMessage.match(/position (\d+)/);
693
+ const position = positionMatch ? parseInt(positionMatch[1], 10) : void 0;
694
+ return {
695
+ isValid: false,
696
+ error: errorMessage,
697
+ position
698
+ };
699
+ }
700
+ }
701
+ /**
702
+ * Main repair function that attempts multiple strategies.
703
+ */
704
+ static repairJson(jsonStr) {
705
+ var _a;
706
+ let current = jsonStr.trim();
707
+ const maxAttempts = 5;
708
+ let lastError = "";
709
+ let lastPosition = -1;
710
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
711
+ const validation = this.validateAndGetError(current);
712
+ if (validation.isValid) {
713
+ return current;
714
+ }
715
+ console.warn(`JSON repair attempt ${attempt + 1}: ${validation.error}`);
716
+ if (validation.error === lastError && validation.position === lastPosition) {
717
+ console.error("Repair attempt stuck on the same error, aborting this strategy.");
718
+ if (validation.position) {
719
+ const truncated = current.substring(0, validation.position);
720
+ const openBraces = (truncated.match(/{/g) || []).length;
721
+ const closeBraces = (truncated.match(/}/g) || []).length;
722
+ const openBrackets = (truncated.match(/\[/g) || []).length;
723
+ const closeBrackets = (truncated.match(/\]/g) || []).length;
724
+ let repaired = truncated.replace(/,\s*$/, "");
725
+ for (let i = 0; i < openBrackets - closeBrackets; i++) repaired += "]";
726
+ for (let i = 0; i < openBraces - closeBraces; i++) repaired += "}";
727
+ current = repaired;
728
+ const finalValidation = this.validateAndGetError(current);
729
+ if (finalValidation.isValid) return current;
730
+ }
731
+ break;
732
+ }
733
+ lastError = validation.error || "";
734
+ lastPosition = validation.position;
735
+ if ((_a = validation.error) == null ? void 0 : _a.includes("Unterminated string")) {
736
+ current = this.repairUnterminatedStrings(current);
737
+ } else {
738
+ current = this.applyCommonFixes(current);
739
+ }
740
+ }
741
+ try {
742
+ let finalAttempt = this.applyCommonFixes(jsonStr.trim());
743
+ finalAttempt = this.repairUnterminatedStrings(finalAttempt);
744
+ JSON.parse(finalAttempt);
745
+ return finalAttempt;
746
+ } catch (e) {
747
+ throw new Error(`Unable to repair JSON after ${maxAttempts} attempts. Last known error: ${lastError}`);
748
+ }
749
+ }
750
+ };
751
+ function extractJsonFromMarkdown(text) {
752
+ if (!text) {
753
+ throw new Error("Input text is empty or null.");
754
+ }
755
+ const trimmedText = text.trim();
756
+ try {
757
+ JSON.parse(trimmedText);
758
+ return trimmedText;
759
+ } catch (e) {
760
+ }
761
+ const markdownPatterns = [
762
+ /```(?:json|JSON)\s*([\s\S]*?)\s*```/,
763
+ // ```json ... ```
764
+ /```\s*({[\s\S]*?}|\[[\s\S]*?\])\s*```/
765
+ // ``` { ... } ``` or ``` [ ... ] ```
766
+ ];
767
+ for (const pattern of markdownPatterns) {
768
+ const match = trimmedText.match(pattern);
769
+ if (match && match[1]) {
770
+ const content = match[1].trim();
771
+ try {
772
+ JSON.parse(content);
773
+ return content;
774
+ } catch (e) {
775
+ console.warn("JSON inside markdown block is invalid, attempting repair...");
776
+ try {
777
+ return JsonRepairEngine.repairJson(content);
778
+ } catch (repairError) {
779
+ console.warn(`Markdown block repair failed: ${repairError.message}. Trying other strategies...`);
780
+ }
781
+ }
782
+ }
783
+ }
784
+ const firstBrace = trimmedText.indexOf("{");
785
+ const firstBracket = trimmedText.indexOf("[");
786
+ let startIndex = -1;
787
+ if (firstBrace === -1 && firstBracket === -1) ; else if (firstBrace === -1) {
788
+ startIndex = firstBracket;
789
+ } else if (firstBracket === -1) {
790
+ startIndex = firstBrace;
791
+ } else {
792
+ startIndex = Math.min(firstBrace, firstBracket);
793
+ }
794
+ if (startIndex !== -1) {
795
+ const textToProcess = trimmedText.substring(startIndex);
796
+ let balance = 0;
797
+ let inString = false;
798
+ let escaped = false;
799
+ const startChar = textToProcess[0];
800
+ const endChar = startChar === "{" ? "}" : "]";
801
+ for (let i = 0; i < textToProcess.length; i++) {
802
+ const char = textToProcess[i];
803
+ if (escaped) {
804
+ escaped = false;
805
+ continue;
806
+ }
807
+ if (char === "\\") {
808
+ escaped = true;
809
+ continue;
810
+ }
811
+ if (char === '"') {
812
+ inString = !inString;
813
+ }
814
+ if (!inString) {
815
+ if (char === startChar) balance++;
816
+ if (char === endChar) balance--;
817
+ }
818
+ if (balance === 0 && i > 0) {
819
+ const potentialJson = textToProcess.substring(0, i + 1);
820
+ try {
821
+ JSON.parse(potentialJson);
822
+ return potentialJson;
823
+ } catch (e) {
824
+ console.warn(`Balanced JSON segment is invalid, attempting repair...`);
825
+ try {
826
+ return JsonRepairEngine.repairJson(potentialJson);
827
+ } catch (repairError) {
828
+ console.warn(`Repair failed for balanced segment: ${repairError.message}`);
829
+ }
830
+ }
831
+ break;
832
+ }
833
+ }
834
+ }
835
+ console.warn("All extraction strategies failed, attempting to repair the entire input text as a last resort.");
836
+ try {
837
+ return JsonRepairEngine.repairJson(trimmedText);
838
+ } catch (finalError) {
839
+ throw new Error(`Unable to extract or repair valid JSON from AI response. Preview: "${trimmedText.substring(0, 100)}...". Final error: ${finalError.message}`);
840
+ }
841
+ }
842
+ z.object({
843
+ language: z.custom(),
844
+ problemPrompt: z.string(),
845
+ userCode: z.string(),
846
+ testCase: z.custom()
847
+ });
848
+ var AIEvaluationOutputSchema = z.object({
849
+ passed: z.boolean().describe("Did the user's code produce the expected output for the given input?"),
850
+ actualOutput: z.any().describe("The actual output produced by the user's code."),
851
+ reasoning: z.string().describe("A brief explanation of why the code passed or failed, or if there was a syntax error.")
852
+ });
853
+ var EvaluateUserCodeOutputSchema = AIEvaluationOutputSchema;
854
+
855
+ // src/ai/flows/evaluate-user-code.ts
856
+ async function evaluateUserCode(clientInput, apiKey) {
857
+ try {
858
+ const ai = genkit({
859
+ plugins: [googleAI({ apiKey })],
860
+ model: gemini20Flash
861
+ });
862
+ const { language, problemPrompt, userCode, testCase } = clientInput;
863
+ const promptText = `
864
+ You are an expert Code Judge and Teaching Assistant for a ${language} programming course.
865
+ Your task is to evaluate a student's code submission for a specific problem against a single test case.
866
+
867
+ ## Problem Description
868
+ ${problemPrompt}
869
+
870
+ ## Student's Code Submission
871
+ \`\`\`${language}
872
+ ${userCode}
873
+ \`\`\`
874
+
875
+ ## Test Case to Evaluate
876
+ - Input(s): ${JSON.stringify(testCase.input)}
877
+ - Expected Output: ${JSON.stringify(testCase.expectedOutput)}
878
+
879
+ ## Your Task
880
+ 1. **Analyze Execution:** Mentally execute the student's code with the provided input(s).
881
+ 2. **Determine Output:** Figure out what the actual output of the code would be.
882
+ 3. **Compare:** Compare the actual output with the expected output.
883
+ 4. **Handle Errors:** If the code has a syntax error or would crash, treat it as a failure.
884
+ 5. **Provide Reasoning:** Briefly explain your conclusion. If it failed, explain why (e.g., "incorrect result", "infinite loop", "syntax error on line 5").
885
+
886
+ **CRITICAL JSON OUTPUT FORMAT:**
887
+ Return ONLY the JSON object with this EXACT structure.
888
+
889
+ \`\`\`json
890
+ {
891
+ "passed": false,
892
+ "actualOutput": 5,
893
+ "reasoning": "The function correctly summed the numbers but did not filter for only even numbers."
894
+ }
895
+ \`\`\`
896
+
897
+ Return only the JSON response.`;
898
+ const response = await ai.generate(promptText);
899
+ const rawText = response.text;
900
+ const jsonText = extractJsonFromMarkdown(rawText);
901
+ const aiGeneratedContent = JSON.parse(jsonText);
902
+ return EvaluateUserCodeOutputSchema.parse(aiGeneratedContent);
903
+ } catch (error) {
904
+ console.error("Error evaluating user code:", error);
905
+ if (error instanceof z.ZodError) {
906
+ throw new Error(`AI evaluation output validation failed: ${error.message}`);
907
+ }
908
+ return {
909
+ passed: false,
910
+ actualOutput: "Evaluation Error",
911
+ reasoning: `The AI judge failed to process the code. Error: ${error.message}`
912
+ };
913
+ }
914
+ }
915
+
916
+ // src/services/APIKeyService.ts
917
+ var GEMINI_API_KEY_SERVICE_NAME = "gemini";
918
+ var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
919
+ function _encode(data) {
920
+ if (typeof window !== "undefined" && typeof window.btoa === "function") {
921
+ try {
922
+ return window.btoa(data);
923
+ } catch (e) {
924
+ console.error("Base64 encoding (btoa) failed:", e);
925
+ return data;
926
+ }
927
+ }
928
+ return data;
929
+ }
930
+ function _decode(data) {
931
+ if (typeof window !== "undefined" && typeof window.atob === "function") {
932
+ try {
933
+ return window.atob(data);
934
+ } catch (e) {
935
+ console.error("Base64 decoding (atob) failed:", e);
936
+ return data;
937
+ }
938
+ }
939
+ return data;
940
+ }
941
+ var APIKeyService = class {
942
+ static getStorageKey(serviceName) {
943
+ return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
944
+ }
945
+ /**
946
+ * Saves an API key to localStorage. The key is mildly obfuscated using Base64.
947
+ * @param serviceName - The name of the service (e.g., 'gemini').
948
+ * @param apiKey - The API key to save.
949
+ */
950
+ static saveAPIKey(serviceName, apiKey) {
951
+ if (typeof window !== "undefined" && window.localStorage) {
952
+ try {
953
+ const encodedKey = _encode(apiKey);
954
+ localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
955
+ } catch (e) {
956
+ console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
957
+ }
958
+ } else {
959
+ console.warn("localStorage is not available. APIKeyService cannot save keys.");
960
+ }
961
+ }
962
+ /**
963
+ * Retrieves an API key from localStorage.
964
+ * @param serviceName - The name of the service.
965
+ * @returns The decoded API key, or null if not found or if localStorage is unavailable.
966
+ */
967
+ static getAPIKey(serviceName) {
968
+ if (typeof window !== "undefined" && window.localStorage) {
969
+ try {
970
+ const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
971
+ if (storedKey) {
972
+ return _decode(storedKey);
973
+ }
974
+ } catch (e) {
975
+ console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
976
+ }
977
+ }
978
+ return null;
979
+ }
980
+ /**
981
+ * Removes an API key from localStorage.
982
+ * @param serviceName - The name of the service.
983
+ */
984
+ static removeAPIKey(serviceName) {
985
+ if (typeof window !== "undefined" && window.localStorage) {
986
+ try {
987
+ localStorage.removeItem(this.getStorageKey(serviceName));
988
+ } catch (e) {
989
+ console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
990
+ }
991
+ }
992
+ }
993
+ /**
994
+ * Checks if an API key exists in localStorage for the given service.
995
+ * @param serviceName - The name of the service.
996
+ * @returns True if a key exists, false otherwise.
997
+ */
998
+ static hasAPIKey(serviceName) {
999
+ return this.getAPIKey(serviceName) !== null;
1000
+ }
1001
+ };
1002
+
1003
+ // src/services/CodeEvaluationService.ts
1004
+ var CodeEvaluationService = class {
1005
+ constructor() {
1006
+ this.apiKey = APIKeyService.getAPIKey(GEMINI_API_KEY_SERVICE_NAME);
1007
+ }
1008
+ /**
1009
+ * Evaluates a user's code against a single test case using an AI judge.
1010
+ * @param question The full CodingQuestion object.
1011
+ * @param userCode The user's submitted code string.
1012
+ * @param testCase The specific TestCase to evaluate against.
1013
+ * @returns A promise that resolves to an EvaluationResult object.
1014
+ */
1015
+ async evaluateSingleTestCase(question, userCode, testCase) {
1016
+ if (!this.apiKey) {
1017
+ return {
1018
+ testCaseId: testCase.id,
1019
+ passed: false,
1020
+ actualOutput: "Configuration Error",
1021
+ reasoning: "API Key is not configured."
1022
+ };
1023
+ }
1024
+ const aiResult = await evaluateUserCode({
1025
+ language: question.language,
1026
+ problemPrompt: question.prompt,
1027
+ userCode,
1028
+ testCase
1029
+ }, this.apiKey);
1030
+ return __spreadValues({
1031
+ testCaseId: testCase.id
1032
+ }, aiResult);
1033
+ }
1034
+ /**
1035
+ * Evaluates user's code against all test cases for a given question.
1036
+ * @param question The full CodingQuestion object.
1037
+ * @param userCode The user's submitted code string.
1038
+ * @returns A promise that resolves to an array of EvaluationResult objects.
1039
+ */
1040
+ async evaluateAllTestCases(question, userCode) {
1041
+ const results = [];
1042
+ for (const testCase of question.testCases) {
1043
+ const result = await this.evaluateSingleTestCase(question, userCode, testCase);
1044
+ results.push(result);
1045
+ }
1046
+ return results;
1047
+ }
1048
+ /**
1049
+ * Evaluates user's code against only the public test cases for a given question.
1050
+ * Useful for a "Run Tests" button before final submission.
1051
+ * @param question The full CodingQuestion object.
1052
+ * @param userCode The user's submitted code string.
1053
+ * @returns A promise that resolves to an array of EvaluationResult objects.
1054
+ */
1055
+ async evaluatePublicTestCases(question, userCode) {
1056
+ const publicTestCases = question.testCases.filter((tc) => tc.isPublic);
1057
+ const results = [];
1058
+ for (const testCase of publicTestCases) {
1059
+ const result = await this.evaluateSingleTestCase(question, userCode, testCase);
1060
+ results.push(result);
1061
+ }
1062
+ return results;
1063
+ }
1064
+ };
1065
+
1066
+ // src/services/evaluators/coding-evaluator.ts
1067
+ var CodingEvaluator = class {
1068
+ async evaluate(question, answer) {
1069
+ var _a;
1070
+ const points = (_a = question.points) != null ? _a : 0;
1071
+ if (typeof answer !== "string" || !answer.trim()) {
1072
+ return {
1073
+ isCorrect: false,
1074
+ correctAnswer: { id: null, value: question.solutionCode },
1075
+ pointsEarned: 0,
1076
+ evaluationDetails: question.testCases.map((tc) => ({
1077
+ testCaseId: tc.id,
1078
+ passed: false,
1079
+ actualOutput: "No submission",
1080
+ reasoning: "User did not submit any code."
1081
+ }))
1082
+ };
1083
+ }
1084
+ try {
1085
+ const evaluationService = new CodeEvaluationService();
1086
+ const testCaseResults = await evaluationService.evaluateAllTestCases(question, answer);
1087
+ const isCorrect = testCaseResults.every((result) => result.passed);
1088
+ const correctAnswerDetail = {
1089
+ id: null,
1090
+ value: question.solutionCode
1091
+ };
1092
+ return {
1093
+ isCorrect,
1094
+ correctAnswer: correctAnswerDetail,
1095
+ pointsEarned: isCorrect ? points : 0,
1096
+ evaluationDetails: testCaseResults
1097
+ // Pass through the detailed results
1098
+ };
1099
+ } catch (error) {
1100
+ console.error("A critical error occurred during code evaluation:", error);
1101
+ return {
1102
+ isCorrect: false,
1103
+ correctAnswer: { id: null, value: question.solutionCode },
1104
+ pointsEarned: 0,
1105
+ evaluationDetails: question.testCases.map((tc) => ({
1106
+ testCaseId: tc.id,
1107
+ passed: false,
1108
+ actualOutput: "Evaluation Error",
1109
+ reasoning: error instanceof Error ? error.message : "An unknown error occurred."
1110
+ }))
1111
+ };
1112
+ }
1113
+ }
1114
+ };
1115
+
1116
+ // src/services/QuizEngine.ts
1117
+ var QuizEngine = class {
1118
+ constructor(options) {
1119
+ this.userAnswers = /* @__PURE__ */ new Map();
1120
+ this.currentQuestionIndex = 0;
1121
+ this.timerId = null;
1122
+ this.timeLeftInSeconds = null;
1123
+ this.scormService = null;
1124
+ this.quizResultState = { scormStatus: "idle" };
1125
+ this.questionStartTime = null;
1126
+ this.questionTimings = /* @__PURE__ */ new Map();
1127
+ var _a, _b, _c, _d, _e;
1128
+ this.config = options.config;
1129
+ this.callbacks = options.callbacks || {};
1130
+ this.questions = ((_a = this.config.settings) == null ? void 0 : _a.shuffleQuestions) ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
1131
+ this.overallStartTime = Date.now();
1132
+ this.evaluators = /* @__PURE__ */ new Map();
1133
+ this.registerEvaluators();
1134
+ if (((_b = this.config.settings) == null ? void 0 : _b.timeLimitMinutes) && this.config.settings.timeLimitMinutes > 0) {
1135
+ this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
1136
+ }
1137
+ if ((_c = this.config.settings) == null ? void 0 : _c.scorm) {
1138
+ this.quizResultState.scormStatus = "initializing";
1139
+ this.scormService = new SCORMService(this.config.settings.scorm);
1140
+ if (this.scormService.hasAPI()) {
1141
+ const initResult = this.scormService.initialize();
1142
+ if (initResult.success) {
1143
+ this.quizResultState.scormStatus = "initialized";
1144
+ this.quizResultState.studentName = initResult.studentName;
1145
+ } else {
1146
+ this.quizResultState.scormStatus = "error";
1147
+ this.quizResultState.scormError = initResult.error || "SCORM initialization failed.";
1148
+ }
1149
+ } else {
1150
+ this.quizResultState.scormStatus = "no_api";
1151
+ }
1152
+ }
1153
+ const initialQ = this.getCurrentQuestion();
1154
+ if (initialQ) {
1155
+ this.questionStartTime = Date.now();
1156
+ }
1157
+ if (this.callbacks.onQuizStart) {
1158
+ this.callbacks.onQuizStart({
1159
+ initialQuestion: initialQ,
1160
+ currentQuestionNumber: this.getCurrentQuestionNumber(),
1161
+ totalQuestions: this.getTotalQuestions(),
1162
+ timeLimitInSeconds: this.timeLeftInSeconds,
1163
+ scormStatus: this.quizResultState.scormStatus,
1164
+ studentName: this.quizResultState.studentName
1165
+ });
1166
+ }
1167
+ if (this.timeLeftInSeconds !== null) {
1168
+ this.startTimer();
1169
+ }
1170
+ (_e = (_d = this.callbacks).onQuestionChange) == null ? void 0 : _e.call(_d, initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1171
+ }
1172
+ registerEvaluators() {
1173
+ this.evaluators.set("multiple_choice", new MultipleChoiceEvaluator());
1174
+ this.evaluators.set("multiple_response", new MultipleResponseEvaluator());
1175
+ this.evaluators.set("true_false", new TrueFalseEvaluator());
1176
+ this.evaluators.set("short_answer", new ShortAnswerEvaluator());
1177
+ this.evaluators.set("numeric", new NumericEvaluator());
1178
+ this.evaluators.set("sequence", new SequenceEvaluator());
1179
+ this.evaluators.set("matching", new MatchingEvaluator());
1180
+ this.evaluators.set("fill_in_the_blanks", new FillInTheBlanksEvaluator());
1181
+ this.evaluators.set("drag_and_drop", new DragAndDropEvaluator());
1182
+ this.evaluators.set("hotspot", new HotspotEvaluator());
1183
+ const programmingEvaluator = new ProgrammingEvaluator();
1184
+ this.evaluators.set("blockly_programming", programmingEvaluator);
1185
+ this.evaluators.set("scratch_programming", programmingEvaluator);
1186
+ this.evaluators.set("coding", new CodingEvaluator());
1187
+ }
1188
+ _recordCurrentQuestionTime() {
1189
+ if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
1190
+ const currentQId = this.questions[this.currentQuestionIndex].id;
1191
+ const elapsedMs = Date.now() - this.questionStartTime;
1192
+ const currentTotalTime = this.questionTimings.get(currentQId) || 0;
1193
+ this.questionTimings.set(currentQId, currentTotalTime + elapsedMs / 1e3);
1194
+ }
1195
+ this.questionStartTime = null;
1196
+ }
1197
+ startTimer() {
1198
+ if (this.timerId !== null) clearInterval(this.timerId);
1199
+ this.timerId = setInterval(() => this.handleTick(), 1e3);
1200
+ }
1201
+ stopTimer() {
1202
+ if (this.timerId !== null) {
1203
+ clearInterval(this.timerId);
1204
+ this.timerId = null;
1205
+ }
1206
+ }
1207
+ handleTick() {
1208
+ var _a, _b, _c, _d;
1209
+ if (this.timeLeftInSeconds === null) return;
1210
+ if (this.timeLeftInSeconds > 0) {
1211
+ this.timeLeftInSeconds--;
1212
+ (_b = (_a = this.callbacks).onTimeTick) == null ? void 0 : _b.call(_a, this.timeLeftInSeconds);
1213
+ }
1214
+ if (this.timeLeftInSeconds <= 0) {
1215
+ this.stopTimer();
1216
+ (_d = (_c = this.callbacks).onQuizTimeUp) == null ? void 0 : _d.call(_c);
1217
+ this.calculateResults();
1218
+ }
1219
+ }
1220
+ getTimeLeftInSeconds() {
1221
+ return this.timeLeftInSeconds;
1222
+ }
1223
+ getCurrentQuestion() {
1224
+ return this.questions[this.currentQuestionIndex] || null;
1225
+ }
1226
+ getCurrentQuestionNumber() {
1227
+ return this.currentQuestionIndex + 1;
1228
+ }
1229
+ getTotalQuestions() {
1230
+ return this.questions.length;
1231
+ }
1232
+ getUserAnswer(questionId) {
1233
+ return this.userAnswers.get(questionId);
1234
+ }
1235
+ isQuizFinished() {
1236
+ return this.quizResultState.score !== void 0;
1237
+ }
1238
+ submitAnswer(questionId, answer) {
1239
+ var _a, _b;
1240
+ this.userAnswers.set(questionId, answer);
1241
+ const question = this.questions.find((q) => q.id === questionId);
1242
+ if (question) (_b = (_a = this.callbacks).onAnswerSubmit) == null ? void 0 : _b.call(_a, question, answer);
1243
+ }
1244
+ nextQuestion() {
1245
+ var _a, _b;
1246
+ this._recordCurrentQuestionTime();
1247
+ if (this.currentQuestionIndex < this.questions.length - 1) {
1248
+ this.currentQuestionIndex++;
1249
+ const currentQ = this.getCurrentQuestion();
1250
+ this.questionStartTime = Date.now();
1251
+ (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1252
+ return currentQ;
1253
+ }
1254
+ return null;
1255
+ }
1256
+ previousQuestion() {
1257
+ var _a, _b;
1258
+ this._recordCurrentQuestionTime();
1259
+ if (this.currentQuestionIndex > 0) {
1260
+ this.currentQuestionIndex--;
1261
+ const currentQ = this.getCurrentQuestion();
1262
+ this.questionStartTime = Date.now();
1263
+ (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1264
+ return currentQ;
1265
+ }
1266
+ return null;
1267
+ }
1268
+ goToQuestion(index) {
1269
+ var _a, _b;
1270
+ if (index >= 0 && index < this.questions.length && index !== this.currentQuestionIndex) {
1271
+ this._recordCurrentQuestionTime();
1272
+ this.currentQuestionIndex = index;
1273
+ const currentQ = this.getCurrentQuestion();
1274
+ this.questionStartTime = Date.now();
1275
+ (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1276
+ return currentQ;
1277
+ }
1278
+ return this.getCurrentQuestion();
1279
+ }
1280
+ getElapsedTime() {
1281
+ return Date.now() - this.overallStartTime;
1282
+ }
1283
+ destroy() {
1284
+ this.stopTimer();
1285
+ this._recordCurrentQuestionTime();
1286
+ if (this.scormService && this.scormService.hasAPI()) {
1287
+ if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
1288
+ const termResult = this.scormService.terminate();
1289
+ if (termResult.success) {
1290
+ this.quizResultState.scormStatus = "terminated";
1291
+ } else {
1292
+ this.quizResultState.scormStatus = "error";
1293
+ this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
1294
+ }
1295
+ }
1296
+ }
1297
+ this.scormService = null;
1298
+ }
1299
+ // (Tiếp theo từ Phần 1)
1300
+ async calculateResults() {
1301
+ var _a, _b, _c, _d, _e;
1302
+ this.stopTimer();
1303
+ this._recordCurrentQuestionTime();
1304
+ let totalScore = 0;
1305
+ let maxScore = 0;
1306
+ const questionResultsArray = [];
1307
+ let accumulatedTotalTimeSpent = 0;
1308
+ for (const question of this.questions) {
1309
+ const userAnswerRaw = this.userAnswers.get(question.id) || null;
1310
+ maxScore += (_a = question.points) != null ? _a : 0;
1311
+ const evaluator = this.evaluators.get(question.questionType);
1312
+ if (!evaluator) {
1313
+ console.warn(`No evaluator found for question type: ${question.questionType}`);
1314
+ questionResultsArray.push({
1315
+ questionId: question.id,
1316
+ questionType: question.questionType,
1317
+ prompt: question.prompt,
1318
+ isCorrect: false,
1319
+ pointsEarned: 0,
1320
+ userAnswer: { id: null, value: userAnswerRaw },
1321
+ correctAnswer: { id: null, value: "Evaluation not implemented." },
1322
+ timeSpentSeconds: parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2))
1323
+ });
1324
+ continue;
1325
+ }
1326
+ const {
1327
+ isCorrect,
1328
+ correctAnswer: correctAnswerDetail,
1329
+ pointsEarned,
1330
+ evaluationDetails
1331
+ } = await evaluator.evaluate(question, userAnswerRaw);
1332
+ totalScore += pointsEarned;
1333
+ const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
1334
+ accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
1335
+ const userAnswerDetail = this.formatUserAnswerDetail(question, userAnswerRaw);
1336
+ questionResultsArray.push({
1337
+ questionId: question.id,
1338
+ questionType: question.questionType,
1339
+ prompt: question.prompt,
1340
+ isCorrect,
1341
+ pointsEarned,
1342
+ userAnswer: userAnswerDetail,
1343
+ correctAnswer: correctAnswerDetail,
1344
+ timeSpentSeconds: timeSpentOnThisQuestion,
1345
+ evaluationDetails
1346
+ });
1347
+ }
1348
+ const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
1349
+ let passed = void 0;
1350
+ if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) != null) {
1351
+ passed = percentage >= this.config.settings.passingScorePercent;
1352
+ }
1353
+ const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
1354
+ const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
1355
+ const metadataPerformance = await this._calculateMetadataPerformance();
1356
+ const finalResults = __spreadValues({
1357
+ score: totalScore,
1358
+ maxScore,
1359
+ percentage,
1360
+ answers: this.userAnswers,
1361
+ questionResults: questionResultsArray,
1362
+ passed,
1363
+ webhookStatus: "idle",
1364
+ scormStatus: this.quizResultState.scormStatus || "idle",
1365
+ scormError: this.quizResultState.scormError,
1366
+ studentName: this.quizResultState.studentName,
1367
+ totalTimeSpentSeconds: totalQuizTimeSpentSeconds,
1368
+ averageTimePerQuestionSeconds
1369
+ }, metadataPerformance);
1370
+ this.quizResultState = __spreadValues(__spreadValues({}, this.quizResultState), finalResults);
1371
+ if ((_c = this.config.settings) == null ? void 0 : _c.scorm) this._sendResultsToSCORM(finalResults);
1372
+ await this._sendResultsToWebhook(finalResults);
1373
+ (_e = (_d = this.callbacks).onQuizFinish) == null ? void 0 : _e.call(_d, finalResults);
1374
+ return finalResults;
1375
+ }
1376
+ formatUserAnswerDetail(question, userAnswerRaw) {
1377
+ var _a, _b, _c, _d, _e;
1378
+ if (userAnswerRaw === null) return null;
1379
+ switch (question.questionType) {
1380
+ case "multiple_choice": {
1381
+ const q = question;
1382
+ const id = userAnswerRaw;
1383
+ return { id, value: ((_a = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a.text) || "" };
1384
+ }
1385
+ case "multiple_response": {
1386
+ const q = question;
1387
+ const ids = userAnswerRaw;
1388
+ const values = ids.map((id) => {
1389
+ var _a2;
1390
+ return ((_a2 = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
1391
+ });
1392
+ return { id: ids, value: values };
1393
+ }
1394
+ case "sequence": {
1395
+ const q = question;
1396
+ const ids = userAnswerRaw;
1397
+ const values = ids.map((id) => {
1398
+ var _a2;
1399
+ return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
1400
+ });
1401
+ return { id: ids, value: values };
1402
+ }
1403
+ case "matching": {
1404
+ const q = question;
1405
+ const userAnswerMap = userAnswerRaw;
1406
+ const valueMap = {};
1407
+ for (const promptId in userAnswerMap) {
1408
+ const optionId = userAnswerMap[promptId];
1409
+ const promptText = ((_b = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _b.content) || "";
1410
+ const optionText = ((_c = q.options.find((o) => o.id === optionId)) == null ? void 0 : _c.content) || "";
1411
+ valueMap[promptText] = optionText;
1412
+ }
1413
+ return { id: null, value: valueMap };
1414
+ }
1415
+ case "drag_and_drop": {
1416
+ const q = question;
1417
+ if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
1418
+ const userAnswerMapByIds = userAnswerRaw;
1419
+ const enrichedUserAnswerMap = {};
1420
+ for (const draggableId in userAnswerMapByIds) {
1421
+ const dropZoneId = userAnswerMapByIds[draggableId];
1422
+ const draggableText = ((_d = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _d.content) || `(ID: ${draggableId})`;
1423
+ const dropZoneText = ((_e = q.dropZones.find((z4) => z4.id === dropZoneId)) == null ? void 0 : _e.label) || `(ID: ${dropZoneId})`;
1424
+ enrichedUserAnswerMap[draggableText] = dropZoneText;
1425
+ }
1426
+ return { id: null, value: enrichedUserAnswerMap };
1427
+ }
1428
+ return { id: null, value: userAnswerRaw };
1429
+ }
1430
+ default:
1431
+ return { id: null, value: userAnswerRaw };
1432
+ }
1433
+ }
1434
+ async _calculateMetadataPerformance() {
1435
+ var _a;
1436
+ const loPerformanceMap = /* @__PURE__ */ new Map();
1437
+ const categoryPerformanceMap = /* @__PURE__ */ new Map();
1438
+ const topicPerformanceMap = /* @__PURE__ */ new Map();
1439
+ const difficultyPerformanceMap = /* @__PURE__ */ new Map();
1440
+ const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
1441
+ const updateMap = (map, key, points, isCorrect) => {
1442
+ if (!key) return;
1443
+ const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
1444
+ current.totalQuestions++;
1445
+ current.maxPoints += points;
1446
+ if (isCorrect) {
1447
+ current.correctQuestions++;
1448
+ current.pointsEarned += points;
1449
+ }
1450
+ map.set(key, current);
1451
+ };
1452
+ for (const q of this.questions) {
1453
+ const userAnswer = this.userAnswers.get(q.id) || null;
1454
+ const evaluator = this.evaluators.get(q.questionType);
1455
+ if (evaluator) {
1456
+ const { isCorrect } = await evaluator.evaluate(q, userAnswer);
1457
+ const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
1458
+ updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
1459
+ updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
1460
+ updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
1461
+ updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
1462
+ updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
1463
+ }
1464
+ }
1465
+ const formatPerformanceArray = (map, keyName) => {
1466
+ return Array.from(map.entries()).map(([key, data]) => ({
1467
+ [keyName]: key,
1468
+ totalQuestions: data.totalQuestions,
1469
+ correctQuestions: data.correctQuestions,
1470
+ pointsEarned: data.pointsEarned,
1471
+ maxPoints: data.maxPoints,
1472
+ percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
1473
+ }));
1474
+ };
1475
+ return {
1476
+ performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
1477
+ performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
1478
+ performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
1479
+ performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
1480
+ performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
1481
+ };
1482
+ }
1483
+ async _sendResultsToWebhook(results) {
1484
+ var _a;
1485
+ if (!((_a = this.config.settings) == null ? void 0 : _a.webhookUrl)) {
1486
+ results.webhookStatus = "idle";
1487
+ return;
1488
+ }
1489
+ results.webhookStatus = "sending";
1490
+ try {
1491
+ const response = await fetch(this.config.settings.webhookUrl, {
1492
+ method: "POST",
1493
+ headers: { "Content-Type": "application/json" },
1494
+ body: JSON.stringify(results)
1495
+ });
1496
+ if (response.ok) {
1497
+ results.webhookStatus = "success";
1498
+ } else {
1499
+ results.webhookStatus = "error";
1500
+ results.webhookError = `Webhook returned status: ${response.status} ${response.statusText}`;
1501
+ try {
1502
+ const errorBody = await response.text();
1503
+ results.webhookError += ` - Body: ${errorBody.substring(0, 200)}`;
1504
+ } catch (e) {
1505
+ }
1506
+ }
1507
+ } catch (error) {
1508
+ results.webhookStatus = "error";
1509
+ results.webhookError = error instanceof Error ? `Fetch error: ${error.message}` : "Unknown webhook error.";
1510
+ }
1511
+ }
1512
+ _sendResultsToSCORM(results) {
1513
+ var _a, _b, _c, _d, _e, _f, _g;
1514
+ if (!this.scormService || !this.scormService.hasAPI() || this.quizResultState.scormStatus === "no_api") {
1515
+ results.scormStatus = this.quizResultState.scormStatus || "idle";
1516
+ return;
1517
+ }
1518
+ if (this.quizResultState.scormStatus === "error" && ((_a = this.quizResultState.scormError) == null ? void 0 : _a.includes("initialization failed"))) {
1519
+ results.scormStatus = "error";
1520
+ results.scormError = this.quizResultState.scormError;
1521
+ return;
1522
+ }
1523
+ results.scormStatus = "sending_data";
1524
+ try {
1525
+ this.scormService.setScore(results.score, results.maxScore, 0);
1526
+ let lessonStatusSetting = "completed";
1527
+ if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) !== void 0 && ((_c = this.config.settings) == null ? void 0 : _c.passingScorePercent) !== null) {
1528
+ lessonStatusSetting = results.passed ? "passed" : "failed";
1529
+ } else if ((_e = (_d = this.config.settings) == null ? void 0 : _d.scorm) == null ? void 0 : _e.setCompletionOnFinish) {
1530
+ lessonStatusSetting = "completed";
1531
+ }
1532
+ this.scormService.setLessonStatus(lessonStatusSetting, results.passed);
1533
+ if (results.totalTimeSpentSeconds !== void 0 && this.scormService.formatCMITime) {
1534
+ const cmiTime = this.scormService.formatCMITime(results.totalTimeSpentSeconds);
1535
+ 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");
1536
+ if (sessionTimeVar) this.scormService.setValue(sessionTimeVar, cmiTime);
1537
+ }
1538
+ const commitResult = this.scormService.commit();
1539
+ if (commitResult.success) {
1540
+ results.scormStatus = "committed";
1541
+ } else {
1542
+ results.scormStatus = "error";
1543
+ results.scormError = commitResult.error || "SCORM commit failed.";
1544
+ }
1545
+ } catch (e) {
1546
+ results.scormStatus = "error";
1547
+ results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
1548
+ }
1549
+ }
1550
+ };
1551
+
1552
+ // src/utils/idGenerators.ts
1553
+ function generateUniqueId(prefix = "id_") {
1554
+ return prefix + Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
1555
+ }
1556
+
1557
+ // src/services/QuizEditorService.ts
1558
+ var QuizEditorService = class {
1559
+ constructor(initialQuiz) {
1560
+ this.quiz = JSON.parse(JSON.stringify(initialQuiz));
1561
+ }
1562
+ getQuiz() {
1563
+ return this.quiz;
1564
+ }
1565
+ static createNewQuestionTemplate(type) {
1566
+ const baseNewQuestion = {
1567
+ id: generateUniqueId(`new_${type}_`),
1568
+ questionType: type,
1569
+ prompt: "",
1570
+ points: 10,
1571
+ difficulty: "medium"
1572
+ };
1573
+ switch (type) {
1574
+ case "true_false":
1575
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "true_false", correctAnswer: true });
1576
+ case "multiple_choice":
1577
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_choice", options: [], correctAnswerId: "" });
1578
+ case "multiple_response":
1579
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_response", options: [], correctAnswerIds: [] });
1580
+ case "short_answer":
1581
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "short_answer", acceptedAnswers: [""], isCaseSensitive: false });
1582
+ case "numeric":
1583
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "numeric", answer: 0 });
1584
+ case "fill_in_the_blanks": {
1585
+ const blankId = generateUniqueId("blank_");
1586
+ return __spreadProps(__spreadValues({}, baseNewQuestion), {
1587
+ questionType: "fill_in_the_blanks",
1588
+ segments: [
1589
+ { type: "text", content: "Your text before " },
1590
+ { type: "blank", id: blankId },
1591
+ { type: "text", content: " and after." }
1592
+ ],
1593
+ answers: [{ blankId, acceptedValues: [""] }],
1594
+ isCaseSensitive: false
1595
+ });
1596
+ }
1597
+ case "sequence":
1598
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "sequence", items: [], correctOrder: [] });
1599
+ case "matching":
1600
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "matching", prompts: [], options: [], correctAnswerMap: [], shuffleOptions: true });
1601
+ case "drag_and_drop":
1602
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "drag_and_drop", draggableItems: [], dropZones: [], answerMap: [] });
1603
+ case "hotspot":
1604
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "hotspot", imageUrl: "", hotspots: [], correctHotspotIds: [] });
1605
+ case "blockly_programming":
1606
+ return __spreadProps(__spreadValues({}, baseNewQuestion), {
1607
+ questionType: "blockly_programming",
1608
+ toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
1609
+ initialWorkspace: "",
1610
+ solutionWorkspaceXML: "",
1611
+ solutionGeneratedCode: ""
1612
+ });
1613
+ case "scratch_programming":
1614
+ return __spreadProps(__spreadValues({}, baseNewQuestion), {
1615
+ questionType: "scratch_programming",
1616
+ toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
1617
+ initialWorkspace: "",
1618
+ solutionWorkspaceXML: "",
1619
+ solutionGeneratedCode: ""
1620
+ });
1621
+ case "coding":
1622
+ return __spreadProps(__spreadValues({}, baseNewQuestion), {
1623
+ questionType: "coding",
1624
+ language: "javascript",
1625
+ solutionCode: "",
1626
+ testCases: [],
1627
+ functionSignature: "",
1628
+ points: 25
1629
+ // Coding questions are worth more by default
1630
+ });
1631
+ default:
1632
+ const _exhaustiveCheck = type;
1633
+ throw new Error(`Question type "${_exhaustiveCheck}" is not supported for creation.`);
1634
+ }
1635
+ }
1636
+ addQuestion(question) {
1637
+ const newQuestion = __spreadValues({}, question);
1638
+ if (newQuestion.id.startsWith("new_")) {
1639
+ newQuestion.id = generateUniqueId(`${newQuestion.questionType}_`);
1640
+ }
1641
+ this.quiz.questions.push(newQuestion);
1642
+ return this.quiz;
1643
+ }
1644
+ updateQuestion(updatedQuestion) {
1645
+ const questionIndex = this.quiz.questions.findIndex((q) => q.id === updatedQuestion.id);
1646
+ if (questionIndex === -1) {
1647
+ throw new Error(`Question with ID "${updatedQuestion.id}" not found.`);
1648
+ }
1649
+ this.quiz.questions[questionIndex] = updatedQuestion;
1650
+ return this.quiz;
1651
+ }
1652
+ deleteQuestionByIndex(index) {
1653
+ if (index < 0 || index >= this.quiz.questions.length) {
1654
+ throw new Error(`Invalid index ${index} for question deletion.`);
1655
+ }
1656
+ this.quiz.questions.splice(index, 1);
1657
+ return this.quiz;
1658
+ }
1659
+ moveQuestion(fromIndex, toIndex) {
1660
+ if (fromIndex < 0 || fromIndex >= this.quiz.questions.length || toIndex < 0 || toIndex >= this.quiz.questions.length) {
1661
+ throw new Error("Invalid index for moving question.");
1662
+ }
1663
+ const [movedItem] = this.quiz.questions.splice(fromIndex, 1);
1664
+ this.quiz.questions.splice(toIndex, 0, movedItem);
1665
+ return this.quiz;
1666
+ }
1667
+ };
1668
+ var BaseRawQuestionSchema = z.object({
1669
+ questionType: z.string(),
1670
+ prompt: z.string().min(1, { message: "Prompt cannot be empty." }),
1671
+ points: z.number().optional(),
1672
+ explanation: z.string().optional(),
1673
+ topic: z.string().optional(),
1674
+ difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1675
+ bloomLevel: z.string().optional()
1676
+ });
1677
+ var RawMCQSchema = BaseRawQuestionSchema.extend({
1678
+ questionType: z.literal("multiple_choice"),
1679
+ options: z.array(z.string()).min(2, { message: "Multiple Choice questions need at least 2 options." }),
1680
+ correctAnswer: z.string({ required_error: "A correct answer is required." })
1681
+ });
1682
+ var RawMRQSchema = BaseRawQuestionSchema.extend({
1683
+ questionType: z.literal("multiple_response"),
1684
+ options: z.array(z.string()).min(2, { message: "Multiple Response questions need at least 2 options." }),
1685
+ correctAnswers: z.array(z.string()).min(1, { message: "At least one correct answer is required." })
1686
+ });
1687
+ var RawTFSchema = BaseRawQuestionSchema.extend({
1688
+ questionType: z.literal("true_false"),
1689
+ correctAnswer: z.boolean({ required_error: "A correct answer (true/false) is required." })
1690
+ });
1691
+ var RawSASchema = BaseRawQuestionSchema.extend({
1692
+ questionType: z.literal("short_answer"),
1693
+ acceptedAnswers: z.array(z.string()).min(1, { message: "At least one accepted answer is required." })
1694
+ });
1695
+ var RawNumericSchema = BaseRawQuestionSchema.extend({
1696
+ questionType: z.literal("numeric"),
1697
+ answer: z.number({ required_error: "A numeric answer is required." }),
1698
+ tolerance: z.number().optional()
1699
+ });
1700
+ var RawSequenceSchema = BaseRawQuestionSchema.extend({
1701
+ questionType: z.literal("sequence"),
1702
+ items: z.array(z.string()).min(2, { message: "Sequence questions need at least 2 items." }),
1703
+ correctOrder: z.array(z.string()).min(2)
1704
+ });
1705
+ var RawMatchingSchema = BaseRawQuestionSchema.extend({
1706
+ questionType: z.literal("matching"),
1707
+ prompts: z.array(z.string()).min(1),
1708
+ options: z.array(z.string()).min(1),
1709
+ correctAnswerMap: z.record(z.string(), { required_error: "A map of correct answers is required." })
1710
+ });
1711
+ var RawFITBSchema = BaseRawQuestionSchema.extend({
1712
+ questionType: z.literal("fill_in_the_blanks"),
1713
+ sentenceWithPlaceholders: z.string().includes("{{", { message: "Sentence must contain at least one placeholder like {{placeholder}}." }),
1714
+ blanks: z.record(z.array(z.string()).min(1), { required_error: "Blanks definitions are required." })
1715
+ });
1716
+ var AnyRawQuestionSchema = z.discriminatedUnion("questionType", [
1717
+ RawMCQSchema,
1718
+ RawMRQSchema,
1719
+ RawTFSchema,
1720
+ RawSASchema,
1721
+ RawNumericSchema,
1722
+ RawSequenceSchema,
1723
+ RawMatchingSchema,
1724
+ RawFITBSchema
1725
+ ]);
1726
+ var QuestionImportService = class {
1727
+ static processJSON(jsonString) {
1728
+ try {
1729
+ const rawData = JSON.parse(jsonString);
1730
+ if (!Array.isArray(rawData)) {
1731
+ return { validQuestions: [], errors: [{ index: 0, message: "JSON content must be an array of question objects.", data: rawData }] };
1732
+ }
1733
+ return this.processRawObjects(rawData);
1734
+ } catch (e) {
1735
+ const message = e instanceof Error ? e.message : "Invalid JSON format.";
1736
+ return { validQuestions: [], errors: [{ index: 0, message, data: jsonString.substring(0, 500) }] };
1737
+ }
1738
+ }
1739
+ static processTSV(tsvString) {
1740
+ const lines = tsvString.split(/\r?\n/).filter((line) => line.trim() !== "");
1741
+ if (lines.length < 2) {
1742
+ return { validQuestions: [], errors: [{ index: 0, message: "TSV file must have a header and at least one data row.", data: tsvString }] };
1743
+ }
1744
+ const header = lines.shift().split(" ").map((h) => h.trim());
1745
+ const rawObjects = [];
1746
+ const errors = [];
1747
+ lines.forEach((line, index) => {
1748
+ const values = line.split(" ");
1749
+ const rowObject = {};
1750
+ header.forEach((h, i) => {
1751
+ var _a;
1752
+ rowObject[h] = ((_a = values[i]) == null ? void 0 : _a.trim()) || "";
1753
+ });
1754
+ try {
1755
+ const transformedObject = this.transformTsvRowToRawObject(rowObject);
1756
+ rawObjects.push(transformedObject);
1757
+ } catch (e) {
1758
+ const message = e instanceof Error ? e.message : "Error transforming TSV row.";
1759
+ errors.push({ index: index + 2, message, data: line });
1760
+ }
1761
+ });
1762
+ const processedResult = this.processRawObjects(rawObjects);
1763
+ return { validQuestions: processedResult.validQuestions, errors: [...errors, ...processedResult.errors] };
1764
+ }
1765
+ static processRawObjects(rawObjects) {
1766
+ const validQuestions = [];
1767
+ const errors = [];
1768
+ rawObjects.forEach((rawQ, index) => {
1769
+ try {
1770
+ const validatedRawQ = AnyRawQuestionSchema.parse(rawQ);
1771
+ const question = this.createQuestionFromRawObject(validatedRawQ);
1772
+ validQuestions.push(question);
1773
+ } catch (e) {
1774
+ const message = e instanceof z.ZodError ? e.errors.map((err) => `${err.path.join(".")} - ${err.message}`).join("; ") : e instanceof Error ? e.message : "Unknown validation error.";
1775
+ errors.push({ index: index + 1, message, data: rawQ });
1776
+ }
1777
+ });
1778
+ return { validQuestions, errors };
1779
+ }
1780
+ static transformTsvRowToRawObject(row) {
1781
+ const { questionType, prompt, options, correctAnswer, points, explanation, topic, difficulty, bloomLevel, tolerance } = row;
1782
+ if (!questionType) throw new Error("`questionType` column is missing or empty.");
1783
+ const base = {
1784
+ questionType,
1785
+ prompt,
1786
+ points: points ? parseInt(points, 10) : void 0,
1787
+ explanation,
1788
+ topic,
1789
+ difficulty,
1790
+ bloomLevel
1791
+ };
1792
+ switch (questionType) {
1793
+ case "multiple_choice":
1794
+ return __spreadProps(__spreadValues({}, base), { options: options.split("|"), correctAnswer });
1795
+ case "multiple_response":
1796
+ return __spreadProps(__spreadValues({}, base), { options: options.split("|"), correctAnswers: correctAnswer.split("|") });
1797
+ case "true_false":
1798
+ return __spreadProps(__spreadValues({}, base), { correctAnswer: correctAnswer.toLowerCase() === "true" });
1799
+ case "short_answer":
1800
+ return __spreadProps(__spreadValues({}, base), { acceptedAnswers: correctAnswer.split("|") });
1801
+ case "numeric":
1802
+ return __spreadProps(__spreadValues({}, base), { answer: parseFloat(correctAnswer), tolerance: tolerance ? parseFloat(tolerance) : void 0 });
1803
+ case "sequence":
1804
+ return __spreadProps(__spreadValues({}, base), { items: options.split("|"), correctOrder: correctAnswer.split("|") });
1805
+ case "matching": {
1806
+ const [promptsStr, optionsStr] = options.split("#");
1807
+ const prompts = promptsStr.replace("prompts:", "").split("|");
1808
+ const opts = optionsStr.replace("options:", "").split("|");
1809
+ const correctAnswerMap = correctAnswer.split("|").reduce((acc, pair) => {
1810
+ const [key, ...valParts] = pair.split(":");
1811
+ acc[key] = valParts.join(":");
1812
+ return acc;
1813
+ }, {});
1814
+ return __spreadProps(__spreadValues({}, base), { prompts, options: opts, correctAnswerMap });
1815
+ }
1816
+ case "fill_in_the_blanks": {
1817
+ const blanks = correctAnswer.split("#").reduce((acc, part) => {
1818
+ const [key, valuesStr] = part.split(":");
1819
+ acc[key] = valuesStr.split("|");
1820
+ return acc;
1821
+ }, {});
1822
+ return __spreadProps(__spreadValues({}, base), { sentenceWithPlaceholders: options, blanks });
1823
+ }
1824
+ default:
1825
+ throw new Error(`Unsupported questionType "${questionType}" in TSV.`);
1826
+ }
1827
+ }
1828
+ static createQuestionFromRawObject(validatedRawQ) {
1829
+ const baseQuestionData = {
1830
+ id: generateUniqueId(validatedRawQ.questionType),
1831
+ prompt: validatedRawQ.prompt,
1832
+ points: validatedRawQ.points,
1833
+ explanation: validatedRawQ.explanation,
1834
+ topic: validatedRawQ.topic,
1835
+ difficulty: validatedRawQ.difficulty,
1836
+ bloomLevel: validatedRawQ.bloomLevel
1837
+ };
1838
+ switch (validatedRawQ.questionType) {
1839
+ case "multiple_choice": {
1840
+ const options = validatedRawQ.options.map((text) => ({ id: generateUniqueId("opt_"), text }));
1841
+ const correctOption = options.find((opt) => opt.text === validatedRawQ.correctAnswer);
1842
+ if (!correctOption) throw new Error(`Correct answer "${validatedRawQ.correctAnswer}" not found in options.`);
1843
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "multiple_choice", options, correctAnswerId: correctOption.id });
1844
+ }
1845
+ case "multiple_response": {
1846
+ const options = validatedRawQ.options.map((text) => ({ id: generateUniqueId("opt_mr_"), text }));
1847
+ const correctIds = options.filter((opt) => validatedRawQ.correctAnswers.includes(opt.text)).map((opt) => opt.id);
1848
+ if (correctIds.length !== validatedRawQ.correctAnswers.length) throw new Error("Some correct answers were not found in options.");
1849
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "multiple_response", options, correctAnswerIds: correctIds });
1850
+ }
1851
+ case "true_false":
1852
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "true_false", correctAnswer: validatedRawQ.correctAnswer });
1853
+ case "short_answer":
1854
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "short_answer", acceptedAnswers: validatedRawQ.acceptedAnswers, isCaseSensitive: false });
1855
+ case "numeric":
1856
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "numeric", answer: validatedRawQ.answer, tolerance: validatedRawQ.tolerance });
1857
+ case "sequence": {
1858
+ if (validatedRawQ.items.length !== validatedRawQ.correctOrder.length) {
1859
+ throw new Error("The number of items must match the number of items in the correct order for a sequence question.");
1860
+ }
1861
+ const items = validatedRawQ.items.map((content) => ({ id: generateUniqueId("seqi_"), content }));
1862
+ const correctOrder = validatedRawQ.correctOrder.map((orderText) => {
1863
+ const foundItem = items.find((item) => item.content === orderText);
1864
+ if (!foundItem) throw new Error(`Sequence item "${orderText}" in correctOrder not found in items list.`);
1865
+ return foundItem.id;
1866
+ });
1867
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "sequence", items, correctOrder });
1868
+ }
1869
+ case "matching": {
1870
+ if (validatedRawQ.prompts.length !== Object.keys(validatedRawQ.correctAnswerMap).length) {
1871
+ throw new Error("Each prompt must have a corresponding correct answer in the map for a matching question.");
1872
+ }
1873
+ const prompts = validatedRawQ.prompts.map((p) => ({ id: generateUniqueId("matp_"), content: p }));
1874
+ const options = validatedRawQ.options.map((o) => ({ id: generateUniqueId("mato_"), content: o }));
1875
+ const correctAnswerMap = Object.entries(validatedRawQ.correctAnswerMap).map(([promptText, optionText]) => {
1876
+ const prompt = prompts.find((p) => p.content === promptText);
1877
+ const option = options.find((o) => o.content === optionText);
1878
+ if (!prompt || !option) throw new Error(`Matching pair "${promptText}":"${optionText}" not found in prompts/options.`);
1879
+ return { promptId: prompt.id, optionId: option.id };
1880
+ });
1881
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "matching", prompts, options, correctAnswerMap, shuffleOptions: true });
1882
+ }
1883
+ case "fill_in_the_blanks": {
1884
+ const { sentenceWithPlaceholders, blanks } = validatedRawQ;
1885
+ const segments = [];
1886
+ const answers = [];
1887
+ const placeholderMap = {};
1888
+ Object.keys(blanks).forEach((placeholder) => {
1889
+ const blankId = generateUniqueId("blank_");
1890
+ placeholderMap[placeholder] = blankId;
1891
+ answers.push({ blankId, acceptedValues: blanks[placeholder] });
1892
+ });
1893
+ const regex = /\{\{([^}]+)\}\}/g;
1894
+ let lastIndex = 0;
1895
+ let match;
1896
+ while ((match = regex.exec(sentenceWithPlaceholders)) !== null) {
1897
+ if (match.index > lastIndex) {
1898
+ segments.push({ type: "text", content: sentenceWithPlaceholders.substring(lastIndex, match.index) });
1899
+ }
1900
+ const placeholder = match[1];
1901
+ const blankId = placeholderMap[placeholder];
1902
+ if (!blankId) throw new Error(`Placeholder "{{${placeholder}}}" found in sentence but not defined in blanks object.`);
1903
+ segments.push({ type: "blank", id: blankId });
1904
+ lastIndex = regex.lastIndex;
1905
+ }
1906
+ if (lastIndex < sentenceWithPlaceholders.length) {
1907
+ segments.push({ type: "text", content: sentenceWithPlaceholders.substring(lastIndex) });
1908
+ }
1909
+ return __spreadProps(__spreadValues({}, baseQuestionData), { questionType: "fill_in_the_blanks", segments, answers, isCaseSensitive: false });
1910
+ }
1911
+ }
1912
+ throw new Error(`Unhandled question type in createQuestionFromRawObject: ${validatedRawQ.questionType}`);
1913
+ }
1914
+ };
1915
+
1916
+ // src/services/UserConfigService.ts
1917
+ var LOCAL_STORAGE_PREFIX2 = "iqk_user_config_";
1918
+ var UserConfigService = class {
1919
+ static getStorageKey(key) {
1920
+ return `${LOCAL_STORAGE_PREFIX2}${key}`;
1921
+ }
1922
+ static setConfig(key, value) {
1923
+ if (typeof window !== "undefined" && window.localStorage) {
1924
+ try {
1925
+ const serializedValue = JSON.stringify(value);
1926
+ localStorage.setItem(this.getStorageKey(key), serializedValue);
1927
+ } catch (e) {
1928
+ console.error(`Error saving config key "${key}" to localStorage:`, e);
1929
+ }
1930
+ }
1931
+ }
1932
+ static getConfig(key, defaultValue = null) {
1933
+ if (typeof window !== "undefined" && window.localStorage) {
1934
+ try {
1935
+ const storedValue = localStorage.getItem(this.getStorageKey(key));
1936
+ if (storedValue !== null) {
1937
+ return JSON.parse(storedValue);
1938
+ }
1939
+ } catch (e) {
1940
+ console.error(`Error retrieving or parsing config key "${key}" from localStorage:`, e);
1941
+ return defaultValue;
1942
+ }
1943
+ }
1944
+ return defaultValue;
1945
+ }
1946
+ static removeConfig(key) {
1947
+ if (typeof window !== "undefined" && window.localStorage) {
1948
+ try {
1949
+ localStorage.removeItem(this.getStorageKey(key));
1950
+ } catch (e) {
1951
+ console.error(`Error removing config key "${key}" from localStorage:`, e);
1952
+ }
1953
+ }
1954
+ }
1955
+ // --- Convenience Methods for Simple Configs ---
1956
+ static getFullName() {
1957
+ return this.getConfig("fullName", null);
1958
+ }
1959
+ static setFullName(name) {
1960
+ this.setConfig("fullName", name);
1961
+ }
1962
+ static getWeeklyGoal() {
1963
+ const goal = this.getConfig("weeklyGoal", 5);
1964
+ return typeof goal === "number" ? goal : 5;
1965
+ }
1966
+ static setWeeklyGoal(goal) {
1967
+ this.setConfig("weeklyGoal", goal);
1968
+ }
1969
+ static getLanguage() {
1970
+ var _a;
1971
+ return (_a = this.getConfig("language", "en")) != null ? _a : "en";
1972
+ }
1973
+ static setLanguage(language) {
1974
+ this.setConfig("language", language);
1975
+ }
1976
+ // --- Methods for Advanced Goal Management ---
1977
+ static getGoals() {
1978
+ return this.getConfig("advanced_goals", []) || [];
1979
+ }
1980
+ static saveGoals(goals) {
1981
+ this.setConfig("advanced_goals", goals);
1982
+ }
1983
+ /**
1984
+ * Adds a new goal to the user's list. The goal object should not contain a description.
1985
+ * @param newGoal A partial Goal object without id, description, isAchieved, or achievedAt.
1986
+ */
1987
+ static addGoal(newGoal) {
1988
+ const goals = this.getGoals();
1989
+ const goalToAdd = __spreadProps(__spreadValues({}, newGoal), {
1990
+ id: generateUniqueId("goal_"),
1991
+ isAchieved: false
1992
+ });
1993
+ this.saveGoals([...goals, goalToAdd]);
1994
+ }
1995
+ static updateGoal(updatedGoal) {
1996
+ const goals = this.getGoals();
1997
+ const index = goals.findIndex((g) => g.id === updatedGoal.id);
1998
+ if (index > -1) {
1999
+ goals[index] = updatedGoal;
2000
+ this.saveGoals(goals);
2001
+ }
2002
+ }
2003
+ static deleteGoal(goalId) {
2004
+ const goals = this.getGoals();
2005
+ const filteredGoals = goals.filter((g) => g.id !== goalId);
2006
+ this.saveGoals(filteredGoals);
2007
+ }
2008
+ };
2009
+
2010
+ // src/services/PracticeHistoryService.ts
2011
+ var LOCAL_STORAGE_KEY = "iqk_practice_history_v2";
2012
+ var PracticeHistoryService = class {
2013
+ static getPracticeHistory() {
2014
+ if (typeof window === "undefined") return [];
2015
+ try {
2016
+ const storedData = localStorage.getItem(LOCAL_STORAGE_KEY);
2017
+ return storedData ? JSON.parse(storedData) : [];
2018
+ } catch (e) {
2019
+ console.error("Error retrieving practice history:", e);
2020
+ return [];
2021
+ }
2022
+ }
2023
+ static saveHistory(history) {
2024
+ if (typeof window !== "undefined") {
2025
+ try {
2026
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(history));
2027
+ } catch (e) {
2028
+ console.error("Error saving practice history to localStorage:", e);
2029
+ }
2030
+ }
2031
+ }
2032
+ static saveCompletedPracticeSession(quizConfig, result, review = null) {
2033
+ const history = this.getPracticeHistory();
2034
+ const topicsCovered = Array.from(
2035
+ new Set(quizConfig.questions.map((q) => JSON.stringify({
2036
+ subject: q.subject || "Uncategorized",
2037
+ category: q.category || "General",
2038
+ topic: q.topic || "General Topic"
2039
+ })))
2040
+ ).map((s) => JSON.parse(s));
2041
+ const newSession = {
2042
+ id: `session_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
2043
+ timestamp: Date.now(),
2044
+ quizConfig,
2045
+ quizResult: result,
2046
+ quizReview: review,
2047
+ summary: {
2048
+ topics: topicsCovered,
2049
+ score: result.score,
2050
+ maxScore: result.maxScore,
2051
+ percentage: result.percentage
2052
+ }
2053
+ };
2054
+ history.unshift(newSession);
2055
+ this.saveHistory(history);
2056
+ return newSession.id;
2057
+ }
2058
+ static updatePracticeReview(sessionId, review) {
2059
+ const history = this.getPracticeHistory();
2060
+ const sessionIndex = history.findIndex((s) => s.id === sessionId);
2061
+ if (sessionIndex > -1) {
2062
+ history[sessionIndex].quizReview = review;
2063
+ this.saveHistory(history);
2064
+ } else {
2065
+ console.warn(`Could not find session with ID "${sessionId}" to update review.`);
2066
+ }
2067
+ }
2068
+ static getPracticeSessionById(sessionId) {
2069
+ return this.getPracticeHistory().find((s) => s.id === sessionId) || null;
2070
+ }
2071
+ static getPracticeHistorySummary() {
2072
+ const history = this.getPracticeHistory();
2073
+ return history.map((session) => ({
2074
+ id: session.id,
2075
+ timestamp: session.timestamp,
2076
+ quizTitle: session.quizConfig.title,
2077
+ topics: session.summary.topics,
2078
+ score: session.summary.score,
2079
+ maxScore: session.summary.maxScore,
2080
+ percentage: session.summary.percentage
2081
+ }));
2082
+ }
2083
+ static getPracticeStats() {
2084
+ const history = this.getPracticeHistory();
2085
+ if (history.length === 0) {
2086
+ return {
2087
+ totalSessions: 0,
2088
+ currentStreak: 0,
2089
+ longestStreak: 0,
2090
+ activityCalendar: {},
2091
+ performanceBySubject: [],
2092
+ performanceByTopic: []
2093
+ };
2094
+ }
2095
+ const toLocalDateString = (timestamp) => {
2096
+ const date = new Date(timestamp);
2097
+ const offset = date.getTimezoneOffset();
2098
+ const adjustedDate = new Date(date.getTime() - offset * 60 * 1e3);
2099
+ return adjustedDate.toISOString().split("T")[0];
2100
+ };
2101
+ const activityCalendar = {};
2102
+ const uniquePracticeDays = /* @__PURE__ */ new Set();
2103
+ history.forEach((session) => {
2104
+ const dateStr = toLocalDateString(session.timestamp);
2105
+ activityCalendar[dateStr] = (activityCalendar[dateStr] || 0) + 1;
2106
+ uniquePracticeDays.add(dateStr);
2107
+ });
2108
+ const sortedDays = Array.from(uniquePracticeDays).sort().reverse();
2109
+ let currentStreak = 0;
2110
+ let longestStreak = 0;
2111
+ if (sortedDays.length > 0) {
2112
+ const today = /* @__PURE__ */ new Date();
2113
+ const todayStr = toLocalDateString(today.getTime());
2114
+ const yesterday = /* @__PURE__ */ new Date();
2115
+ yesterday.setDate(today.getDate() - 1);
2116
+ const yesterdayStr = toLocalDateString(yesterday.getTime());
2117
+ if (sortedDays[0] === todayStr || sortedDays[0] === yesterdayStr) {
2118
+ currentStreak = 1;
2119
+ for (let i = 0; i < sortedDays.length - 1; i++) {
2120
+ const currentDay = new Date(sortedDays[i]);
2121
+ const nextDay = new Date(sortedDays[i + 1]);
2122
+ const diffTime = currentDay.getTime() - nextDay.getTime();
2123
+ const diffDays = Math.round(diffTime / (1e3 * 60 * 60 * 24));
2124
+ if (diffDays === 1) {
2125
+ currentStreak++;
2126
+ } else {
2127
+ break;
2128
+ }
2129
+ }
2130
+ }
2131
+ }
2132
+ if (sortedDays.length > 0) {
2133
+ let tempStreak = 1;
2134
+ longestStreak = 1;
2135
+ for (let i = 0; i < sortedDays.length - 1; i++) {
2136
+ const currentDay = new Date(sortedDays[i]);
2137
+ const nextDay = new Date(sortedDays[i + 1]);
2138
+ const diffTime = currentDay.getTime() - nextDay.getTime();
2139
+ const diffDays = Math.round(diffTime / (1e3 * 60 * 60 * 24));
2140
+ if (diffDays === 1) {
2141
+ tempStreak++;
2142
+ } else {
2143
+ tempStreak = 1;
2144
+ }
2145
+ if (tempStreak > longestStreak) {
2146
+ longestStreak = tempStreak;
2147
+ }
2148
+ }
2149
+ } else {
2150
+ longestStreak = 0;
2151
+ }
2152
+ const subjectPerf = {};
2153
+ const topicPerf = {};
2154
+ history.forEach((session) => {
2155
+ session.summary.topics.forEach((topicInfo) => {
2156
+ if (session.summary.percentage !== null) {
2157
+ if (!subjectPerf[topicInfo.subject]) subjectPerf[topicInfo.subject] = { total: 0, count: 0 };
2158
+ subjectPerf[topicInfo.subject].total += session.summary.percentage;
2159
+ subjectPerf[topicInfo.subject].count++;
2160
+ if (!topicPerf[topicInfo.topic]) topicPerf[topicInfo.topic] = { total: 0, count: 0 };
2161
+ topicPerf[topicInfo.topic].total += session.summary.percentage;
2162
+ topicPerf[topicInfo.topic].count++;
2163
+ }
2164
+ });
2165
+ });
2166
+ const formatPerf = (perfData) => {
2167
+ return Object.entries(perfData).map(([name, data]) => ({
2168
+ name,
2169
+ totalSessions: data.count,
2170
+ averageScore: parseFloat((data.total / data.count).toFixed(2))
2171
+ })).sort((a, b) => b.totalSessions - a.totalSessions);
2172
+ };
2173
+ return {
2174
+ totalSessions: history.length,
2175
+ currentStreak,
2176
+ longestStreak,
2177
+ activityCalendar,
2178
+ performanceBySubject: formatPerf(subjectPerf),
2179
+ performanceByTopic: formatPerf(topicPerf)
2180
+ };
2181
+ }
2182
+ static clearHistory() {
2183
+ if (typeof window !== "undefined") {
2184
+ localStorage.removeItem(LOCAL_STORAGE_KEY);
2185
+ }
2186
+ }
2187
+ };
2188
+
2189
+ // src/data/achievements.json
2190
+ var achievements_default = [
2191
+ {
2192
+ id: "first_session",
2193
+ icon: "Rocket",
2194
+ nameKey: "achievements.first_session.name",
2195
+ descriptionKey: "achievements.first_session.description",
2196
+ condition: {
2197
+ type: "SESSION_COUNT",
2198
+ params: { count: 1 }
2199
+ }
2200
+ },
2201
+ {
2202
+ id: "five_sessions",
2203
+ icon: "BookOpen",
2204
+ nameKey: "achievements.five_sessions.name",
2205
+ descriptionKey: "achievements.five_sessions.description",
2206
+ condition: {
2207
+ type: "SESSION_COUNT",
2208
+ params: { count: 5 }
2209
+ }
2210
+ },
2211
+ {
2212
+ id: "perfect_score",
2213
+ icon: "Star",
2214
+ nameKey: "achievements.perfect_score.name",
2215
+ descriptionKey: "achievements.perfect_score.description",
2216
+ condition: {
2217
+ type: "PERFECT_SCORE",
2218
+ params: {}
2219
+ }
2220
+ },
2221
+ {
2222
+ id: "streak_3_days",
2223
+ icon: "Flame",
2224
+ nameKey: "achievements.streak_3_days.name",
2225
+ descriptionKey: "achievements.streak_3_days.description",
2226
+ condition: {
2227
+ type: "STREAK",
2228
+ params: { days: 3 }
2229
+ }
2230
+ },
2231
+ {
2232
+ id: "ten_sessions",
2233
+ icon: "Award",
2234
+ nameKey: "achievements.ten_sessions.name",
2235
+ descriptionKey: "achievements.ten_sessions.description",
2236
+ condition: {
2237
+ type: "SESSION_COUNT",
2238
+ params: { count: 10 }
2239
+ }
2240
+ },
2241
+ {
2242
+ id: "streak_7_days",
2243
+ icon: "Sparkles",
2244
+ nameKey: "achievements.streak_7_days.name",
2245
+ descriptionKey: "achievements.streak_7_days.description",
2246
+ condition: {
2247
+ type: "STREAK",
2248
+ params: { days: 7 }
2249
+ }
2250
+ },
2251
+ {
2252
+ id: "weekend_warrior",
2253
+ icon: "CalendarDays",
2254
+ nameKey: "achievements.weekend_warrior.name",
2255
+ descriptionKey: "achievements.weekend_warrior.description",
2256
+ condition: {
2257
+ type: "SESSION_ON_WEEKEND",
2258
+ params: {}
2259
+ }
2260
+ },
2261
+ {
2262
+ id: "night_owl",
2263
+ icon: "Moon",
2264
+ nameKey: "achievements.night_owl.name",
2265
+ descriptionKey: "achievements.night_owl.description",
2266
+ condition: {
2267
+ type: "SESSION_AFTER_HOUR",
2268
+ params: { hour: 22 }
2269
+ }
2270
+ },
2271
+ {
2272
+ id: "polymath_prospect",
2273
+ icon: "Library",
2274
+ nameKey: "achievements.polymath_prospect.name",
2275
+ descriptionKey: "achievements.polymath_prospect.description",
2276
+ condition: {
2277
+ type: "DISTINCT_SUBJECTS",
2278
+ params: { count: 3 }
2279
+ }
2280
+ },
2281
+ {
2282
+ id: "subject_specialist",
2283
+ icon: "Target",
2284
+ nameKey: "achievements.subject_specialist.name",
2285
+ descriptionKey: "achievements.subject_specialist.description",
2286
+ condition: {
2287
+ type: "SESSIONS_IN_SUBJECT",
2288
+ params: { count: 5 }
2289
+ }
2290
+ },
2291
+ {
2292
+ id: "high_achiever",
2293
+ icon: "TrendingUp",
2294
+ nameKey: "achievements.high_achiever.name",
2295
+ descriptionKey: "achievements.high_achiever.description",
2296
+ condition: {
2297
+ type: "CONSECUTIVE_HIGH_SCORE",
2298
+ params: { count: 3, score: 90 }
2299
+ }
2300
+ },
2301
+ {
2302
+ id: "comeback_kid",
2303
+ icon: "ChevronsUp",
2304
+ nameKey: "achievements.comeback_kid.name",
2305
+ descriptionKey: "achievements.comeback_kid.description",
2306
+ condition: {
2307
+ type: "SCORE_IMPROVEMENT",
2308
+ params: { improvement: 20 }
2309
+ }
2310
+ },
2311
+ {
2312
+ id: "twenty_five_sessions",
2313
+ icon: "Medal",
2314
+ nameKey: "achievements.twenty_five_sessions.name",
2315
+ descriptionKey: "achievements.twenty_five_sessions.description",
2316
+ condition: {
2317
+ type: "SESSION_COUNT",
2318
+ params: { count: 25 }
2319
+ }
2320
+ },
2321
+ {
2322
+ id: "streak_14_days",
2323
+ icon: "Gem",
2324
+ nameKey: "achievements.streak_14_days.name",
2325
+ descriptionKey: "achievements.streak_14_days.description",
2326
+ condition: {
2327
+ type: "STREAK",
2328
+ params: { days: 14 }
2329
+ }
2330
+ },
2331
+ {
2332
+ id: "subject_master",
2333
+ icon: "Crown",
2334
+ nameKey: "achievements.subject_master.name",
2335
+ descriptionKey: "achievements.subject_master.description",
2336
+ condition: {
2337
+ type: "SUBJECT_MASTERY",
2338
+ params: { score: 90, sessions: 5 }
2339
+ }
2340
+ },
2341
+ {
2342
+ id: "centurion",
2343
+ icon: "ShieldCheck",
2344
+ nameKey: "achievements.centurion.name",
2345
+ descriptionKey: "achievements.centurion.description",
2346
+ condition: {
2347
+ type: "SESSION_COUNT",
2348
+ params: { count: 100 }
2349
+ }
2350
+ }
2351
+ ];
2352
+
2353
+ // src/services/AchievementService.ts
2354
+ var LOCAL_STORAGE_KEY2 = "iqk_unlocked_achievements_v2";
2355
+ var ALL_ACHIEVEMENT_DEFS = achievements_default;
2356
+ var conditionEvaluators = {
2357
+ SESSION_COUNT: (params, history) => history.length >= params.count,
2358
+ PERFECT_SCORE: (params, history) => history.some((s) => s.summary.percentage === 100),
2359
+ STREAK: (params, history, stats) => stats.longestStreak >= params.days,
2360
+ SESSION_ON_WEEKEND: (params, history) => history.some((s) => [0, 6].includes(new Date(s.timestamp).getDay())),
2361
+ SESSION_AFTER_HOUR: (params, history) => history.some((s) => new Date(s.timestamp).getHours() >= params.hour),
2362
+ DISTINCT_SUBJECTS: (params, history) => {
2363
+ const subjects = new Set(history.flatMap((s) => s.summary.topics.map((t) => t.subject)));
2364
+ return subjects.size >= params.count;
2365
+ },
2366
+ SESSIONS_IN_SUBJECT: (params, history) => {
2367
+ const subjectCounts = history.flatMap((s) => s.summary.topics.map((t) => t.subject)).reduce((acc, subject) => {
2368
+ acc[subject] = (acc[subject] || 0) + 1;
2369
+ return acc;
2370
+ }, {});
2371
+ return Object.values(subjectCounts).some((count) => count >= params.count);
2372
+ },
2373
+ CONSECUTIVE_HIGH_SCORE: (params, history) => {
2374
+ if (history.length < params.count) return false;
2375
+ for (let i = 0; i <= history.length - params.count; i++) {
2376
+ const recentSessions = history.slice(i, i + params.count);
2377
+ if (recentSessions.every((s) => s.summary.percentage !== null && s.summary.percentage >= params.score)) {
2378
+ return true;
2379
+ }
2380
+ }
2381
+ return false;
2382
+ },
2383
+ SCORE_IMPROVEMENT: (params, history) => {
2384
+ if (history.length < 2) return false;
2385
+ for (let i = 0; i < history.length - 1; i++) {
2386
+ const currentPercent = history[i].summary.percentage;
2387
+ const previousPercent = history[i + 1].summary.percentage;
2388
+ if (currentPercent !== null && previousPercent !== null && currentPercent - previousPercent >= params.improvement) {
2389
+ return true;
2390
+ }
2391
+ }
2392
+ return false;
2393
+ },
2394
+ SUBJECT_MASTERY: (params, history, stats) => {
2395
+ return stats.performanceBySubject.some((s) => s.averageScore >= params.score && s.totalSessions >= params.sessions);
2396
+ }
2397
+ };
2398
+ var AchievementService = class {
2399
+ static getUnlockedAchievementsMap() {
2400
+ if (typeof window === "undefined") return {};
2401
+ try {
2402
+ const storedData = localStorage.getItem(LOCAL_STORAGE_KEY2);
2403
+ return storedData ? JSON.parse(storedData) : {};
2404
+ } catch (e) {
2405
+ console.error("Error retrieving unlocked achievements map:", e);
2406
+ return {};
2407
+ }
2408
+ }
2409
+ static saveUnlockedAchievementsMap(unlockedMap) {
2410
+ if (typeof window !== "undefined") {
2411
+ try {
2412
+ localStorage.setItem(LOCAL_STORAGE_KEY2, JSON.stringify(unlockedMap));
2413
+ } catch (e) {
2414
+ console.error("Error saving unlocked achievements map:", e);
2415
+ }
2416
+ }
2417
+ }
2418
+ static checkAndUnlockAchievements(history, stats) {
2419
+ const unlockedMap = this.getUnlockedAchievementsMap();
2420
+ const newlyUnlocked = [];
2421
+ let hasChanges = false;
2422
+ ALL_ACHIEVEMENT_DEFS.forEach((achievementDef) => {
2423
+ if (unlockedMap[achievementDef.id]) {
2424
+ return;
2425
+ }
2426
+ const evaluator = conditionEvaluators[achievementDef.condition.type];
2427
+ if (evaluator) {
2428
+ const isUnlocked = evaluator(achievementDef.condition.params, history, stats);
2429
+ if (isUnlocked) {
2430
+ const unlockTime = Date.now();
2431
+ unlockedMap[achievementDef.id] = unlockTime;
2432
+ newlyUnlocked.push(achievementDef);
2433
+ hasChanges = true;
2434
+ }
2435
+ } else {
2436
+ console.warn(`No evaluator found for achievement condition type: "${achievementDef.condition.type}"`);
2437
+ }
2438
+ });
2439
+ if (hasChanges) {
2440
+ this.saveUnlockedAchievementsMap(unlockedMap);
2441
+ }
2442
+ return newlyUnlocked;
2443
+ }
2444
+ static getAllAchievementDefinitions() {
2445
+ return ALL_ACHIEVEMENT_DEFS;
2446
+ }
2447
+ static getAllAchievementsWithStatus() {
2448
+ const unlockedMap = this.getUnlockedAchievementsMap();
2449
+ return ALL_ACHIEVEMENT_DEFS.map((def) => {
2450
+ const unlockedAt = unlockedMap[def.id];
2451
+ return {
2452
+ id: def.id,
2453
+ icon: def.icon,
2454
+ name: def.nameKey,
2455
+ description: def.descriptionKey,
2456
+ unlockedAt: unlockedAt || void 0
2457
+ };
2458
+ });
2459
+ }
2460
+ };
2461
+
2462
+ // src/services/MotivationalQuotes.ts
2463
+ var QUOTE_BANK = [
2464
+ { text: "H\u1ECDc, h\u1ECDc n\u1EEFa, h\u1ECDc m\xE3i.", author: "V.I. Lenin" },
2465
+ { text: "H\xE0nh tr\xECnh v\u1EA1n d\u1EB7m b\u1EAFt \u0111\u1EA7u b\u1EB1ng m\u1ED9t b\u01B0\u1EDBc ch\xE2n.", author: "L\xE3o T\u1EED" },
2466
+ { text: "C\xE1ch t\u1ED1t nh\u1EA5t \u0111\u1EC3 d\u1EF1 \u0111o\xE1n t\u01B0\u01A1ng lai l\xE0 t\u1EA1o ra n\xF3.", author: "Peter Drucker" },
2467
+ { text: "\u0110\u1EEBng lo l\u1EAFng v\u1EC1 th\u1EA5t b\u1EA1i; h\xE3y lo l\u1EAFng v\u1EC1 nh\u1EEFng c\u01A1 h\u1ED9i b\u1EA1n b\u1ECF l\u1EE1 khi kh\xF4ng d\xE1m th\u1EED.", author: "Jack Canfield" },
2468
+ { text: "Th\xE0nh c\xF4ng kh\xF4ng ph\u1EA3i l\xE0 cu\u1ED1i c\xF9ng, th\u1EA5t b\u1EA1i kh\xF4ng ph\u1EA3i l\xE0 ch\u1EBFt ng\u01B0\u1EDDi: l\xF2ng can \u0111\u1EA3m \u0111i ti\u1EBFp m\u1EDBi quan tr\u1ECDng.", author: "Winston Churchill" },
2469
+ { text: "Ch\u1EC9 nh\u1EEFng ng\u01B0\u1EDDi d\xE1m th\u1EA5t b\u1EA1i l\u1EDBn m\u1EDBi c\xF3 th\u1EC3 \u0111\u1EA1t \u0111\u01B0\u1EE3c th\xE0nh c\xF4ng l\u1EDBn.", author: "Robert F. Kennedy" },
2470
+ { text: "Ki\u1EBFn th\u1EE9c l\xE0 s\u1EE9c m\u1EA1nh.", author: "Francis Bacon" },
2471
+ { text: "S\u1EF1 kh\xE1c bi\u1EC7t gi\u1EEFa b\xECnh th\u01B0\u1EDDng v\xE0 phi th\u01B0\u1EDDng ch\u1EC9 l\xE0 m\u1ED9t ch\xFAt n\u1ED7 l\u1EF1c.", author: "Jimmy Johnson" },
2472
+ { text: "H\xE3y l\xE0 s\u1EF1 thay \u0111\u1ED5i m\xE0 b\u1EA1n mu\u1ED1n th\u1EA5y tr\xEAn th\u1EBF gi\u1EDBi.", author: "Mahatma Gandhi" },
2473
+ { text: "H\xF4m nay b\u1EA1n \u0111\u1ECDc, ng\xE0y mai b\u1EA1n s\u1EBD l\xE3nh \u0111\u1EA1o.", author: "Margaret Fuller" },
2474
+ { text: "R\u1EC5 c\u1EE7a gi\xE1o d\u1EE5c th\xEC \u0111\u1EAFng, nh\u01B0ng qu\u1EA3 c\u1EE7a n\xF3 th\xEC ng\u1ECDt.", author: "Aristotle" },
2475
+ { text: "C\xE1ch \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u l\xE0 ng\u1EEBng n\xF3i v\xE0 b\u1EAFt \u0111\u1EA7u l\xE0m.", author: "Walt Disney" },
2476
+ { text: "K\u1EF7 lu\u1EADt l\xE0 c\u1EA7u n\u1ED1i gi\u1EEFa m\u1EE5c ti\xEAu v\xE0 th\xE0nh t\u1EF1u.", author: "Jim Rohn" },
2477
+ { text: "B\u1EA1n kh\xF4ng c\u1EA7n ph\u1EA3i v\u0129 \u0111\u1EA1i \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u, nh\u01B0ng b\u1EA1n ph\u1EA3i b\u1EAFt \u0111\u1EA7u \u0111\u1EC3 tr\u1EDF n\xEAn v\u0129 \u0111\u1EA1i.", author: "Zig Ziglar" },
2478
+ { text: "Tin r\u1EB1ng b\u1EA1n c\xF3 th\u1EC3 v\xE0 b\u1EA1n \u0111\xE3 \u0111i \u0111\u01B0\u1EE3c n\u1EEDa \u0111\u01B0\u1EDDng.", author: "Theodore Roosevelt" },
2479
+ { text: "M\u1ED9t ng\u01B0\u1EDDi kh\xF4ng bao gi\u1EDD m\u1EAFc sai l\u1EA7m l\xE0 ng\u01B0\u1EDDi kh\xF4ng bao gi\u1EDD th\u1EED b\u1EA5t c\u1EE9 \u0111i\u1EC1u g\xEC m\u1EDBi.", author: "Albert Einstein" },
2480
+ { text: "Th\u1EED th\xE1ch l\xE0 th\u1EE9 l\xE0m cho cu\u1ED9c s\u1ED1ng th\xFA v\u1ECB v\xE0 v\u01B0\u1EE3t qua ch\xFAng l\xE0 \u0111i\u1EC1u l\xE0m cho cu\u1ED9c s\u1ED1ng c\xF3 \xFD ngh\u0129a.", author: "Joshua J. Marine" },
2481
+ { text: "Thi\xEAn t\xE0i l\xE0 m\u1ED9t ph\u1EA7n tr\u0103m c\u1EA3m h\u1EE9ng v\xE0 ch\xEDn m\u01B0\u01A1i ch\xEDn ph\u1EA7n tr\u0103m m\u1ED3 h\xF4i.", author: "Thomas Edison" },
2482
+ { text: "\u0110\u1EEBng \u0111\u1EC3 ng\xE0y h\xF4m qua chi\u1EBFm qu\xE1 nhi\u1EC1u th\u1EDDi gian c\u1EE7a ng\xE0y h\xF4m nay.", author: "Will Rogers" },
2483
+ { text: "N\u1EBFu c\u01A1 h\u1ED9i kh\xF4ng g\xF5 c\u1EEDa, h\xE3y x\xE2y m\u1ED9t c\xE1nh c\u1EEDa.", author: "Milton Berle" }
2484
+ ];
2485
+ var QuoteService = class {
2486
+ /**
2487
+ * Retrieves a random quote from the internal quote bank.
2488
+ * @returns A quote object containing text and author.
2489
+ */
2490
+ static getRandomQuote() {
2491
+ if (QUOTE_BANK.length === 0) {
2492
+ return { text: "H\xE3y b\u1EAFt \u0111\u1EA7u h\xE0nh tr\xECnh h\u1ECDc t\u1EADp c\u1EE7a b\u1EA1n.", author: "QuizKit" };
2493
+ }
2494
+ const randomIndex = Math.floor(Math.random() * QUOTE_BANK.length);
2495
+ return QUOTE_BANK[randomIndex];
2496
+ }
2497
+ };
2498
+
2499
+ // src/services/KnowledgeCardService.ts
2500
+ var LOCAL_STORAGE_KEY3 = "iqk_knowledge_cards_store";
2501
+ var GENERATION_FLAG_KEY = "iqk_card_generation_in_progress";
2502
+ var KnowledgeCardService = class {
2503
+ static generateHash(str) {
2504
+ let hash = 0;
2505
+ if (str.length === 0) return hash.toString();
2506
+ for (let i = 0; i < str.length; i++) {
2507
+ const char = str.charCodeAt(i);
2508
+ hash = (hash << 5) - hash + char;
2509
+ hash |= 0;
2510
+ }
2511
+ return hash.toString();
2512
+ }
2513
+ static getStore() {
2514
+ if (typeof window === "undefined") {
2515
+ return { sourceHash: null, conceptsPlan: [], generatedConcepts: {}, cards: [] };
2516
+ }
2517
+ try {
2518
+ const storedData = localStorage.getItem(LOCAL_STORAGE_KEY3);
2519
+ if (storedData) {
2520
+ return JSON.parse(storedData);
2521
+ }
2522
+ } catch (e) {
2523
+ console.error("Error reading Knowledge Card store from localStorage:", e);
2524
+ }
2525
+ return { sourceHash: null, conceptsPlan: [], generatedConcepts: {}, cards: [] };
2526
+ }
2527
+ static saveStore(store) {
2528
+ if (typeof window !== "undefined") {
2529
+ try {
2530
+ localStorage.setItem(LOCAL_STORAGE_KEY3, JSON.stringify(store));
2531
+ } catch (e) {
2532
+ console.error("Error saving Knowledge Card store to localStorage:", e);
2533
+ }
2534
+ }
2535
+ }
2536
+ static getSourceHash() {
2537
+ return this.getStore().sourceHash;
2538
+ }
2539
+ /**
2540
+ * Saves a new card generation plan. This clears old cards and resets the generation progress.
2541
+ * @param sourceHash The hash of the new source content.
2542
+ * @param concepts An array of concept strings to be generated.
2543
+ */
2544
+ static saveCardPlan(sourceHash, concepts) {
2545
+ const newStore = {
2546
+ sourceHash,
2547
+ conceptsPlan: concepts,
2548
+ generatedConcepts: {},
2549
+ cards: []
2550
+ };
2551
+ this.saveStore(newStore);
2552
+ }
2553
+ /**
2554
+ * Saves a newly generated card to the store and marks its concept as completed.
2555
+ * @param card The KnowledgeCard object to save.
2556
+ */
2557
+ static saveGeneratedCard(card) {
2558
+ const store = this.getStore();
2559
+ if (!store.cards.some((c) => c.id === card.id)) {
2560
+ store.cards.push(card);
2561
+ store.generatedConcepts[card.concept] = true;
2562
+ this.saveStore(store);
2563
+ }
2564
+ }
2565
+ /**
2566
+ * Gets the list of concepts that are in the plan but have not been generated yet.
2567
+ * @returns An array of concept strings that are pending generation.
2568
+ */
2569
+ static getPendingConcepts() {
2570
+ const store = this.getStore();
2571
+ return store.conceptsPlan.filter((concept) => !store.generatedConcepts[concept]);
2572
+ }
2573
+ /**
2574
+ * Gets the current status of the card generation process.
2575
+ * @returns An object with the total number of cards planned and the number completed.
2576
+ */
2577
+ static getGenerationStatus() {
2578
+ const store = this.getStore();
2579
+ return {
2580
+ total: store.conceptsPlan.length,
2581
+ completed: Object.keys(store.generatedConcepts).length
2582
+ };
2583
+ }
2584
+ /**
2585
+ * Checks if a generation process is currently marked as running in this browser tab/session.
2586
+ * Uses sessionStorage to prevent multiple tabs from running the same process.
2587
+ * @returns True if a process is marked as running, false otherwise.
2588
+ */
2589
+ static isGenerationInProgress() {
2590
+ if (typeof window === "undefined") return false;
2591
+ return sessionStorage.getItem(GENERATION_FLAG_KEY) === "true";
2592
+ }
2593
+ /**
2594
+ * Sets or clears the flag indicating that a generation process is active.
2595
+ * @param status The status to set (true for running, false for stopped).
2596
+ */
2597
+ static setGenerationStatus(status) {
2598
+ if (typeof window !== "undefined") {
2599
+ if (status) {
2600
+ sessionStorage.setItem(GENERATION_FLAG_KEY, "true");
2601
+ } else {
2602
+ sessionStorage.removeItem(GENERATION_FLAG_KEY);
2603
+ }
2604
+ }
2605
+ }
2606
+ static getCards() {
2607
+ return this.getStore().cards;
2608
+ }
2609
+ static getRandomCard() {
2610
+ const cards = this.getCards();
2611
+ if (cards.length === 0) return null;
2612
+ const randomIndex = Math.floor(Math.random() * cards.length);
2613
+ return cards[randomIndex];
2614
+ }
2615
+ static searchCards(query) {
2616
+ const cards = this.getCards();
2617
+ if (!query.trim()) return [];
2618
+ const lowerCaseQuery = query.toLowerCase();
2619
+ return cards.filter(
2620
+ (card) => card.concept.toLowerCase().includes(lowerCaseQuery) || card.definition.toLowerCase().includes(lowerCaseQuery)
2621
+ );
2622
+ }
2623
+ };
2624
+
2625
+ // src/services/metadataService.ts
2626
+ var LocalStorageManager = class {
2627
+ constructor(key) {
2628
+ this.key = `iqk_metadata_${key}`;
2629
+ }
2630
+ getAll() {
2631
+ if (typeof window === "undefined") return [];
2632
+ try {
2633
+ const stored = localStorage.getItem(this.key);
2634
+ return stored ? JSON.parse(stored) : [];
2635
+ } catch (e) {
2636
+ console.error(`Error reading from localStorage key ${this.key}:`, e);
2637
+ return [];
2638
+ }
2639
+ }
2640
+ saveAll(items) {
2641
+ if (typeof window === "undefined") return;
2642
+ try {
2643
+ localStorage.setItem(this.key, JSON.stringify(items));
2644
+ } catch (e) {
2645
+ console.error(`Error writing to localStorage key ${this.key}:`, e);
2646
+ }
2647
+ }
2648
+ add(item) {
2649
+ const items = this.getAll();
2650
+ if (items.some((i) => i.code === item.code)) {
2651
+ throw new Error(`An item with code "${item.code}" already exists for ${this.key}.`);
2652
+ }
2653
+ const newItem = __spreadProps(__spreadValues({}, item), { id: generateUniqueId(`${this.key}_`) });
2654
+ this.saveAll([...items, newItem]);
2655
+ return newItem;
2656
+ }
2657
+ // ===== FIX IS HERE =====
2658
+ // Changed the type of 'updates' to allow 'code' to be part of the update object.
2659
+ update(id, updates) {
2660
+ const items = this.getAll();
2661
+ const index = items.findIndex((i) => i.id === id);
2662
+ if (index === -1) {
2663
+ console.warn(`Item with id "${id}" not found in ${this.key} for update.`);
2664
+ return null;
2665
+ }
2666
+ const updatedItem = __spreadValues(__spreadValues({}, items[index]), updates);
2667
+ items[index] = updatedItem;
2668
+ this.saveAll(items);
2669
+ return updatedItem;
2670
+ }
2671
+ // =======================
2672
+ delete(code) {
2673
+ const items = this.getAll();
2674
+ const newItems = items.filter((i) => i.code !== code);
2675
+ if (items.length === newItems.length) {
2676
+ return false;
2677
+ }
2678
+ this.saveAll(newItems);
2679
+ return true;
2680
+ }
2681
+ };
2682
+ var subjectManager = new LocalStorageManager("subjects");
2683
+ var gradeLevelManager = new LocalStorageManager("grade_levels");
2684
+ var topicManager = new LocalStorageManager("topics");
2685
+ var categoryManager = new LocalStorageManager("categories");
2686
+ var bloomLevelManager = new LocalStorageManager("bloom_levels");
2687
+ var questionTypeManager = new LocalStorageManager("question_types");
2688
+ var learningObjectiveManager = new LocalStorageManager("learning_objectives");
2689
+ var contextManager = new LocalStorageManager("contexts");
2690
+ var approachManager = new LocalStorageManager("approaches");
2691
+ function mapRawDifficultyToStandard(rawDifficulty) {
2692
+ switch (rawDifficulty) {
2693
+ case "E":
2694
+ case "E~M":
2695
+ return "easy";
2696
+ case "M":
2697
+ case "M~H":
2698
+ return "medium";
2699
+ case "H":
2700
+ return "hard";
2701
+ default:
2702
+ return "medium";
2703
+ }
2704
+ }
2705
+ var _MetadataService = class _MetadataService {
2706
+ };
2707
+ // --- Subject Services ---
2708
+ _MetadataService.getSubjects = () => subjectManager.getAll().sort((a, b) => a.name.localeCompare(b.name));
2709
+ _MetadataService.addSubject = (name, code) => {
2710
+ return subjectManager.add({ code, name, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2711
+ };
2712
+ _MetadataService.updateSubject = (id, name, code) => {
2713
+ return subjectManager.update(id, { name, code, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2714
+ };
2715
+ _MetadataService.deleteSubject = (code) => {
2716
+ const topics = _MetadataService.getTopics(code);
2717
+ if (topics.length > 0) {
2718
+ throw new Error("Cannot delete subject: It is referenced by topics.");
2719
+ }
2720
+ return subjectManager.delete(code);
2721
+ };
2722
+ // --- GradeLevel Services ---
2723
+ _MetadataService.getGradeLevels = () => gradeLevelManager.getAll().sort((a, b) => a.name.localeCompare(b.name));
2724
+ _MetadataService.addGradeLevel = (name, code) => gradeLevelManager.add({ name, code });
2725
+ _MetadataService.updateGradeLevel = (id, name, code) => gradeLevelManager.update(id, { name, code });
2726
+ _MetadataService.deleteGradeLevel = (code) => gradeLevelManager.delete(code);
2727
+ // --- Topic Services ---
2728
+ _MetadataService.getTopics = (subjectCode) => {
2729
+ const allTopics = topicManager.getAll();
2730
+ const filtered = subjectCode ? allTopics.filter((t) => t.subjectCode === subjectCode) : allTopics;
2731
+ return filtered.sort((a, b) => a.name.localeCompare(b.name));
2732
+ };
2733
+ _MetadataService.addTopic = (name, code, subjectCode) => topicManager.add({ name, code, subjectCode });
2734
+ _MetadataService.updateTopic = (id, name, code, subjectCode) => topicManager.update(id, { name, code, subjectCode });
2735
+ _MetadataService.deleteTopic = (code) => topicManager.delete(code);
2736
+ // --- BloomLevel Services ---
2737
+ _MetadataService.getBloomLevels = () => bloomLevelManager.getAll().sort((a, b) => a.name.localeCompare(b.name));
2738
+ _MetadataService.addBloomLevel = (name, code, description) => bloomLevelManager.add({ name, code, description });
2739
+ _MetadataService.updateBloomLevel = (id, name, code, description) => bloomLevelManager.update(id, { name, code, description });
2740
+ _MetadataService.deleteBloomLevel = (code) => bloomLevelManager.delete(code);
2741
+ // --- QuestionType Services ---
2742
+ _MetadataService.getQuestionTypes = () => questionTypeManager.getAll().sort((a, b) => a.name.localeCompare(b.name));
2743
+ _MetadataService.addQuestionType = (name, code, description) => questionTypeManager.add({ name, code, description });
2744
+ _MetadataService.updateQuestionType = (id, name, code, description) => questionTypeManager.update(id, { name, code, description });
2745
+ _MetadataService.deleteQuestionType = (code) => questionTypeManager.delete(code);
2746
+ // --- Category Services ---
2747
+ _MetadataService.getCategories = () => categoryManager.getAll().sort((a, b) => a.name.localeCompare(b.name));
2748
+ _MetadataService.addCategory = (name, code, description) => categoryManager.add({ name, code, description });
2749
+ _MetadataService.updateCategory = (id, name, code, description) => categoryManager.update(id, { name, code, description });
2750
+ _MetadataService.deleteCategory = (code) => categoryManager.delete(code);
2751
+ // --- Context Services ---
2752
+ _MetadataService.getContexts = () => contextManager.getAll().sort((a, b) => a.name.localeCompare(b.name));
2753
+ _MetadataService.addContext = (name, code, description) => contextManager.add({ name, code, description });
2754
+ _MetadataService.updateContext = (id, name, code, description) => contextManager.update(id, { name, code, description });
2755
+ _MetadataService.deleteContext = (code) => contextManager.delete(code);
2756
+ // --- Approach Services ---
2757
+ _MetadataService.getApproaches = () => approachManager.getAll().sort((a, b) => a.code.localeCompare(b.code));
2758
+ _MetadataService.addApproach = (approachData) => {
2759
+ const difficulty = mapRawDifficultyToStandard(approachData.rawDifficulty);
2760
+ return approachManager.add(__spreadProps(__spreadValues({}, approachData), { difficulty }));
2761
+ };
2762
+ _MetadataService.updateApproach = (id, approachData) => {
2763
+ const updates = __spreadValues({}, approachData);
2764
+ if (approachData.rawDifficulty) {
2765
+ updates.difficulty = mapRawDifficultyToStandard(approachData.rawDifficulty);
2766
+ }
2767
+ return approachManager.update(id, updates);
2768
+ };
2769
+ _MetadataService.deleteApproach = (code) => approachManager.delete(code);
2770
+ // --- LearningObjective Services ---
2771
+ _MetadataService.getLearningObjectives = (subjectCode) => {
2772
+ const allLOs = learningObjectiveManager.getAll();
2773
+ const filtered = subjectCode ? allLOs.filter((lo) => lo.subjectCode === subjectCode) : allLOs;
2774
+ return filtered.sort((a, b) => a.name.localeCompare(b.name));
2775
+ };
2776
+ _MetadataService.addLearningObjective = (name, code, subjectCode, description) => learningObjectiveManager.add({ name, code, subjectCode, description });
2777
+ _MetadataService.updateLearningObjective = (id, name, code, subjectCode, description) => learningObjectiveManager.update(id, { name, code, subjectCode, description });
2778
+ _MetadataService.deleteLearningObjective = (code) => learningObjectiveManager.delete(code);
2779
+ var MetadataService = _MetadataService;
2780
+
2781
+ // src/services/questionBankService.ts
2782
+ var LocalStorageManager2 = class {
2783
+ constructor(key) {
2784
+ this.key = key;
2785
+ }
2786
+ getAll() {
2787
+ if (typeof window === "undefined") return [];
2788
+ try {
2789
+ const stored = localStorage.getItem(this.key);
2790
+ return stored ? JSON.parse(stored) : [];
2791
+ } catch (e) {
2792
+ console.error(`Error reading from localStorage key ${this.key}:`, e);
2793
+ return [];
2794
+ }
2795
+ }
2796
+ saveAll(items) {
2797
+ if (typeof window === "undefined") return;
2798
+ try {
2799
+ localStorage.setItem(this.key, JSON.stringify(items));
2800
+ } catch (e) {
2801
+ console.error(`Error writing to localStorage key ${this.key}:`, e);
2802
+ }
2803
+ }
2804
+ };
2805
+ var questionBankManager = new LocalStorageManager2("iqk_question_bank");
2806
+ var QuestionBankService = class {
2807
+ static getQuestions(filters) {
2808
+ let questions = questionBankManager.getAll();
2809
+ if (filters) {
2810
+ if (filters.subjectCode) {
2811
+ questions = questions.filter((q) => q.subjectCode === filters.subjectCode);
2812
+ }
2813
+ if (filters.topicCode) {
2814
+ questions = questions.filter((q) => q.topicCode === filters.topicCode);
2815
+ }
2816
+ if (filters.gradeLevelCode) {
2817
+ questions = questions.filter((q) => q.gradeLevelCode === filters.gradeLevelCode);
2818
+ }
2819
+ if (filters.bloomLevelCode) {
2820
+ questions = questions.filter((q) => q.bloomLevelCode === filters.bloomLevelCode);
2821
+ }
2822
+ if (filters.questionTypeCode) {
2823
+ questions = questions.filter((q) => q.questionTypeCode === filters.questionTypeCode);
2824
+ }
2825
+ if (filters.difficulty) {
2826
+ questions = questions.filter((q) => q.difficulty === filters.difficulty);
2827
+ }
2828
+ if (filters.searchTerm) {
2829
+ const lowercasedTerm = filters.searchTerm.toLowerCase();
2830
+ questions = questions.filter(
2831
+ (q) => q.text.toLowerCase().includes(lowercasedTerm) || q.code.toLowerCase().includes(lowercasedTerm)
2832
+ );
2833
+ }
2834
+ }
2835
+ return questions.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
2836
+ }
2837
+ static getQuestionByCode(code) {
2838
+ return questionBankManager.getAll().find((q) => q.code === code);
2839
+ }
2840
+ // CHANGE 2: Simplified function signature. It now takes the full object to be added.
2841
+ static addQuestion(questionData) {
2842
+ const allQuestions = questionBankManager.getAll();
2843
+ if (allQuestions.some((q) => q.code === questionData.code)) {
2844
+ throw new Error(`A question with code "${questionData.code}" already exists.`);
2845
+ }
2846
+ const newQuestion = __spreadProps(__spreadValues({}, questionData), {
2847
+ id: generateUniqueId("qb_"),
2848
+ lastModified: (/* @__PURE__ */ new Date()).toISOString()
2849
+ });
2850
+ questionBankManager.saveAll([...allQuestions, newQuestion]);
2851
+ return newQuestion;
2852
+ }
2853
+ // CHANGE 2: Simplified function signature.
2854
+ static updateQuestion(id, updates) {
2855
+ const allQuestions = questionBankManager.getAll();
2856
+ const index = allQuestions.findIndex((q) => q.id === id);
2857
+ if (index === -1) {
2858
+ console.warn(`Question with id "${id}" not found for update.`);
2859
+ return null;
2860
+ }
2861
+ const updatedQuestion = __spreadProps(__spreadValues(__spreadValues({}, allQuestions[index]), updates), {
2862
+ lastModified: (/* @__PURE__ */ new Date()).toISOString()
2863
+ });
2864
+ allQuestions[index] = updatedQuestion;
2865
+ questionBankManager.saveAll(allQuestions);
2866
+ return updatedQuestion;
2867
+ }
2868
+ static deleteQuestionByCode(code) {
2869
+ const allQuestions = questionBankManager.getAll();
2870
+ const newQuestions = allQuestions.filter((q) => q.code !== code);
2871
+ if (allQuestions.length === newQuestions.length) {
2872
+ return false;
2873
+ }
2874
+ questionBankManager.saveAll(newQuestions);
2875
+ return true;
2876
+ }
2877
+ };
2878
+
2879
+ // src/services/HTMLLauncherGenerator.ts
2880
+ var escapeAttribute = (unsafe) => {
2881
+ if (typeof unsafe !== "string") return "";
2882
+ return unsafe.replace(/"/g, '"');
2883
+ };
2884
+ var generateLauncherHTML = (quizConfig, libraryJSPath, quizDataPath, blocklyCSSPath, mainCSSPath, title) => {
2885
+ const pageTitle = escapeAttribute(title || quizConfig.title || "Interactive Quiz");
2886
+ const relLibraryJSPath = libraryJSPath.startsWith("./") ? libraryJSPath : `./${libraryJSPath}`;
2887
+ const relQuizDataPath = quizDataPath.startsWith("./") ? quizDataPath : `./${quizDataPath}`;
2888
+ const relBlocklyCSSPath = blocklyCSSPath.startsWith("./") ? blocklyCSSPath : `./${blocklyCSSPath}`;
2889
+ const relMainCSSPath = mainCSSPath.startsWith("./") ? mainCSSPath : `./${mainCSSPath}`;
2890
+ return `<!DOCTYPE html>
2891
+ <html lang="en">
2892
+ <head>
2893
+ <meta charset="UTF-8">
2894
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2895
+ <title>${pageTitle}</title>
2896
+ <link rel="stylesheet" href="${escapeAttribute(relMainCSSPath)}">
2897
+ <link rel="stylesheet" href="${escapeAttribute(relBlocklyCSSPath)}">
2898
+ <style>
2899
+ /* --- CSS \u0110\u1EC2 C\u0102N GI\u1EEEA CARD --- */
2900
+ html, body {
2901
+ height: 100%; /* \u0110\u1EA3m b\u1EA3o body chi\u1EBFm to\xE0n b\u1ED9 chi\u1EC1u cao */
2902
+ margin: 0;
2903
+ }
2904
+ body {
2905
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
2906
+ background-color: #f0f2f5; /* M\xE0u n\u1EC1n x\xE1m */
2907
+ color: #1f2937;
2908
+
2909
+ /* S\u1EED d\u1EE5ng Flexbox \u0111\u1EC3 c\u0103n gi\u1EEFa */
2910
+ display: flex;
2911
+ justify-content: center; /* C\u0103n gi\u1EEFa theo chi\u1EC1u ngang */
2912
+ align-items: center; /* C\u0103n gi\u1EEFa theo chi\u1EC1u d\u1ECDc */
2913
+
2914
+ padding: 20px; /* Th\xEAm m\u1ED9t ch\xFAt l\u1EC1 cho \u0111\u1EB9p m\u1EAFt tr\xEAn m\xE0n h\xECnh nh\u1ECF */
2915
+ box-sizing: border-box;
2916
+ }
2917
+ #root {
2918
+ width: 100%;
2919
+ max-width: 900px; /* Gi\u1EDBi h\u1EA1n chi\u1EC1u r\u1ED9ng t\u1ED1i \u0111a c\u1EE7a card quiz */
2920
+ /* #root s\u1EBD t\u1EF1 \u0111\u1ED9ng co l\u1EA1i theo n\u1ED9i dung b\xEAn trong n\xF3, nh\u01B0ng kh\xF4ng v\u01B0\u1EE3t qu\xE1 900px */
2921
+ }
2922
+ /* C\xE1c style c\xF2n l\u1EA1i gi\u1EEF nguy\xEAn */
2923
+ .loading-spinner { border: 4px solid #e5e7eb; border-top: 4px solid #3b82f6; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 60px auto 20px auto; }
2924
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
2925
+ .status-message { text-align: center; padding: 20px; margin-top: 10px; color: #4b5563; }
2926
+ </style>
2927
+ </head>
2928
+ <body>
2929
+ <div id="root">
2930
+ <!-- N\u1ED9i dung ban \u0111\u1EA7u s\u1EBD \u0111\u01B0\u1EE3c React thay th\u1EBF -->
2931
+ <div class="loading-spinner" aria-label="Loading quiz content"></div>
2932
+ <p class="status-message" role="status">Loading Quiz...</p>
2933
+ </div>
2934
+
2935
+ <script type="module">
2936
+ // ... To\xE0n b\u1ED9 ph\u1EA7n script gi\u1EEF nguy\xEAn nh\u01B0 c\u0169 ...
2937
+ import { mountQuizPlayer } from '${escapeAttribute(relLibraryJSPath)}';
2938
+
2939
+ function showStatusMessage(message, isError = false) {
2940
+ const rootEl = document.getElementById('root');
2941
+ if (rootEl) {
2942
+ rootEl.innerHTML = '';
2943
+ const messageEl = document.createElement('p');
2944
+ messageEl.textContent = message;
2945
+ messageEl.className = 'status-message';
2946
+ if(isError) messageEl.style.color = '#ef4444';
2947
+ rootEl.appendChild(messageEl);
2948
+ }
2949
+ }
2950
+
2951
+ async function main() {
2952
+ let quizConfigData;
2953
+ try {
2954
+ const response = await fetch('${escapeAttribute(relQuizDataPath)}');
2955
+ if (!response.ok) {
2956
+ throw new Error('Failed to load quiz data: Status ' + response.status + ' - ' + response.statusText);
2957
+ }
2958
+ quizConfigData = await response.json();
2959
+ } catch (error) {
2960
+ console.error("Error loading quiz data:", error);
2961
+ showStatusMessage('Error: Could not load quiz configuration. ' + error.message, true);
2962
+ return;
2963
+ }
2964
+
2965
+ if (typeof mountQuizPlayer !== 'function') {
2966
+ showStatusMessage('Error: Quiz mount function not found in the library bundle. The build might be corrupted.', true);
2967
+ return;
2968
+ }
2969
+
2970
+ const rootElement = document.getElementById('root');
2971
+ if (rootElement) {
2972
+ rootElement.innerHTML = '';
2973
+ mountQuizPlayer('root', quizConfigData);
2974
+ } else {
2975
+ console.error('Critical Error: Root element (#root) not found in the DOM.');
2976
+ document.body.innerHTML = '<p style="color: red; text-align: center; padding: 20px;">Critical Error: Root HTML element not found.</p>';
2977
+ }
2978
+ }
2979
+
2980
+ if (document.readyState === 'loading') {
2981
+ document.addEventListener('DOMContentLoaded', main);
2982
+ } else {
2983
+ main();
2984
+ }
2985
+ </script>
2986
+ </body>
2987
+ </html>`;
2988
+ };
2989
+
2990
+ // src/services/SCORMManifestGenerator.ts
2991
+ var escapeXML = (unsafe) => {
2992
+ if (typeof unsafe !== "string") return "";
2993
+ return unsafe.replace(/[<>&'"]/g, (c) => {
2994
+ switch (c) {
2995
+ case "<":
2996
+ return "&lt;";
2997
+ case ">":
2998
+ return "&gt;";
2999
+ case "&":
3000
+ return "&amp;";
3001
+ case "'":
3002
+ return "&apos;";
3003
+ case '"':
3004
+ return "&quot;";
3005
+ default:
3006
+ return c;
3007
+ }
3008
+ });
3009
+ };
3010
+ var generateSCORMManifest = (quizConfig, scormVersion, launcherFile = "index.html", libraryJSPath = "scorm-bundle/player.js", quizDataPath = "quiz_data.json", blocklyCSSPath = "blockly-styles.css", mainCSSPath = "styles.css") => {
3011
+ var _a;
3012
+ const uniqueId = `iqk_${quizConfig.id.replace(/[^a-zA-Z0-9_]/g, "_")}`;
3013
+ const organizationId = `ORG-${uniqueId}`;
3014
+ const itemId = `ITEM-${uniqueId}`;
3015
+ const resourceId = `RES-${uniqueId}`;
3016
+ const quizTitle = escapeXML(quizConfig.title);
3017
+ const passingScore = (_a = quizConfig.settings) == null ? void 0 : _a.passingScorePercent;
3018
+ const effectiveScormVersion = scormVersion;
3019
+ const schemaVersion = effectiveScormVersion === "2004" ? "2004 4th Edition" : "1.2";
3020
+ const adlcpNamespace = effectiveScormVersion === "2004" ? "http://www.adlnet.org/xsd/adlcp_v1p3" : "http://www.adlnet.org/xsd/adlcp_rootv1p2";
3021
+ const imsmdNamespace = effectiveScormVersion === "2004" ? "http://www.imsglobal.org/xsd/imsmd_v1p2" : "http://www.imsglobal.org/xsd/imsmd_rootv1p2p1";
3022
+ 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";
3023
+ const files = [
3024
+ launcherFile,
3025
+ libraryJSPath,
3026
+ quizDataPath,
3027
+ blocklyCSSPath,
3028
+ mainCSSPath
3029
+ ].map((file) => `<file href="${escapeXML(file)}"/>`).join("\n ");
3030
+ const manifestHeader = effectiveScormVersion === "2004" ? `<?xml version="1.0" encoding="UTF-8"?>
3031
+ <manifest identifier="${uniqueId}-MANIFEST" version="1.0"
3032
+ xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
3033
+ xmlns:adlcp="${adlcpNamespace}"
3034
+ xmlns:imsmd="${imsmdNamespace}"
3035
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3036
+ xsi:schemaLocation="${xsiSchemaLocation}">` : (
3037
+ // SCORM 1.2
3038
+ `<?xml version="1.0" encoding="UTF-8"?>
3039
+ <manifest identifier="${uniqueId}-MANIFEST" version="1.2"
3040
+ xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
3041
+ xmlns:adlcp="${adlcpNamespace}"
3042
+ xmlns:imsmd="${imsmdNamespace}"
3043
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3044
+ xsi:schemaLocation="${xsiSchemaLocation}">`
3045
+ );
3046
+ const organizationStructure = effectiveScormVersion === "2004" ? `<organizations default="${organizationId}">
3047
+ <organization identifier="${organizationId}" structure="hierarchical">
3048
+ <title>${quizTitle}</title>
3049
+ <item identifier="${itemId}" identifierref="${resourceId}">
3050
+ <title>${quizTitle}</title>
3051
+ ${passingScore !== void 0 ? `<adlcp:masteryscore>${passingScore}</adlcp:masteryscore>` : ""}
3052
+ </item>
3053
+ </organization>
3054
+ </organizations>` : (
3055
+ // SCORM 1.2
3056
+ `<organizations default="${organizationId}">
3057
+ <organization identifier="${organizationId}">
3058
+ <title>${quizTitle}</title>
3059
+ <item identifier="${itemId}" identifierref="${resourceId}" isvisible="true">
3060
+ <title>${quizTitle}</title>
3061
+ ${passingScore !== void 0 ? `<adlcp:masteryscore>${passingScore}</adlcp:masteryscore>` : ""}
3062
+ </item>
3063
+ </organization>
3064
+ </organizations>`
3065
+ );
3066
+ const resourceScormType = effectiveScormVersion === "2004" ? "sco" : "sco";
3067
+ return `${manifestHeader}
3068
+ <metadata>
3069
+ <schema>ADL SCORM</schema>
3070
+ <schemaversion>${schemaVersion}</schemaversion>
3071
+ <imsmd:lom>
3072
+ <imsmd:general>
3073
+ <imsmd:title>
3074
+ <imsmd:langstring xml:lang="en">${quizTitle}</imsmd:langstring>
3075
+ </imsmd:title>
3076
+ ${quizConfig.description ? `<imsmd:description><imsmd:langstring xml:lang="en">${escapeXML(quizConfig.description)}</imsmd:langstring></imsmd:description>` : ""}
3077
+ </imsmd:general>
3078
+ </imsmd:lom>
3079
+ </metadata>
3080
+ ${organizationStructure}
3081
+ <resources>
3082
+ <resource identifier="${resourceId}" type="webcontent" adlcp:scormtype="${resourceScormType}" href="${escapeXML(launcherFile)}">
3083
+ ${files}
3084
+ </resource>
3085
+ </resources>
3086
+ </manifest>`;
3087
+ };
3088
+ var sanitizeFilename = (name) => {
3089
+ return name.replace(/[^a-z0-9_.-]/gi, "_").toLowerCase();
3090
+ };
3091
+ var exportQuizAsSCORMZip = async (quiz, options) => {
3092
+ try {
3093
+ const zip = new JSZip();
3094
+ const playerJSUrlToFetch = "/static/scorm-bundle/player.js";
3095
+ const mainCSSUrlToFetch = "/static/scorm-bundle/styles.css";
3096
+ const blocklyCSSUrlToFetch = "/blockly-styles.css";
3097
+ const libraryJSPathInZip = "player.js";
3098
+ const mainCSSPathInZip = "styles.css";
3099
+ const blocklyCSSPathInZip = "blockly-styles.css";
3100
+ const quizDataPathInZip = "quiz_data.json";
3101
+ console.log(`Fetching Player JS from: ${playerJSUrlToFetch}`);
3102
+ console.log(`Fetching Main CSS from: ${mainCSSUrlToFetch}`);
3103
+ const [playerJSContent, mainCSSContent, blocklyCSSContent] = await Promise.all([
3104
+ // Fetch file JS của player
3105
+ fetch(playerJSUrlToFetch).then((res) => {
3106
+ if (!res.ok) throw new Error(`Could not fetch Player JS at ${playerJSUrlToFetch}. Make sure the file exists in your app's public folder.`);
3107
+ return res.text();
3108
+ }),
3109
+ // Fetch file CSS chính
3110
+ fetch(mainCSSUrlToFetch).then((res) => {
3111
+ if (!res.ok) throw new Error(`Could not fetch Main CSS at ${mainCSSUrlToFetch}. Make sure the file exists in your app's public folder.`);
3112
+ return res.text();
3113
+ }),
3114
+ // Fetch file CSS của Blockly (tùy chọn)
3115
+ fetch(blocklyCSSUrlToFetch).then((res) => {
3116
+ if (!res.ok) {
3117
+ console.warn(`Could not fetch ${blocklyCSSUrlToFetch}. This is okay if you don't use Blockly/Scratch questions.`);
3118
+ return "";
3119
+ }
3120
+ return res.text();
3121
+ })
3122
+ ]);
3123
+ zip.file(libraryJSPathInZip, playerJSContent);
3124
+ zip.file(mainCSSPathInZip, mainCSSContent);
3125
+ if (blocklyCSSContent) {
3126
+ zip.file(blocklyCSSPathInZip, blocklyCSSContent);
3127
+ }
3128
+ const quizDataString = JSON.stringify(quiz, null, 2);
3129
+ zip.file(quizDataPathInZip, quizDataString);
3130
+ const manifestContent = generateSCORMManifest(
3131
+ quiz,
3132
+ options.scormVersion,
3133
+ "index.html",
3134
+ libraryJSPathInZip,
3135
+ quizDataPathInZip,
3136
+ blocklyCSSPathInZip,
3137
+ mainCSSPathInZip
3138
+ );
3139
+ zip.file("imsmanifest.xml", manifestContent);
3140
+ const launcherContent = generateLauncherHTML(
3141
+ quiz,
3142
+ libraryJSPathInZip,
3143
+ quizDataPathInZip,
3144
+ blocklyCSSPathInZip,
3145
+ mainCSSPathInZip,
3146
+ quiz.title
3147
+ );
3148
+ zip.file("index.html", launcherContent);
3149
+ const blob = await zip.generateAsync({ type: "blob" });
3150
+ const fileName = `${sanitizeFilename(quiz.title || "quiz")}_scorm_${options.scormVersion.replace(".", "_")}.zip`;
3151
+ const link = document.createElement("a");
3152
+ link.href = URL.createObjectURL(blob);
3153
+ link.download = fileName;
3154
+ document.body.appendChild(link);
3155
+ link.click();
3156
+ document.body.removeChild(link);
3157
+ URL.revokeObjectURL(link.href);
3158
+ return { success: true, fileName };
3159
+ } catch (err) {
3160
+ console.error("Error creating SCORM ZIP:", err);
3161
+ return { success: false, error: err instanceof Error ? err.message : "Unknown error during ZIP creation." };
3162
+ }
3163
+ };
3164
+
3165
+ // src/services/sampleQuiz.ts
3166
+ var trueFalseQ1 = {
3167
+ id: generateUniqueId("tfq_"),
3168
+ questionType: "true_false",
3169
+ prompt: "B\u1EA7u tr\u1EDDi c\xF3 m\xE0u xanh do hi\u1EC7n t\u01B0\u1EE3ng t\xE1n x\u1EA1 Rayleigh.",
3170
+ correctAnswer: true,
3171
+ points: 10,
3172
+ 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.",
3173
+ difficulty: "easy",
3174
+ topic: "V\u1EADt l\xFD",
3175
+ category: "Khoa h\u1ECDc",
3176
+ learningObjective: "Hi\u1EC3u v\u1EC1 quang h\u1ECDc kh\xED quy\u1EC3n c\u01A1 b\u1EA3n."
3177
+ };
3178
+ var mcq1 = {
3179
+ id: generateUniqueId("mcq_"),
3180
+ questionType: "multiple_choice",
3181
+ prompt: "Th\u1EE7 \u0111\xF4 c\u1EE7a Ph\xE1p l\xE0 g\xEC?",
3182
+ options: [
3183
+ { id: generateUniqueId("opt_"), text: "Berlin" },
3184
+ { id: generateUniqueId("opt_"), text: "Madrid" },
3185
+ { id: generateUniqueId("opt_"), text: "Paris" },
3186
+ { id: generateUniqueId("opt_"), text: "Rome" }
3187
+ ],
3188
+ correctAnswerId: "",
3189
+ points: 15,
3190
+ difficulty: "easy",
3191
+ topic: "\u0110\u1ECBa l\xFD",
3192
+ category: "Khoa h\u1ECDc X\xE3 h\u1ED9i"
3193
+ };
3194
+ var parisOption = mcq1.options.find((opt) => opt.text === "Paris");
3195
+ if (parisOption) {
3196
+ mcq1.correctAnswerId = parisOption.id;
3197
+ }
3198
+ var mrq1_opt1_id = generateUniqueId("opt_");
3199
+ var mrq1_opt2_id = generateUniqueId("opt_");
3200
+ var mrq1_opt3_id = generateUniqueId("opt_");
3201
+ var mrq1_opt4_id = generateUniqueId("opt_");
3202
+ var mrq1_opt5_id = generateUniqueId("opt_");
3203
+ var mrq1 = {
3204
+ id: generateUniqueId("mrq_"),
3205
+ questionType: "multiple_response",
3206
+ 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)?",
3207
+ options: [
3208
+ { id: mrq1_opt1_id, text: "Sao Th\u1ED5 (Saturn)" },
3209
+ { id: mrq1_opt2_id, text: "Sao M\u1ED9c (Jupiter)" },
3210
+ { id: mrq1_opt3_id, text: "Sao Thi\xEAn V\u01B0\u01A1ng (Uranus)" },
3211
+ { id: mrq1_opt4_id, text: "Sao H\u1EA3i V\u01B0\u01A1ng (Neptune)" },
3212
+ { id: mrq1_opt5_id, text: "Tr\xE1i \u0110\u1EA5t (Earth)" }
3213
+ ],
3214
+ correctAnswerIds: [mrq1_opt1_id, mrq1_opt2_id, mrq1_opt3_id, mrq1_opt4_id],
3215
+ points: 20,
3216
+ 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.",
3217
+ difficulty: "medium",
3218
+ topic: "Thi\xEAn v\u0103n h\u1ECDc",
3219
+ category: "Khoa h\u1ECDc"
3220
+ };
3221
+ var shortAnswerQ1 = {
3222
+ id: generateUniqueId("saq_"),
3223
+ questionType: "short_answer",
3224
+ 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?",
3225
+ acceptedAnswers: ["JavaScript", "Javascript", "javascript", "JS", "js"],
3226
+ points: 10,
3227
+ 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.",
3228
+ difficulty: "easy",
3229
+ topic: "Ph\xE1t tri\u1EC3n Web",
3230
+ category: "C\xF4ng ngh\u1EC7",
3231
+ isCaseSensitive: false
3232
+ };
3233
+ var numericQ1 = {
3234
+ id: generateUniqueId("nq_"),
3235
+ questionType: "numeric",
3236
+ 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?",
3237
+ answer: 100,
3238
+ tolerance: 1,
3239
+ points: 10,
3240
+ 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.",
3241
+ difficulty: "easy",
3242
+ topic: "H\xF3a h\u1ECDc",
3243
+ category: "Khoa h\u1ECDc"
3244
+ };
3245
+ var fillInTheBlanksQ1 = {
3246
+ id: generateUniqueId("fitb_"),
3247
+ questionType: "fill_in_the_blanks",
3248
+ prompt: "\u0110i\u1EC1n v\xE0o ch\u1ED7 tr\u1ED1ng \u0111\u1EC3 ho\xE0n th\xE0nh c\xE2u sau:",
3249
+ segments: [
3250
+ { type: "text", content: "N\u01B0\u1EDBc \u0111\u01B0\u1EE3c c\u1EA5u t\u1EA1o t\u1EEB hai nguy\xEAn t\u1ED1 l\xE0 " },
3251
+ { type: "blank", id: "fitb_h" },
3252
+ { type: "text", content: " v\xE0 " },
3253
+ { type: "blank", id: "fitb_o" },
3254
+ { type: "text", content: "." }
3255
+ ],
3256
+ answers: [
3257
+ { blankId: "fitb_h", acceptedValues: ["Hydro", "Hydrogen", "H"] },
3258
+ { blankId: "fitb_o", acceptedValues: ["Oxy", "Oxygen", "O"] }
3259
+ ],
3260
+ isCaseSensitive: false,
3261
+ points: 15,
3262
+ 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.",
3263
+ difficulty: "easy",
3264
+ topic: "H\xF3a h\u1ECDc C\u01A1 b\u1EA3n",
3265
+ category: "Khoa h\u1ECDc"
3266
+ };
3267
+ var sequenceQ1_item1_id = generateUniqueId("seqi_");
3268
+ var sequenceQ1_item2_id = generateUniqueId("seqi_");
3269
+ var sequenceQ1_item3_id = generateUniqueId("seqi_");
3270
+ var sequenceQ1_item4_id = generateUniqueId("seqi_");
3271
+ var sequenceQ1 = {
3272
+ id: generateUniqueId("seqq_"),
3273
+ questionType: "sequence",
3274
+ 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:",
3275
+ items: [
3276
+ { id: sequenceQ1_item1_id, content: "Sao H\u1ECFa (Mars)" },
3277
+ { id: sequenceQ1_item2_id, content: "Tr\xE1i \u0110\u1EA5t (Earth)" },
3278
+ { id: sequenceQ1_item3_id, content: "Sao Th\u1EE7y (Mercury)" },
3279
+ { id: sequenceQ1_item4_id, content: "Sao Kim (Venus)" }
3280
+ ],
3281
+ correctOrder: [sequenceQ1_item3_id, sequenceQ1_item4_id, sequenceQ1_item2_id, sequenceQ1_item1_id],
3282
+ points: 20,
3283
+ 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.",
3284
+ difficulty: "medium",
3285
+ topic: "Thi\xEAn v\u0103n h\u1ECDc",
3286
+ category: "Khoa h\u1ECDc"
3287
+ };
3288
+ var matchingQ1_prompt_vn = generateUniqueId("matp_");
3289
+ var matchingQ1_prompt_jp = generateUniqueId("matp_");
3290
+ var matchingQ1_prompt_us = generateUniqueId("matp_");
3291
+ var matchingQ1_opt_hanoi = generateUniqueId("mato_");
3292
+ var matchingQ1_opt_tokyo = generateUniqueId("mato_");
3293
+ var matchingQ1_opt_dc = generateUniqueId("mato_");
3294
+ var matchingQ1 = {
3295
+ id: generateUniqueId("matq_"),
3296
+ questionType: "matching",
3297
+ prompt: "H\xE3y gh\xE9p m\u1ED7i qu\u1ED1c gia v\u1EDBi th\u1EE7 \u0111\xF4 t\u01B0\u01A1ng \u1EE9ng.",
3298
+ prompts: [
3299
+ { id: matchingQ1_prompt_vn, content: "Vi\u1EC7t Nam" },
3300
+ { id: matchingQ1_prompt_jp, content: "Nh\u1EADt B\u1EA3n" },
3301
+ { id: matchingQ1_prompt_us, content: "Hoa K\u1EF3" }
3302
+ ],
3303
+ options: [
3304
+ { id: matchingQ1_opt_tokyo, content: "Tokyo" },
3305
+ { id: matchingQ1_opt_hanoi, content: "H\xE0 N\u1ED9i" },
3306
+ { id: matchingQ1_opt_dc, content: "Washington D.C." }
3307
+ ],
3308
+ correctAnswerMap: [
3309
+ { promptId: matchingQ1_prompt_vn, optionId: matchingQ1_opt_hanoi },
3310
+ { promptId: matchingQ1_prompt_jp, optionId: matchingQ1_opt_tokyo },
3311
+ { promptId: matchingQ1_prompt_us, optionId: matchingQ1_opt_dc }
3312
+ ],
3313
+ points: 15,
3314
+ 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.",
3315
+ difficulty: "easy",
3316
+ topic: "\u0110\u1ECBa l\xFD Th\u1EBF gi\u1EDBi",
3317
+ shuffleOptions: true
3318
+ };
3319
+ var dndQ1_drag_apple = generateUniqueId("dndi_");
3320
+ var dndQ1_drag_banana = generateUniqueId("dndi_");
3321
+ var dndQ1_drag_orange = generateUniqueId("dndi_");
3322
+ var dndQ1_drop_red = generateUniqueId("dndz_");
3323
+ var dndQ1_drop_yellow = generateUniqueId("dndz_");
3324
+ var dndQ1_drop_orange_color = generateUniqueId("dndz_");
3325
+ var dragAndDropQ1 = {
3326
+ id: generateUniqueId("dndq_"),
3327
+ questionType: "drag_and_drop",
3328
+ 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).",
3329
+ draggableItems: [
3330
+ { id: dndQ1_drag_apple, content: "T\xE1o" },
3331
+ { id: dndQ1_drag_banana, content: "Chu\u1ED1i" },
3332
+ { id: dndQ1_drag_orange, content: "Cam" }
3333
+ ],
3334
+ dropZones: [
3335
+ { id: dndQ1_drop_red, label: "Gi\u1ECF \u0110\u1ECF" },
3336
+ { id: dndQ1_drop_yellow, label: "Gi\u1ECF V\xE0ng" },
3337
+ { id: dndQ1_drop_orange_color, label: "Gi\u1ECF Cam" }
3338
+ ],
3339
+ answerMap: [
3340
+ { draggableId: dndQ1_drag_apple, dropZoneId: dndQ1_drop_red },
3341
+ { draggableId: dndQ1_drag_banana, dropZoneId: dndQ1_drop_yellow },
3342
+ { draggableId: dndQ1_drag_orange, dropZoneId: dndQ1_drop_orange_color }
3343
+ ],
3344
+ points: 15,
3345
+ 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).",
3346
+ difficulty: "easy",
3347
+ topic: "M\xE0u s\u1EAFc v\xE0 V\u1EADt th\u1EC3",
3348
+ backgroundImageUrl: "https://placehold.co/600x200.png",
3349
+ imageAltText: "colored baskets"
3350
+ };
3351
+ var hotspotQ1_engine_left = generateUniqueId("hs_");
3352
+ var hotspotQ1_engine_right = generateUniqueId("hs_");
3353
+ var hotspotQ1_cockpit = generateUniqueId("hs_");
3354
+ var hotspotQ1 = {
3355
+ id: generateUniqueId("hsq_"),
3356
+ questionType: "hotspot",
3357
+ prompt: "Nh\u1EA5p v\xE0o (c\xE1c) \u0111\u1ED9ng c\u01A1 c\u1EE7a m\xE1y bay trong h\xECnh.",
3358
+ imageUrl: "https://placehold.co/600x400.png",
3359
+ imageAltText: "airplane diagram",
3360
+ hotspots: [
3361
+ { id: hotspotQ1_engine_left, shape: "rect", coords: [150, 200, 80, 60], description: "\u0110\u1ED9ng c\u01A1 b\xEAn tr\xE1i" },
3362
+ { id: hotspotQ1_engine_right, shape: "rect", coords: [370, 200, 80, 60], description: "\u0110\u1ED9ng c\u01A1 b\xEAn ph\u1EA3i" },
3363
+ { id: hotspotQ1_cockpit, shape: "rect", coords: [250, 120, 100, 70], description: "Bu\u1ED3ng l\xE1i" }
3364
+ ],
3365
+ correctHotspotIds: [hotspotQ1_engine_left, hotspotQ1_engine_right],
3366
+ points: 15,
3367
+ explanation: "M\xE1y bay n\xE0y c\xF3 hai \u0111\u1ED9ng c\u01A1 ch\xEDnh, n\u1EB1m d\u01B0\u1EDBi c\xE1nh.",
3368
+ difficulty: "medium",
3369
+ topic: "H\xE0ng kh\xF4ng",
3370
+ category: "K\u1EF9 thu\u1EADt"
3371
+ };
3372
+ var blocklyQ1 = {
3373
+ id: generateUniqueId("blkq_"),
3374
+ questionType: "blockly_programming",
3375
+ 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.",
3376
+ points: 25,
3377
+ difficulty: "easy",
3378
+ topic: "L\u1EADp tr\xECnh C\u01A1 b\u1EA3n",
3379
+ category: "C\xF4ng ngh\u1EC7 Th\xF4ng tin",
3380
+ toolboxDefinition: `
3381
+ <xml xmlns="https://developers.google.com/blockly/xml">
3382
+ <category name="Text" colour="%{BKY_TEXTS_HUE}">
3383
+ <block type="text"></block>
3384
+ <block type="text_print"></block>
3385
+ </category>
3386
+ </xml>
3387
+ `,
3388
+ initialWorkspace: `
3389
+ <xml xmlns="https://developers.google.com/blockly/xml">
3390
+ <block type="text_print" id="${generateUniqueId("blki_")}" x="70" y="70">
3391
+ <value name="TEXT">
3392
+ <shadow type="text" id="${generateUniqueId("blki_")}">
3393
+ <field name="TEXT">abc</field>
3394
+ </shadow>
3395
+ </value>
3396
+ </block>
3397
+ </xml>
3398
+ `,
3399
+ solutionWorkspaceXML: `
3400
+ <xml xmlns="https://developers.google.com/blockly/xml">
3401
+ <block type="text_print" id="${generateUniqueId("blki_solution_")}" x="70" y="70">
3402
+ <value name="TEXT">
3403
+ <block type="text" id="${generateUniqueId("blki_text_solution_")}">
3404
+ <field name="TEXT">Hello, World!</field>
3405
+ </block>
3406
+ </value>
3407
+ </block>
3408
+ </xml>
3409
+ `,
3410
+ solutionGeneratedCode: "window.alert('Hello, World!');",
3411
+ // Normalized JS code
3412
+ 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!'."
3413
+ };
3414
+ var scratchQ1 = {
3415
+ id: generateUniqueId("scrq_"),
3416
+ questionType: "scratch_programming",
3417
+ 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.",
3418
+ points: 20,
3419
+ difficulty: "easy",
3420
+ topic: "L\u1EADp tr\xECnh Scratch",
3421
+ category: "C\xF4ng ngh\u1EC7 Th\xF4ng tin",
3422
+ toolboxDefinition: `
3423
+ <xml xmlns="https://developers.google.com/blockly/xml">
3424
+ <category name="Motion" colour="#4C97FF">
3425
+ <block type="motion_movesteps"></block>
3426
+ </category>
3427
+ <category name="Events" colour="#FFBF00">
3428
+ <block type="event_whenflagclicked"></block>
3429
+ </category>
3430
+ </xml>
3431
+ `,
3432
+ initialWorkspace: `
3433
+ <xml xmlns="https://developers.google.com/blockly/xml"></xml>
3434
+ `,
3435
+ solutionWorkspaceXML: `
3436
+ <xml xmlns="https://developers.google.com/blockly/xml">
3437
+ <block type="event_whenflagclicked" id="${generateUniqueId("scr_event_")}" x="50" y="50">
3438
+ <next>
3439
+ <block type="motion_movesteps" id="${generateUniqueId("scr_motion_")}">
3440
+ <value name="STEPS">
3441
+ <shadow type="math_number">
3442
+ <field name="NUM">10</field>
3443
+ </shadow>
3444
+ </value>
3445
+ </block>
3446
+ </next>
3447
+ </block>
3448
+ </xml>
3449
+ `,
3450
+ solutionGeneratedCode: "whenGreenFlagClicked(() => { move(10); });",
3451
+ // Example pseudo-code or JS representation
3452
+ explanation: "S\u1EED d\u1EE5ng kh\u1ED1i 'when green flag clicked' t\u1EEB Events v\xE0 kh\u1ED1i 'move 10 steps' t\u1EEB Motion."
3453
+ };
3454
+ var sampleQuiz = {
3455
+ id: "sample-quiz-001",
3456
+ title: "Sample Quiz for Testers",
3457
+ description: "A short quiz with a few different question types to test the QuizKit functionality.",
3458
+ questions: [
3459
+ trueFalseQ1,
3460
+ mcq1,
3461
+ mrq1,
3462
+ shortAnswerQ1,
3463
+ numericQ1,
3464
+ fillInTheBlanksQ1,
3465
+ sequenceQ1,
3466
+ matchingQ1,
3467
+ dragAndDropQ1,
3468
+ hotspotQ1,
3469
+ blocklyQ1,
3470
+ scratchQ1
3471
+ // Added Scratch question
3472
+ ],
3473
+ settings: {
3474
+ shuffleQuestions: true,
3475
+ shuffleOptions: true,
3476
+ showCorrectAnswers: "end_of_quiz",
3477
+ passingScorePercent: 70,
3478
+ timeLimitMinutes: 25
3479
+ }
3480
+ };
3481
+ var emptyQuiz = {
3482
+ id: generateUniqueId("quiz_"),
3483
+ title: "New Quiz",
3484
+ description: "",
3485
+ questions: [],
3486
+ settings: {
3487
+ language: "English",
3488
+ // <-- ĐÃ THÊM
3489
+ shuffleQuestions: false,
3490
+ shuffleOptions: false,
3491
+ showCorrectAnswers: "end_of_quiz",
3492
+ passingScorePercent: 0,
3493
+ timeLimitMinutes: 0
3494
+ }
3495
+ };
3496
+ function cn(...inputs) {
3497
+ return twMerge(clsx(inputs));
3498
+ }
3499
+
3500
+ export { APIKeyService, AchievementService, GEMINI_API_KEY_SERVICE_NAME, KnowledgeCardService, MetadataService, PracticeHistoryService, QuestionBankService, QuestionImportService, QuizEditorService, QuizEngine, QuoteService, SCORMService, UserConfigService, cn, emptyQuiz, exportQuizAsSCORMZip, generateLauncherHTML, generateSCORMManifest, generateUniqueId, sampleQuiz };