@thanh01.pmt/interactive-quiz-kit 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/dist/ai.cjs +0 -1449
- package/dist/ai.js +0 -1438
- package/dist/authoring.cjs +0 -9447
- package/dist/authoring.js +0 -9379
- package/dist/index.cjs +0 -2458
- package/dist/index.js +0 -2440
- package/dist/player.cjs +0 -2942
- package/dist/player.js +0 -2906
- package/dist/react-ui.cjs +0 -9584
- package/dist/react-ui.js +0 -9505
package/dist/index.cjs
DELETED
|
@@ -1,2458 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var zod = require('zod');
|
|
4
|
-
var JSZip = require('jszip');
|
|
5
|
-
var clsx = require('clsx');
|
|
6
|
-
var tailwindMerge = require('tailwind-merge');
|
|
7
|
-
|
|
8
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
-
|
|
10
|
-
var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
|
|
11
|
-
|
|
12
|
-
var __defProp = Object.defineProperty;
|
|
13
|
-
var __defProps = Object.defineProperties;
|
|
14
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
15
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
16
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
17
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
18
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
19
|
-
var __spreadValues = (a, b) => {
|
|
20
|
-
for (var prop in b || (b = {}))
|
|
21
|
-
if (__hasOwnProp.call(b, prop))
|
|
22
|
-
__defNormalProp(a, prop, b[prop]);
|
|
23
|
-
if (__getOwnPropSymbols)
|
|
24
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
25
|
-
if (__propIsEnum.call(b, prop))
|
|
26
|
-
__defNormalProp(a, prop, b[prop]);
|
|
27
|
-
}
|
|
28
|
-
return a;
|
|
29
|
-
};
|
|
30
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
31
|
-
|
|
32
|
-
// src/services/SCORMService.ts
|
|
33
|
-
var SCORM_TRUE = "true";
|
|
34
|
-
var SCORM_NO_ERROR = "0";
|
|
35
|
-
var CMI_CORE_LESSON_STATUS_PASSED = "passed";
|
|
36
|
-
var CMI_CORE_LESSON_STATUS_FAILED = "failed";
|
|
37
|
-
var CMI_CORE_LESSON_STATUS_COMPLETED = "completed";
|
|
38
|
-
var CMI_CORE_LESSON_STATUS_INCOMPLETE = "incomplete";
|
|
39
|
-
var CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED = "not attempted";
|
|
40
|
-
var CMI_COMPLETION_STATUS_COMPLETED = "completed";
|
|
41
|
-
var CMI_COMPLETION_STATUS_INCOMPLETE = "incomplete";
|
|
42
|
-
var CMI_SUCCESS_STATUS_PASSED = "passed";
|
|
43
|
-
var CMI_SUCCESS_STATUS_FAILED = "failed";
|
|
44
|
-
var SCORMService = class {
|
|
45
|
-
constructor(settings) {
|
|
46
|
-
this.scormAPI = null;
|
|
47
|
-
this.scormVersionFound = null;
|
|
48
|
-
this.isInitialized = false;
|
|
49
|
-
this.isTerminated = false;
|
|
50
|
-
this.studentName = null;
|
|
51
|
-
this.settings = __spreadValues({
|
|
52
|
-
setCompletionOnFinish: true,
|
|
53
|
-
setSuccessOnPass: true,
|
|
54
|
-
autoCommit: true
|
|
55
|
-
}, settings);
|
|
56
|
-
if (typeof window !== "undefined") {
|
|
57
|
-
this._findAPI();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
_findAPIRecursive(win) {
|
|
61
|
-
if (win === null) return null;
|
|
62
|
-
if (win.API_1484_11) {
|
|
63
|
-
this.scormVersionFound = "2004";
|
|
64
|
-
return win.API_1484_11;
|
|
65
|
-
}
|
|
66
|
-
if (win.API) {
|
|
67
|
-
this.scormVersionFound = "1.2";
|
|
68
|
-
return win.API;
|
|
69
|
-
}
|
|
70
|
-
if (win.parent && win.parent !== win) {
|
|
71
|
-
return this._findAPIRecursive(win.parent);
|
|
72
|
-
}
|
|
73
|
-
if (win.opener && typeof win.opener !== "undefined" && win.opener !== win && win.opener !== win.parent) {
|
|
74
|
-
try {
|
|
75
|
-
if (win.opener.document) {
|
|
76
|
-
return this._findAPIRecursive(win.opener);
|
|
77
|
-
}
|
|
78
|
-
} catch (e) {
|
|
79
|
-
console.warn("SCORMService: Could not access win.opener for API search due to cross-origin restrictions.");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
_findAPI() {
|
|
85
|
-
try {
|
|
86
|
-
this.scormAPI = this._findAPIRecursive(window);
|
|
87
|
-
if (this.scormAPI) {
|
|
88
|
-
if (!this.scormVersionFound) this.scormVersionFound = this.settings.version;
|
|
89
|
-
console.log(`SCORMService: API Found. Version determined: ${this.scormVersionFound}`);
|
|
90
|
-
} else {
|
|
91
|
-
console.warn("SCORMService: SCORM API not found in window hierarchy.");
|
|
92
|
-
}
|
|
93
|
-
} catch (e) {
|
|
94
|
-
console.error("SCORMService: Error finding SCORM API", e);
|
|
95
|
-
this.scormAPI = null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
hasAPI() {
|
|
99
|
-
return this.scormAPI !== null;
|
|
100
|
-
}
|
|
101
|
-
getSCORMVersion() {
|
|
102
|
-
return this.scormVersionFound;
|
|
103
|
-
}
|
|
104
|
-
initialize() {
|
|
105
|
-
if (!this.hasAPI()) return { success: false, error: "SCORM API not found." };
|
|
106
|
-
if (this.isInitialized) return { success: true, studentName: this.studentName || void 0 };
|
|
107
|
-
const result = this.scormVersionFound === "2004" ? this.scormAPI.Initialize("") : this.scormAPI.LMSInitialize("");
|
|
108
|
-
if (result.toString() === SCORM_TRUE || result === true) {
|
|
109
|
-
this.isInitialized = true;
|
|
110
|
-
this.isTerminated = false;
|
|
111
|
-
const studentNameVar = this.settings.studentNameVar || (this.scormVersionFound === "2004" ? "cmi.learner_name" : "cmi.core.student_name");
|
|
112
|
-
this.studentName = this.getValue(studentNameVar);
|
|
113
|
-
if (this.scormVersionFound === "2004") {
|
|
114
|
-
const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
|
|
115
|
-
if (this.getValue(completionStatusVar) === "not attempted") {
|
|
116
|
-
this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
|
|
120
|
-
if (this.getValue(lessonStatusVar) === CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED) {
|
|
121
|
-
this.setValue(lessonStatusVar, CMI_CORE_LESSON_STATUS_INCOMPLETE);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (this.settings.autoCommit) this.commit();
|
|
125
|
-
return { success: true, studentName: this.studentName || void 0 };
|
|
126
|
-
} else {
|
|
127
|
-
const error = this.getLastError();
|
|
128
|
-
return { success: false, error: `Initialization failed: ${error.message}` };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
terminate() {
|
|
132
|
-
if (!this.hasAPI() || !this.isInitialized || this.isTerminated) {
|
|
133
|
-
const reason = !this.hasAPI() ? "API not found" : !this.isInitialized ? "Not initialized" : "Already terminated";
|
|
134
|
-
return { success: !this.hasAPI() || this.isTerminated, error: this.isTerminated ? void 0 : reason };
|
|
135
|
-
}
|
|
136
|
-
const result = this.scormVersionFound === "2004" ? this.scormAPI.Terminate("") : this.scormAPI.LMSFinish("");
|
|
137
|
-
if (result.toString() === SCORM_TRUE || result === true) {
|
|
138
|
-
this.isTerminated = true;
|
|
139
|
-
this.isInitialized = false;
|
|
140
|
-
return { success: true };
|
|
141
|
-
} else {
|
|
142
|
-
const error = this.getLastError();
|
|
143
|
-
return { success: false, error: `Termination failed: ${error.message}` };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
setValue(element, value) {
|
|
147
|
-
if (!this.hasAPI() || !this.isInitialized) {
|
|
148
|
-
return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
|
|
149
|
-
}
|
|
150
|
-
const valStr = value.toString();
|
|
151
|
-
const result = this.scormVersionFound === "2004" ? this.scormAPI.SetValue(element, valStr) : this.scormAPI.LMSSetValue(element, valStr);
|
|
152
|
-
if (result.toString() === SCORM_TRUE || result === true) {
|
|
153
|
-
if (this.settings.autoCommit) this.commit();
|
|
154
|
-
return { success: true };
|
|
155
|
-
} else {
|
|
156
|
-
const error = this.getLastError();
|
|
157
|
-
return { success: false, error: `SetValue failed for ${element}: ${error.message}` };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
getValue(element) {
|
|
161
|
-
var _a;
|
|
162
|
-
if (!this.hasAPI() || !this.isInitialized) return null;
|
|
163
|
-
const value = this.scormVersionFound === "2004" ? this.scormAPI.GetValue(element) : this.scormAPI.LMSGetValue(element);
|
|
164
|
-
const error = this.getLastError();
|
|
165
|
-
if (error.code !== SCORM_NO_ERROR && error.code !== "403" && error.code !== "0") {
|
|
166
|
-
console.warn(`SCORMService: GetValue for ${element} produced an error ${error.code}: ${error.message}. Returning raw value:`, value);
|
|
167
|
-
}
|
|
168
|
-
return (_a = value == null ? void 0 : value.toString()) != null ? _a : null;
|
|
169
|
-
}
|
|
170
|
-
commit() {
|
|
171
|
-
if (!this.hasAPI() || !this.isInitialized) {
|
|
172
|
-
return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
|
|
173
|
-
}
|
|
174
|
-
const result = this.scormVersionFound === "2004" ? this.scormAPI.Commit("") : this.scormAPI.LMSCommit("");
|
|
175
|
-
if (result.toString() === SCORM_TRUE || result === true) {
|
|
176
|
-
return { success: true };
|
|
177
|
-
} else {
|
|
178
|
-
const error = this.getLastError();
|
|
179
|
-
return { success: false, error: `Commit failed: ${error.message}` };
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
setScore(rawScore, maxScore, minScore = 0) {
|
|
183
|
-
if (!this.hasAPI() || !this.isInitialized) return;
|
|
184
|
-
if (this.scormVersionFound === "2004") {
|
|
185
|
-
const scoreRawVar = this.settings.scoreRawVar_2004 || this.settings.scoreRawVar || "cmi.score.raw";
|
|
186
|
-
const scoreMaxVar = this.settings.scoreMaxVar_2004 || this.settings.scoreMaxVar || "cmi.score.max";
|
|
187
|
-
const scoreMinVar = this.settings.scoreMinVar_2004 || this.settings.scoreMinVar || "cmi.score.min";
|
|
188
|
-
const scoreScaledVar = this.settings.scoreScaledVar_2004 || "cmi.score.scaled";
|
|
189
|
-
this.setValue(scoreMinVar, minScore);
|
|
190
|
-
this.setValue(scoreMaxVar, maxScore);
|
|
191
|
-
this.setValue(scoreRawVar, rawScore);
|
|
192
|
-
if (maxScore > minScore) {
|
|
193
|
-
const scaledScore = (rawScore - minScore) / (maxScore - minScore);
|
|
194
|
-
this.setValue(scoreScaledVar, parseFloat(scaledScore.toFixed(4)));
|
|
195
|
-
} else if (maxScore === minScore && maxScore !== 0) {
|
|
196
|
-
this.setValue(scoreScaledVar, rawScore >= maxScore ? 1 : 0);
|
|
197
|
-
} else {
|
|
198
|
-
this.setValue(scoreScaledVar, 0);
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
const scoreRawVar = this.settings.scoreRawVar_1_2 || this.settings.scoreRawVar || "cmi.core.score.raw";
|
|
202
|
-
const scoreMaxVar = this.settings.scoreMaxVar_1_2 || this.settings.scoreMaxVar || "cmi.core.score.max";
|
|
203
|
-
const scoreMinVar = this.settings.scoreMinVar_1_2 || this.settings.scoreMinVar || "cmi.core.score.min";
|
|
204
|
-
this.setValue(scoreMinVar, minScore);
|
|
205
|
-
this.setValue(scoreMaxVar, maxScore);
|
|
206
|
-
this.setValue(scoreRawVar, rawScore);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
setLessonStatus(status, passed) {
|
|
210
|
-
if (!this.hasAPI() || !this.isInitialized) return;
|
|
211
|
-
if (this.scormVersionFound === "2004") {
|
|
212
|
-
const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
|
|
213
|
-
const successStatusVar = this.settings.successStatusVar_2004 || "cmi.success_status";
|
|
214
|
-
if (this.settings.setCompletionOnFinish && (status === "completed" || status === "passed" || status === "failed")) {
|
|
215
|
-
this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_COMPLETED);
|
|
216
|
-
} else if (status === "incomplete" || status === "browsed") {
|
|
217
|
-
this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
|
|
218
|
-
}
|
|
219
|
-
if (this.settings.setSuccessOnPass && passed !== void 0) {
|
|
220
|
-
this.setValue(successStatusVar, passed ? CMI_SUCCESS_STATUS_PASSED : CMI_SUCCESS_STATUS_FAILED);
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
|
|
224
|
-
let finalStatus = status;
|
|
225
|
-
if (this.settings.setCompletionOnFinish) {
|
|
226
|
-
if (this.settings.setSuccessOnPass && passed !== void 0) {
|
|
227
|
-
finalStatus = passed ? CMI_CORE_LESSON_STATUS_PASSED : CMI_CORE_LESSON_STATUS_FAILED;
|
|
228
|
-
} else {
|
|
229
|
-
finalStatus = CMI_CORE_LESSON_STATUS_COMPLETED;
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
if (status === CMI_CORE_LESSON_STATUS_PASSED || status === CMI_CORE_LESSON_STATUS_FAILED) ; else {
|
|
233
|
-
finalStatus = CMI_CORE_LESSON_STATUS_INCOMPLETE;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
this.setValue(lessonStatusVar, finalStatus);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
getLastError() {
|
|
240
|
-
var _a, _b;
|
|
241
|
-
if (!this.hasAPI()) return { code: "-1", message: "SCORM API not found." };
|
|
242
|
-
const errorCode = this.scormVersionFound === "2004" ? this.scormAPI.GetLastError() : this.scormAPI.LMSGetLastError();
|
|
243
|
-
if (errorCode === SCORM_NO_ERROR || errorCode === 0 || errorCode === "0") {
|
|
244
|
-
return { code: SCORM_NO_ERROR, message: "No error." };
|
|
245
|
-
}
|
|
246
|
-
const errorMessage = this.scormVersionFound === "2004" ? this.scormAPI.GetErrorString(errorCode.toString()) : this.scormAPI.LMSGetErrorString(errorCode.toString());
|
|
247
|
-
const diagnostic = this.scormVersionFound === "2004" ? this.scormAPI.GetDiagnostic(errorCode.toString()) : this.scormAPI.LMSGetDiagnostic(errorCode.toString());
|
|
248
|
-
return {
|
|
249
|
-
code: errorCode.toString(),
|
|
250
|
-
message: (_a = errorMessage == null ? void 0 : errorMessage.toString()) != null ? _a : "Unknown error.",
|
|
251
|
-
diagnostic: (_b = diagnostic == null ? void 0 : diagnostic.toString()) != null ? _b : void 0
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
formatCMITime(totalSeconds) {
|
|
255
|
-
const pad = (num, size = 2) => num.toString().padStart(size, "0");
|
|
256
|
-
if (this.scormVersionFound === "2004") {
|
|
257
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
258
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
259
|
-
const seconds = parseFloat((totalSeconds % 60).toFixed(2));
|
|
260
|
-
let timeString = "PT";
|
|
261
|
-
if (hours > 0) timeString += `${hours}H`;
|
|
262
|
-
if (minutes > 0 || hours > 0 && seconds > 0) {
|
|
263
|
-
timeString += `${minutes}M`;
|
|
264
|
-
}
|
|
265
|
-
if (seconds > 0 || timeString === "PT") {
|
|
266
|
-
timeString += `${seconds}S`;
|
|
267
|
-
}
|
|
268
|
-
return timeString === "PT" ? "PT0S" : timeString;
|
|
269
|
-
} else {
|
|
270
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
271
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
272
|
-
const secondsOnly = Math.floor(totalSeconds % 60);
|
|
273
|
-
const centiseconds = Math.floor((totalSeconds - Math.floor(totalSeconds)) * 100);
|
|
274
|
-
return `${pad(hours, 4)}:${pad(minutes)}:${pad(secondsOnly)}.${pad(centiseconds)}`;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// src/services/QuizEngine.ts
|
|
280
|
-
var QuizEngine = class {
|
|
281
|
-
constructor(options) {
|
|
282
|
-
this.userAnswers = /* @__PURE__ */ new Map();
|
|
283
|
-
this.currentQuestionIndex = 0;
|
|
284
|
-
this.timerId = null;
|
|
285
|
-
this.timeLeftInSeconds = null;
|
|
286
|
-
this.scormService = null;
|
|
287
|
-
this.quizResultState = { scormStatus: "idle" };
|
|
288
|
-
this.questionStartTime = null;
|
|
289
|
-
this.questionTimings = /* @__PURE__ */ new Map();
|
|
290
|
-
var _a, _b, _c, _d, _e;
|
|
291
|
-
this.config = options.config;
|
|
292
|
-
this.callbacks = options.callbacks || {};
|
|
293
|
-
this.questions = ((_a = this.config.settings) == null ? void 0 : _a.shuffleQuestions) ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
|
|
294
|
-
this.overallStartTime = Date.now();
|
|
295
|
-
if (((_b = this.config.settings) == null ? void 0 : _b.timeLimitMinutes) && this.config.settings.timeLimitMinutes > 0) {
|
|
296
|
-
this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
|
|
297
|
-
}
|
|
298
|
-
if ((_c = this.config.settings) == null ? void 0 : _c.scorm) {
|
|
299
|
-
this.quizResultState.scormStatus = "initializing";
|
|
300
|
-
this.scormService = new SCORMService(this.config.settings.scorm);
|
|
301
|
-
if (this.scormService.hasAPI()) {
|
|
302
|
-
const initResult = this.scormService.initialize();
|
|
303
|
-
if (initResult.success) {
|
|
304
|
-
this.quizResultState.scormStatus = "initialized";
|
|
305
|
-
this.quizResultState.studentName = initResult.studentName;
|
|
306
|
-
} else {
|
|
307
|
-
this.quizResultState.scormStatus = "error";
|
|
308
|
-
this.quizResultState.scormError = initResult.error || "SCORM initialization failed.";
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
this.quizResultState.scormStatus = "no_api";
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const initialQ = this.getCurrentQuestion();
|
|
315
|
-
if (initialQ) {
|
|
316
|
-
this.questionStartTime = Date.now();
|
|
317
|
-
}
|
|
318
|
-
if (this.callbacks.onQuizStart) {
|
|
319
|
-
this.callbacks.onQuizStart({
|
|
320
|
-
initialQuestion: initialQ,
|
|
321
|
-
currentQuestionNumber: this.getCurrentQuestionNumber(),
|
|
322
|
-
totalQuestions: this.getTotalQuestions(),
|
|
323
|
-
timeLimitInSeconds: this.timeLeftInSeconds,
|
|
324
|
-
scormStatus: this.quizResultState.scormStatus,
|
|
325
|
-
studentName: this.quizResultState.studentName
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
if (this.timeLeftInSeconds !== null) {
|
|
329
|
-
this.startTimer();
|
|
330
|
-
}
|
|
331
|
-
(_e = (_d = this.callbacks).onQuestionChange) == null ? void 0 : _e.call(_d, initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
|
|
332
|
-
}
|
|
333
|
-
_recordCurrentQuestionTime() {
|
|
334
|
-
if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
|
|
335
|
-
const currentQId = this.questions[this.currentQuestionIndex].id;
|
|
336
|
-
const elapsedMs = Date.now() - this.questionStartTime;
|
|
337
|
-
const currentTotalTime = this.questionTimings.get(currentQId) || 0;
|
|
338
|
-
this.questionTimings.set(currentQId, currentTotalTime + elapsedMs / 1e3);
|
|
339
|
-
}
|
|
340
|
-
this.questionStartTime = null;
|
|
341
|
-
}
|
|
342
|
-
startTimer() {
|
|
343
|
-
if (this.timerId !== null) clearInterval(this.timerId);
|
|
344
|
-
this.timerId = setInterval(() => this.handleTick(), 1e3);
|
|
345
|
-
}
|
|
346
|
-
stopTimer() {
|
|
347
|
-
if (this.timerId !== null) {
|
|
348
|
-
clearInterval(this.timerId);
|
|
349
|
-
this.timerId = null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
handleTick() {
|
|
353
|
-
var _a, _b, _c, _d;
|
|
354
|
-
if (this.timeLeftInSeconds === null) return;
|
|
355
|
-
if (this.timeLeftInSeconds > 0) {
|
|
356
|
-
this.timeLeftInSeconds--;
|
|
357
|
-
(_b = (_a = this.callbacks).onTimeTick) == null ? void 0 : _b.call(_a, this.timeLeftInSeconds);
|
|
358
|
-
}
|
|
359
|
-
if (this.timeLeftInSeconds <= 0) {
|
|
360
|
-
this.stopTimer();
|
|
361
|
-
(_d = (_c = this.callbacks).onQuizTimeUp) == null ? void 0 : _d.call(_c);
|
|
362
|
-
this.calculateResults();
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
getTimeLeftInSeconds() {
|
|
366
|
-
return this.timeLeftInSeconds;
|
|
367
|
-
}
|
|
368
|
-
getCurrentQuestion() {
|
|
369
|
-
return this.questions[this.currentQuestionIndex] || null;
|
|
370
|
-
}
|
|
371
|
-
getCurrentQuestionNumber() {
|
|
372
|
-
return this.currentQuestionIndex + 1;
|
|
373
|
-
}
|
|
374
|
-
getTotalQuestions() {
|
|
375
|
-
return this.questions.length;
|
|
376
|
-
}
|
|
377
|
-
getUserAnswer(questionId) {
|
|
378
|
-
return this.userAnswers.get(questionId);
|
|
379
|
-
}
|
|
380
|
-
isQuizFinished() {
|
|
381
|
-
return this.quizResultState.score !== void 0;
|
|
382
|
-
}
|
|
383
|
-
submitAnswer(questionId, answer) {
|
|
384
|
-
var _a, _b;
|
|
385
|
-
this.userAnswers.set(questionId, answer);
|
|
386
|
-
const question = this.questions.find((q) => q.id === questionId);
|
|
387
|
-
if (question) (_b = (_a = this.callbacks).onAnswerSubmit) == null ? void 0 : _b.call(_a, question, answer);
|
|
388
|
-
}
|
|
389
|
-
nextQuestion() {
|
|
390
|
-
var _a, _b;
|
|
391
|
-
this._recordCurrentQuestionTime();
|
|
392
|
-
if (this.currentQuestionIndex < this.questions.length - 1) {
|
|
393
|
-
this.currentQuestionIndex++;
|
|
394
|
-
const currentQ = this.getCurrentQuestion();
|
|
395
|
-
this.questionStartTime = Date.now();
|
|
396
|
-
(_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
|
|
397
|
-
return currentQ;
|
|
398
|
-
}
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
previousQuestion() {
|
|
402
|
-
var _a, _b;
|
|
403
|
-
this._recordCurrentQuestionTime();
|
|
404
|
-
if (this.currentQuestionIndex > 0) {
|
|
405
|
-
this.currentQuestionIndex--;
|
|
406
|
-
const currentQ = this.getCurrentQuestion();
|
|
407
|
-
this.questionStartTime = Date.now();
|
|
408
|
-
(_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
|
|
409
|
-
return currentQ;
|
|
410
|
-
}
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
goToQuestion(index) {
|
|
414
|
-
var _a, _b;
|
|
415
|
-
if (index >= 0 && index < this.questions.length && index !== this.currentQuestionIndex) {
|
|
416
|
-
this._recordCurrentQuestionTime();
|
|
417
|
-
this.currentQuestionIndex = index;
|
|
418
|
-
const currentQ = this.getCurrentQuestion();
|
|
419
|
-
this.questionStartTime = Date.now();
|
|
420
|
-
(_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
|
|
421
|
-
return currentQ;
|
|
422
|
-
}
|
|
423
|
-
return this.getCurrentQuestion();
|
|
424
|
-
}
|
|
425
|
-
evaluateQuestion(question, answer) {
|
|
426
|
-
var _a, _b, _c;
|
|
427
|
-
let isCorrect = false;
|
|
428
|
-
let correctAnswerDetail = null;
|
|
429
|
-
const points = (_a = question.points) != null ? _a : 0;
|
|
430
|
-
const findOptionText = (q, id) => {
|
|
431
|
-
var _a2;
|
|
432
|
-
return ((_a2 = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
|
|
433
|
-
};
|
|
434
|
-
switch (question.questionType) {
|
|
435
|
-
case "multiple_choice": {
|
|
436
|
-
const q = question;
|
|
437
|
-
const correctAnswerId = q.correctAnswerId;
|
|
438
|
-
const correctValue = findOptionText(q, correctAnswerId);
|
|
439
|
-
correctAnswerDetail = { id: correctAnswerId, value: correctValue };
|
|
440
|
-
isCorrect = answer === correctAnswerId;
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
case "multiple_response": {
|
|
444
|
-
const q = question;
|
|
445
|
-
const correctAnswerIds = q.correctAnswerIds;
|
|
446
|
-
const correctValues = correctAnswerIds.map((id) => findOptionText(q, id));
|
|
447
|
-
correctAnswerDetail = { id: correctAnswerIds, value: correctValues };
|
|
448
|
-
if (Array.isArray(answer)) {
|
|
449
|
-
const userAnswerSet = new Set(answer);
|
|
450
|
-
const correctAnswerSet = new Set(correctAnswerIds);
|
|
451
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
452
|
-
}
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
case "true_false": {
|
|
456
|
-
const q = question;
|
|
457
|
-
correctAnswerDetail = { id: null, value: q.correctAnswer };
|
|
458
|
-
let tfAnswer = answer;
|
|
459
|
-
if (typeof answer === "string") tfAnswer = answer.toLowerCase() === "true";
|
|
460
|
-
isCorrect = typeof tfAnswer === "boolean" && tfAnswer === q.correctAnswer;
|
|
461
|
-
break;
|
|
462
|
-
}
|
|
463
|
-
case "short_answer": {
|
|
464
|
-
const q = question;
|
|
465
|
-
correctAnswerDetail = { id: null, value: q.acceptedAnswers };
|
|
466
|
-
if (typeof answer === "string") {
|
|
467
|
-
const userAnswerTrimmed = answer.trim();
|
|
468
|
-
const caseSensitive = (_b = q.isCaseSensitive) != null ? _b : false;
|
|
469
|
-
isCorrect = q.acceptedAnswers.some((accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase());
|
|
470
|
-
}
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
case "numeric": {
|
|
474
|
-
const q = question;
|
|
475
|
-
correctAnswerDetail = { id: null, value: q.answer };
|
|
476
|
-
if (typeof answer === "string" || typeof answer === "number") {
|
|
477
|
-
const userAnswerNum = parseFloat(String(answer));
|
|
478
|
-
if (!isNaN(userAnswerNum)) {
|
|
479
|
-
isCorrect = q.tolerance != null ? Math.abs(userAnswerNum - q.answer) <= q.tolerance : userAnswerNum === q.answer;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
case "sequence": {
|
|
485
|
-
const q = question;
|
|
486
|
-
const correctValues = q.correctOrder.map((id) => {
|
|
487
|
-
var _a2;
|
|
488
|
-
return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
|
|
489
|
-
});
|
|
490
|
-
correctAnswerDetail = { id: q.correctOrder, value: correctValues };
|
|
491
|
-
if (Array.isArray(answer) && answer.length === q.correctOrder.length) {
|
|
492
|
-
isCorrect = answer.every((itemId, index) => itemId === q.correctOrder[index]);
|
|
493
|
-
}
|
|
494
|
-
break;
|
|
495
|
-
}
|
|
496
|
-
case "matching": {
|
|
497
|
-
const q = question;
|
|
498
|
-
const correctMap = q.correctAnswerMap.reduce((acc, curr) => {
|
|
499
|
-
var _a2, _b2;
|
|
500
|
-
const promptText = ((_a2 = q.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
|
|
501
|
-
const optionText = ((_b2 = q.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b2.content) || "";
|
|
502
|
-
acc[promptText] = optionText;
|
|
503
|
-
return acc;
|
|
504
|
-
}, {});
|
|
505
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
506
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
507
|
-
const userAnswerMap = answer;
|
|
508
|
-
isCorrect = q.correctAnswerMap.length === Object.keys(userAnswerMap).length && q.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
|
|
509
|
-
}
|
|
510
|
-
break;
|
|
511
|
-
}
|
|
512
|
-
case "fill_in_the_blanks": {
|
|
513
|
-
const q = question;
|
|
514
|
-
const correctMap = q.answers.reduce((acc, curr) => {
|
|
515
|
-
acc[curr.blankId] = curr.acceptedValues.join(" | ");
|
|
516
|
-
return acc;
|
|
517
|
-
}, {});
|
|
518
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
519
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
520
|
-
const userAnswerMap = answer;
|
|
521
|
-
isCorrect = q.answers.every((correctAnsDef) => {
|
|
522
|
-
var _a2, _b2;
|
|
523
|
-
const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
|
|
524
|
-
if (userValForBlank === void 0) return false;
|
|
525
|
-
const caseSensitive = (_b2 = q.isCaseSensitive) != null ? _b2 : false;
|
|
526
|
-
return correctAnsDef.acceptedValues.some((accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase());
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
case "drag_and_drop": {
|
|
532
|
-
const q = question;
|
|
533
|
-
const correctMap = q.answerMap.reduce((acc, curr) => {
|
|
534
|
-
var _a2, _b2;
|
|
535
|
-
const draggableText = ((_a2 = q.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
|
|
536
|
-
const dropZoneText = ((_b2 = q.dropZones.find((z13) => z13.id === curr.dropZoneId)) == null ? void 0 : _b2.label) || "";
|
|
537
|
-
acc[draggableText] = dropZoneText;
|
|
538
|
-
return acc;
|
|
539
|
-
}, {});
|
|
540
|
-
correctAnswerDetail = { id: null, value: correctMap };
|
|
541
|
-
if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
|
|
542
|
-
const userAnswerMap = answer;
|
|
543
|
-
isCorrect = q.answerMap.length === Object.keys(userAnswerMap).length && q.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
|
|
544
|
-
}
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
case "hotspot": {
|
|
548
|
-
const q = question;
|
|
549
|
-
const correctValues = q.correctHotspotIds.map((id) => {
|
|
550
|
-
var _a2;
|
|
551
|
-
return ((_a2 = q.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
|
|
552
|
-
});
|
|
553
|
-
correctAnswerDetail = { id: q.correctHotspotIds, value: correctValues };
|
|
554
|
-
if (Array.isArray(answer)) {
|
|
555
|
-
const userAnswerSet = new Set(answer);
|
|
556
|
-
const correctAnswerSet = new Set(q.correctHotspotIds);
|
|
557
|
-
isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
|
|
558
|
-
}
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
case "blockly_programming":
|
|
562
|
-
case "scratch_programming": {
|
|
563
|
-
const q = question;
|
|
564
|
-
correctAnswerDetail = { id: null, value: q.solutionGeneratedCode || "" };
|
|
565
|
-
if (typeof answer === "string" && typeof q.solutionGeneratedCode === "string") {
|
|
566
|
-
if (typeof window !== "undefined" && ((_c = window.Blockly) == null ? void 0 : _c.JavaScript)) {
|
|
567
|
-
const LocalBlockly = window.Blockly;
|
|
568
|
-
let generatedUserCode = "";
|
|
569
|
-
try {
|
|
570
|
-
const tempWorkspace = new LocalBlockly.Workspace();
|
|
571
|
-
const dom = LocalBlockly.Xml.textToDom(answer);
|
|
572
|
-
LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
|
|
573
|
-
generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
|
|
574
|
-
const normalize = (code) => code.replace(/\s+/g, " ").trim();
|
|
575
|
-
isCorrect = normalize(generatedUserCode) === normalize(q.solutionGeneratedCode);
|
|
576
|
-
tempWorkspace.dispose();
|
|
577
|
-
} catch (e) {
|
|
578
|
-
console.error(`Error generating code from user's ${q.questionType} XML for evaluation:`, e);
|
|
579
|
-
isCorrect = false;
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
console.warn(`Blockly library not available in QuizEngine for ${q.questionType} code generation during evaluation. Skipping code comparison.`);
|
|
583
|
-
isCorrect = false;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
default: {
|
|
589
|
-
const _exhaustiveCheck = question;
|
|
590
|
-
console.warn("Unsupported question type in QuizEngine evaluation:", _exhaustiveCheck);
|
|
591
|
-
isCorrect = false;
|
|
592
|
-
correctAnswerDetail = { id: null, value: "Evaluation not implemented." };
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
return { isCorrect, correctAnswer: correctAnswerDetail, pointsEarned: isCorrect ? points : 0 };
|
|
596
|
-
}
|
|
597
|
-
async calculateResults() {
|
|
598
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
599
|
-
this.stopTimer();
|
|
600
|
-
this._recordCurrentQuestionTime();
|
|
601
|
-
let totalScore = 0;
|
|
602
|
-
let maxScore = 0;
|
|
603
|
-
const questionResultsArray = [];
|
|
604
|
-
let accumulatedTotalTimeSpent = 0;
|
|
605
|
-
for (const question of this.questions) {
|
|
606
|
-
const userAnswerRaw = this.userAnswers.get(question.id) || null;
|
|
607
|
-
maxScore += (_a = question.points) != null ? _a : 0;
|
|
608
|
-
const { isCorrect, correctAnswer: correctAnswerDetail, pointsEarned } = this.evaluateQuestion(question, userAnswerRaw);
|
|
609
|
-
totalScore += pointsEarned;
|
|
610
|
-
const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
|
|
611
|
-
accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
|
|
612
|
-
let userAnswerDetail = null;
|
|
613
|
-
let allOptions = void 0;
|
|
614
|
-
if (userAnswerRaw !== null) {
|
|
615
|
-
switch (question.questionType) {
|
|
616
|
-
case "multiple_choice": {
|
|
617
|
-
const q = question;
|
|
618
|
-
allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
|
|
619
|
-
const id = userAnswerRaw;
|
|
620
|
-
userAnswerDetail = { id, value: ((_b = allOptions.find((opt) => opt.id === id)) == null ? void 0 : _b.value) || "" };
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
case "multiple_response": {
|
|
624
|
-
const q = question;
|
|
625
|
-
allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
|
|
626
|
-
const ids = userAnswerRaw;
|
|
627
|
-
const values = ids.map((id) => {
|
|
628
|
-
var _a2;
|
|
629
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
630
|
-
});
|
|
631
|
-
userAnswerDetail = { id: ids, value: values };
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
case "true_false":
|
|
635
|
-
case "short_answer":
|
|
636
|
-
case "numeric":
|
|
637
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
638
|
-
break;
|
|
639
|
-
case "sequence": {
|
|
640
|
-
const q = question;
|
|
641
|
-
allOptions = q.items.map((item) => ({ id: item.id, value: item.content }));
|
|
642
|
-
const ids = userAnswerRaw;
|
|
643
|
-
const values = ids.map((id) => {
|
|
644
|
-
var _a2;
|
|
645
|
-
return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
|
|
646
|
-
});
|
|
647
|
-
userAnswerDetail = { id: ids, value: values };
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
case "matching": {
|
|
651
|
-
const q = question;
|
|
652
|
-
const userAnswerMap = userAnswerRaw;
|
|
653
|
-
const valueMap = {};
|
|
654
|
-
for (const promptId in userAnswerMap) {
|
|
655
|
-
const optionId = userAnswerMap[promptId];
|
|
656
|
-
const promptText = ((_c = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _c.content) || "";
|
|
657
|
-
const optionText = ((_d = q.options.find((o) => o.id === optionId)) == null ? void 0 : _d.content) || "";
|
|
658
|
-
valueMap[promptText] = optionText;
|
|
659
|
-
}
|
|
660
|
-
userAnswerDetail = { id: null, value: valueMap };
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
// --- LOGIC MỚI ĐƯỢC THÊM VÀO ---
|
|
664
|
-
case "fill_in_the_blanks": {
|
|
665
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
666
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
667
|
-
}
|
|
668
|
-
break;
|
|
669
|
-
}
|
|
670
|
-
case "drag_and_drop": {
|
|
671
|
-
const q = question;
|
|
672
|
-
if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
|
|
673
|
-
const userAnswerMapByIds = userAnswerRaw;
|
|
674
|
-
const enrichedUserAnswerMap = {};
|
|
675
|
-
for (const draggableId in userAnswerMapByIds) {
|
|
676
|
-
const dropZoneId = userAnswerMapByIds[draggableId];
|
|
677
|
-
const draggableText = ((_e = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _e.content) || `(ID: ${draggableId})`;
|
|
678
|
-
const dropZoneText = ((_f = q.dropZones.find((z13) => z13.id === dropZoneId)) == null ? void 0 : _f.label) || `(ID: ${dropZoneId})`;
|
|
679
|
-
enrichedUserAnswerMap[draggableText] = dropZoneText;
|
|
680
|
-
}
|
|
681
|
-
userAnswerDetail = { id: null, value: enrichedUserAnswerMap };
|
|
682
|
-
}
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
685
|
-
// ------------------------------------
|
|
686
|
-
// Các loại câu hỏi còn lại vẫn giữ fallback
|
|
687
|
-
case "hotspot":
|
|
688
|
-
case "blockly_programming":
|
|
689
|
-
case "scratch_programming":
|
|
690
|
-
userAnswerDetail = { id: null, value: userAnswerRaw };
|
|
691
|
-
break;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
questionResultsArray.push({
|
|
695
|
-
questionId: question.id,
|
|
696
|
-
questionType: question.questionType,
|
|
697
|
-
prompt: question.prompt,
|
|
698
|
-
isCorrect,
|
|
699
|
-
pointsEarned,
|
|
700
|
-
userAnswer: userAnswerDetail,
|
|
701
|
-
correctAnswer: correctAnswerDetail,
|
|
702
|
-
allOptions,
|
|
703
|
-
timeSpentSeconds: timeSpentOnThisQuestion
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
|
|
707
|
-
let passed = void 0;
|
|
708
|
-
if (((_g = this.config.settings) == null ? void 0 : _g.passingScorePercent) != null) {
|
|
709
|
-
passed = percentage >= this.config.settings.passingScorePercent;
|
|
710
|
-
}
|
|
711
|
-
const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
|
|
712
|
-
const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
|
|
713
|
-
const metadataPerformance = this._calculateMetadataPerformance();
|
|
714
|
-
const finalResults = __spreadValues({
|
|
715
|
-
score: totalScore,
|
|
716
|
-
maxScore,
|
|
717
|
-
percentage,
|
|
718
|
-
answers: this.userAnswers,
|
|
719
|
-
questionResults: questionResultsArray,
|
|
720
|
-
passed,
|
|
721
|
-
webhookStatus: "idle",
|
|
722
|
-
scormStatus: this.quizResultState.scormStatus || "idle",
|
|
723
|
-
scormError: this.quizResultState.scormError,
|
|
724
|
-
studentName: this.quizResultState.studentName,
|
|
725
|
-
totalTimeSpentSeconds: totalQuizTimeSpentSeconds,
|
|
726
|
-
averageTimePerQuestionSeconds
|
|
727
|
-
}, metadataPerformance);
|
|
728
|
-
this.quizResultState = __spreadValues(__spreadValues({}, this.quizResultState), finalResults);
|
|
729
|
-
if ((_h = this.config.settings) == null ? void 0 : _h.scorm) this._sendResultsToSCORM(finalResults);
|
|
730
|
-
await this._sendResultsToWebhook(finalResults);
|
|
731
|
-
(_j = (_i = this.callbacks).onQuizFinish) == null ? void 0 : _j.call(_i, finalResults);
|
|
732
|
-
return finalResults;
|
|
733
|
-
}
|
|
734
|
-
async _sendResultsToWebhook(results) {
|
|
735
|
-
var _a;
|
|
736
|
-
if (!((_a = this.config.settings) == null ? void 0 : _a.webhookUrl)) {
|
|
737
|
-
results.webhookStatus = "idle";
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
results.webhookStatus = "sending";
|
|
741
|
-
try {
|
|
742
|
-
const response = await fetch(this.config.settings.webhookUrl, {
|
|
743
|
-
method: "POST",
|
|
744
|
-
headers: { "Content-Type": "application/json" },
|
|
745
|
-
body: JSON.stringify(results)
|
|
746
|
-
});
|
|
747
|
-
if (response.ok) {
|
|
748
|
-
results.webhookStatus = "success";
|
|
749
|
-
} else {
|
|
750
|
-
results.webhookStatus = "error";
|
|
751
|
-
results.webhookError = `Webhook returned status: ${response.status} ${response.statusText}`;
|
|
752
|
-
try {
|
|
753
|
-
const errorBody = await response.text();
|
|
754
|
-
results.webhookError += ` - Body: ${errorBody.substring(0, 200)}`;
|
|
755
|
-
} catch (e) {
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
} catch (error) {
|
|
759
|
-
results.webhookStatus = "error";
|
|
760
|
-
results.webhookError = error instanceof Error ? `Fetch error: ${error.message}` : "Unknown webhook error.";
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
_sendResultsToSCORM(results) {
|
|
764
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
765
|
-
if (!this.scormService || !this.scormService.hasAPI() || this.quizResultState.scormStatus === "no_api") {
|
|
766
|
-
results.scormStatus = this.quizResultState.scormStatus || "idle";
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
if (this.quizResultState.scormStatus === "error" && ((_a = this.quizResultState.scormError) == null ? void 0 : _a.includes("initialization failed"))) {
|
|
770
|
-
results.scormStatus = "error";
|
|
771
|
-
results.scormError = this.quizResultState.scormError;
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
results.scormStatus = "sending_data";
|
|
775
|
-
try {
|
|
776
|
-
this.scormService.setScore(results.score, results.maxScore, 0);
|
|
777
|
-
let lessonStatusSetting = "completed";
|
|
778
|
-
if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) !== void 0 && ((_c = this.config.settings) == null ? void 0 : _c.passingScorePercent) !== null) {
|
|
779
|
-
lessonStatusSetting = results.passed ? "passed" : "failed";
|
|
780
|
-
} else if ((_e = (_d = this.config.settings) == null ? void 0 : _d.scorm) == null ? void 0 : _e.setCompletionOnFinish) {
|
|
781
|
-
lessonStatusSetting = "completed";
|
|
782
|
-
}
|
|
783
|
-
this.scormService.setLessonStatus(lessonStatusSetting, results.passed);
|
|
784
|
-
if (results.totalTimeSpentSeconds !== void 0 && this.scormService.formatCMITime) {
|
|
785
|
-
const cmiTime = this.scormService.formatCMITime(results.totalTimeSpentSeconds);
|
|
786
|
-
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");
|
|
787
|
-
if (sessionTimeVar) this.scormService.setValue(sessionTimeVar, cmiTime);
|
|
788
|
-
}
|
|
789
|
-
const commitResult = this.scormService.commit();
|
|
790
|
-
if (commitResult.success) {
|
|
791
|
-
results.scormStatus = "committed";
|
|
792
|
-
} else {
|
|
793
|
-
results.scormStatus = "error";
|
|
794
|
-
results.scormError = commitResult.error || "SCORM commit failed.";
|
|
795
|
-
}
|
|
796
|
-
} catch (e) {
|
|
797
|
-
results.scormStatus = "error";
|
|
798
|
-
results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
_calculateMetadataPerformance() {
|
|
802
|
-
const loPerformanceMap = /* @__PURE__ */ new Map();
|
|
803
|
-
const categoryPerformanceMap = /* @__PURE__ */ new Map();
|
|
804
|
-
const topicPerformanceMap = /* @__PURE__ */ new Map();
|
|
805
|
-
const difficultyPerformanceMap = /* @__PURE__ */ new Map();
|
|
806
|
-
const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
|
|
807
|
-
const updateMap = (map, key, points, isCorrect) => {
|
|
808
|
-
if (!key) return;
|
|
809
|
-
const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
|
|
810
|
-
current.totalQuestions++;
|
|
811
|
-
current.maxPoints += points;
|
|
812
|
-
if (isCorrect) {
|
|
813
|
-
current.correctQuestions++;
|
|
814
|
-
current.pointsEarned += points;
|
|
815
|
-
}
|
|
816
|
-
map.set(key, current);
|
|
817
|
-
};
|
|
818
|
-
this.questions.forEach((q) => {
|
|
819
|
-
var _a;
|
|
820
|
-
const qResult = this.userAnswers.get(q.id);
|
|
821
|
-
const { isCorrect } = this.evaluateQuestion(q, qResult || null);
|
|
822
|
-
const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
|
|
823
|
-
updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
|
|
824
|
-
updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
|
|
825
|
-
updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
|
|
826
|
-
updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
|
|
827
|
-
updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
|
|
828
|
-
});
|
|
829
|
-
const formatPerformanceArray = (map, keyName) => {
|
|
830
|
-
return Array.from(map.entries()).map(([key, data]) => ({
|
|
831
|
-
[keyName]: key,
|
|
832
|
-
totalQuestions: data.totalQuestions,
|
|
833
|
-
correctQuestions: data.correctQuestions,
|
|
834
|
-
pointsEarned: data.pointsEarned,
|
|
835
|
-
maxPoints: data.maxPoints,
|
|
836
|
-
percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
|
|
837
|
-
}));
|
|
838
|
-
};
|
|
839
|
-
return {
|
|
840
|
-
performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
|
|
841
|
-
performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
|
|
842
|
-
performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
|
|
843
|
-
performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
|
|
844
|
-
performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
getElapsedTime() {
|
|
848
|
-
return Date.now() - this.overallStartTime;
|
|
849
|
-
}
|
|
850
|
-
destroy() {
|
|
851
|
-
this.stopTimer();
|
|
852
|
-
this._recordCurrentQuestionTime();
|
|
853
|
-
if (this.scormService && this.scormService.hasAPI()) {
|
|
854
|
-
if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
|
|
855
|
-
const termResult = this.scormService.terminate();
|
|
856
|
-
if (termResult.success) {
|
|
857
|
-
this.quizResultState.scormStatus = "terminated";
|
|
858
|
-
} else {
|
|
859
|
-
this.quizResultState.scormStatus = "error";
|
|
860
|
-
this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
this.scormService = null;
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
// src/utils/idGenerators.ts
|
|
869
|
-
function generateUniqueId(prefix = "id_") {
|
|
870
|
-
return prefix + Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// src/services/QuizEditorService.ts
|
|
874
|
-
var QuizEditorService = class {
|
|
875
|
-
constructor(initialQuiz) {
|
|
876
|
-
this.quiz = JSON.parse(JSON.stringify(initialQuiz));
|
|
877
|
-
}
|
|
878
|
-
/**
|
|
879
|
-
* Returns the current state of the quiz configuration.
|
|
880
|
-
* @returns The current QuizConfig object.
|
|
881
|
-
*/
|
|
882
|
-
getQuiz() {
|
|
883
|
-
return this.quiz;
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Creates a new, "empty" question object based on the specified question type.
|
|
887
|
-
* @param type The type of question to create.
|
|
888
|
-
* @returns A new QuizQuestion object with default values.
|
|
889
|
-
*/
|
|
890
|
-
static createNewQuestionTemplate(type) {
|
|
891
|
-
const baseNewQuestion = {
|
|
892
|
-
id: generateUniqueId(`new_${type}_`),
|
|
893
|
-
// 'new_' prefix indicates it's a new, unsaved question
|
|
894
|
-
questionType: type,
|
|
895
|
-
prompt: "",
|
|
896
|
-
points: 10,
|
|
897
|
-
difficulty: "medium"
|
|
898
|
-
};
|
|
899
|
-
switch (type) {
|
|
900
|
-
case "true_false":
|
|
901
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "true_false", correctAnswer: true });
|
|
902
|
-
case "multiple_choice":
|
|
903
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_choice", options: [], correctAnswerId: "" });
|
|
904
|
-
case "multiple_response":
|
|
905
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_response", options: [], correctAnswerIds: [] });
|
|
906
|
-
case "short_answer":
|
|
907
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "short_answer", acceptedAnswers: [""], isCaseSensitive: false });
|
|
908
|
-
case "numeric":
|
|
909
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "numeric", answer: 0 });
|
|
910
|
-
case "fill_in_the_blanks": {
|
|
911
|
-
const blankId = generateUniqueId("blank_");
|
|
912
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), {
|
|
913
|
-
questionType: "fill_in_the_blanks",
|
|
914
|
-
segments: [
|
|
915
|
-
{ type: "text", content: "Your text before " },
|
|
916
|
-
{ type: "blank", id: blankId },
|
|
917
|
-
{ type: "text", content: " and after." }
|
|
918
|
-
],
|
|
919
|
-
answers: [{ blankId, acceptedValues: [""] }],
|
|
920
|
-
isCaseSensitive: false
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
case "sequence":
|
|
924
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "sequence", items: [], correctOrder: [] });
|
|
925
|
-
case "matching":
|
|
926
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "matching", prompts: [], options: [], correctAnswerMap: [], shuffleOptions: true });
|
|
927
|
-
case "drag_and_drop":
|
|
928
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "drag_and_drop", draggableItems: [], dropZones: [], answerMap: [] });
|
|
929
|
-
case "hotspot":
|
|
930
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "hotspot", imageUrl: "", hotspots: [], correctHotspotIds: [] });
|
|
931
|
-
case "blockly_programming":
|
|
932
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), {
|
|
933
|
-
questionType: "blockly_programming",
|
|
934
|
-
toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
|
|
935
|
-
initialWorkspace: "",
|
|
936
|
-
solutionWorkspaceXML: "",
|
|
937
|
-
solutionGeneratedCode: ""
|
|
938
|
-
});
|
|
939
|
-
case "scratch_programming":
|
|
940
|
-
return __spreadProps(__spreadValues({}, baseNewQuestion), {
|
|
941
|
-
questionType: "scratch_programming",
|
|
942
|
-
toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
|
|
943
|
-
initialWorkspace: "",
|
|
944
|
-
solutionWorkspaceXML: "",
|
|
945
|
-
solutionGeneratedCode: ""
|
|
946
|
-
});
|
|
947
|
-
default:
|
|
948
|
-
const _exhaustiveCheck = type;
|
|
949
|
-
throw new Error(`Question type "${_exhaustiveCheck}" is not supported for creation.`);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
/**
|
|
953
|
-
* Adds a new question to the quiz. If the question ID is temporary, a new permanent ID is generated.
|
|
954
|
-
* @param question The question object to add.
|
|
955
|
-
* @returns The updated QuizConfig.
|
|
956
|
-
*/
|
|
957
|
-
addQuestion(question) {
|
|
958
|
-
const newQuestion = __spreadValues({}, question);
|
|
959
|
-
if (newQuestion.id.startsWith("new_")) {
|
|
960
|
-
newQuestion.id = generateUniqueId(`${newQuestion.questionType}_`);
|
|
961
|
-
}
|
|
962
|
-
this.quiz.questions.push(newQuestion);
|
|
963
|
-
return this.quiz;
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Updates an existing question in the quiz.
|
|
967
|
-
* @param updatedQuestion The full question object with changes.
|
|
968
|
-
* @returns The updated QuizConfig.
|
|
969
|
-
*/
|
|
970
|
-
updateQuestion(updatedQuestion) {
|
|
971
|
-
const questionIndex = this.quiz.questions.findIndex((q) => q.id === updatedQuestion.id);
|
|
972
|
-
if (questionIndex === -1) {
|
|
973
|
-
throw new Error(`Question with ID "${updatedQuestion.id}" not found.`);
|
|
974
|
-
}
|
|
975
|
-
this.quiz.questions[questionIndex] = updatedQuestion;
|
|
976
|
-
return this.quiz;
|
|
977
|
-
}
|
|
978
|
-
/**
|
|
979
|
-
* Deletes a question from the quiz by its index.
|
|
980
|
-
* @param index The index of the question to delete.
|
|
981
|
-
* @returns The updated QuizConfig.
|
|
982
|
-
*/
|
|
983
|
-
deleteQuestionByIndex(index) {
|
|
984
|
-
if (index < 0 || index >= this.quiz.questions.length) {
|
|
985
|
-
throw new Error(`Invalid index ${index} for question deletion.`);
|
|
986
|
-
}
|
|
987
|
-
this.quiz.questions.splice(index, 1);
|
|
988
|
-
return this.quiz;
|
|
989
|
-
}
|
|
990
|
-
/**
|
|
991
|
-
* Moves a question from one position to another.
|
|
992
|
-
* @param fromIndex The current index of the question.
|
|
993
|
-
* @param toIndex The target index for the question.
|
|
994
|
-
* @returns The updated QuizConfig.
|
|
995
|
-
*/
|
|
996
|
-
moveQuestion(fromIndex, toIndex) {
|
|
997
|
-
if (fromIndex < 0 || fromIndex >= this.quiz.questions.length || toIndex < 0 || toIndex >= this.quiz.questions.length) {
|
|
998
|
-
throw new Error("Invalid index for moving question.");
|
|
999
|
-
}
|
|
1000
|
-
const [movedItem] = this.quiz.questions.splice(fromIndex, 1);
|
|
1001
|
-
this.quiz.questions.splice(toIndex, 0, movedItem);
|
|
1002
|
-
return this.quiz;
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1005
|
-
var QuestionOptionSchema = zod.z.object({
|
|
1006
|
-
id: zod.z.string().describe("Unique ID for the option."),
|
|
1007
|
-
text: zod.z.string().describe("Text content of the option.")
|
|
1008
|
-
});
|
|
1009
|
-
var MultipleChoiceQuestionSchema = zod.z.object({
|
|
1010
|
-
// Các trường bắt buộc (required)
|
|
1011
|
-
id: zod.z.string().describe("Unique identifier."),
|
|
1012
|
-
questionType: zod.z.literal("multiple_choice"),
|
|
1013
|
-
// Tương đương với "const" trong JSON Schema
|
|
1014
|
-
prompt: zod.z.string().describe("Question statement."),
|
|
1015
|
-
options: zod.z.array(QuestionOptionSchema).min(1).describe("Array of answer choices."),
|
|
1016
|
-
correctAnswerId: zod.z.string().describe("ID of the correct option."),
|
|
1017
|
-
// Các trường tùy chọn (optional)
|
|
1018
|
-
points: zod.z.number().optional().describe("Points for correct answer."),
|
|
1019
|
-
explanation: zod.z.string().optional().describe("Explanation for the answer."),
|
|
1020
|
-
learningObjective: zod.z.string().optional(),
|
|
1021
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1022
|
-
bloomLevel: zod.z.string().optional(),
|
|
1023
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1024
|
-
// Có thể làm chặt chẽ hơn với z.enum
|
|
1025
|
-
contextCode: zod.z.string().optional(),
|
|
1026
|
-
gradeBand: zod.z.string().optional(),
|
|
1027
|
-
course: zod.z.string().optional(),
|
|
1028
|
-
category: zod.z.string().optional(),
|
|
1029
|
-
topic: zod.z.string().optional()
|
|
1030
|
-
});
|
|
1031
|
-
var exampleData = {
|
|
1032
|
-
id: "mcq-123",
|
|
1033
|
-
questionType: "multiple_choice",
|
|
1034
|
-
prompt: "What is 2 + 2?",
|
|
1035
|
-
options: [
|
|
1036
|
-
{ id: "opt-1", text: "3" },
|
|
1037
|
-
{ id: "opt-2", text: "4" }
|
|
1038
|
-
],
|
|
1039
|
-
correctAnswerId: "opt-2",
|
|
1040
|
-
points: 10
|
|
1041
|
-
};
|
|
1042
|
-
try {
|
|
1043
|
-
const validatedQuestion = MultipleChoiceQuestionSchema.parse(exampleData);
|
|
1044
|
-
console.log("Validation successful:", validatedQuestion);
|
|
1045
|
-
} catch (error) {
|
|
1046
|
-
console.error("Validation failed:", error);
|
|
1047
|
-
}
|
|
1048
|
-
var QuestionOptionSchema2 = zod.z.object({
|
|
1049
|
-
id: zod.z.string(),
|
|
1050
|
-
text: zod.z.string()
|
|
1051
|
-
});
|
|
1052
|
-
var MultipleResponseQuestionSchema = zod.z.object({
|
|
1053
|
-
// Các trường bắt buộc
|
|
1054
|
-
id: zod.z.string(),
|
|
1055
|
-
questionType: zod.z.literal("multiple_response"),
|
|
1056
|
-
prompt: zod.z.string(),
|
|
1057
|
-
options: zod.z.array(QuestionOptionSchema2).min(1),
|
|
1058
|
-
correctAnswerIds: zod.z.array(zod.z.string()).min(1).describe("Array of IDs of the correct options."),
|
|
1059
|
-
// Các trường tùy chọn
|
|
1060
|
-
points: zod.z.number().optional(),
|
|
1061
|
-
explanation: zod.z.string().optional(),
|
|
1062
|
-
learningObjective: zod.z.string().optional(),
|
|
1063
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1064
|
-
bloomLevel: zod.z.string().optional(),
|
|
1065
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1066
|
-
contextCode: zod.z.string().optional(),
|
|
1067
|
-
gradeBand: zod.z.string().optional(),
|
|
1068
|
-
course: zod.z.string().optional(),
|
|
1069
|
-
category: zod.z.string().optional(),
|
|
1070
|
-
topic: zod.z.string().optional()
|
|
1071
|
-
});
|
|
1072
|
-
var TextSegmentSchema = zod.z.object({
|
|
1073
|
-
type: zod.z.literal("text"),
|
|
1074
|
-
content: zod.z.string().describe("Text content for 'text' type segments.")
|
|
1075
|
-
});
|
|
1076
|
-
var BlankSegmentSchema = zod.z.object({
|
|
1077
|
-
type: zod.z.literal("blank"),
|
|
1078
|
-
id: zod.z.string().describe("Unique ID for 'blank' type segments, used to map to answers.")
|
|
1079
|
-
});
|
|
1080
|
-
var SegmentSchema = zod.z.discriminatedUnion("type", [
|
|
1081
|
-
TextSegmentSchema,
|
|
1082
|
-
BlankSegmentSchema
|
|
1083
|
-
]);
|
|
1084
|
-
var AnswerSchema = zod.z.object({
|
|
1085
|
-
blankId: zod.z.string().describe("ID of the blank this answer corresponds to."),
|
|
1086
|
-
acceptedValues: zod.z.array(zod.z.string()).min(1).describe("Array of acceptable string values for this blank.")
|
|
1087
|
-
});
|
|
1088
|
-
var FillInTheBlanksQuestionSchema = zod.z.object({
|
|
1089
|
-
// Các trường bắt buộc
|
|
1090
|
-
id: zod.z.string(),
|
|
1091
|
-
questionType: zod.z.literal("fill_in_the_blanks"),
|
|
1092
|
-
prompt: zod.z.string().describe("Overall instruction or context for the fill-in-the-blanks sentence(s)."),
|
|
1093
|
-
segments: zod.z.array(SegmentSchema).min(1).describe("Array of text and blank segments constructing the question."),
|
|
1094
|
-
answers: zod.z.array(AnswerSchema).min(1).describe("Definitions of correct answers for each blank."),
|
|
1095
|
-
// Các trường tùy chọn
|
|
1096
|
-
isCaseSensitive: zod.z.boolean().optional().describe("Whether answer evaluation should be case sensitive."),
|
|
1097
|
-
points: zod.z.number().optional(),
|
|
1098
|
-
explanation: zod.z.string().optional(),
|
|
1099
|
-
learningObjective: zod.z.string().optional(),
|
|
1100
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1101
|
-
bloomLevel: zod.z.string().optional(),
|
|
1102
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1103
|
-
contextCode: zod.z.string().optional(),
|
|
1104
|
-
gradeBand: zod.z.string().optional(),
|
|
1105
|
-
course: zod.z.string().optional(),
|
|
1106
|
-
category: zod.z.string().optional(),
|
|
1107
|
-
topic: zod.z.string().optional()
|
|
1108
|
-
});
|
|
1109
|
-
var DraggableItemSchema = zod.z.object({
|
|
1110
|
-
id: zod.z.string(),
|
|
1111
|
-
content: zod.z.string()
|
|
1112
|
-
});
|
|
1113
|
-
var DropZoneSchema = zod.z.object({
|
|
1114
|
-
id: zod.z.string(),
|
|
1115
|
-
label: zod.z.string()
|
|
1116
|
-
});
|
|
1117
|
-
var AnswerMapSchema = zod.z.object({
|
|
1118
|
-
draggableId: zod.z.string(),
|
|
1119
|
-
dropZoneId: zod.z.string()
|
|
1120
|
-
});
|
|
1121
|
-
var DragAndDropQuestionSchema = zod.z.object({
|
|
1122
|
-
// Các trường bắt buộc
|
|
1123
|
-
id: zod.z.string(),
|
|
1124
|
-
questionType: zod.z.literal("drag_and_drop"),
|
|
1125
|
-
prompt: zod.z.string(),
|
|
1126
|
-
draggableItems: zod.z.array(DraggableItemSchema).min(1),
|
|
1127
|
-
dropZones: zod.z.array(DropZoneSchema).min(1),
|
|
1128
|
-
answerMap: zod.z.array(AnswerMapSchema).min(1),
|
|
1129
|
-
// Các trường tùy chọn
|
|
1130
|
-
backgroundImageUrl: zod.z.string().url().optional().describe("Must be a valid URL format."),
|
|
1131
|
-
// .url() tương đương "format": "uri-reference"
|
|
1132
|
-
imageAltText: zod.z.string().optional(),
|
|
1133
|
-
points: zod.z.number().optional(),
|
|
1134
|
-
explanation: zod.z.string().optional(),
|
|
1135
|
-
learningObjective: zod.z.string().optional(),
|
|
1136
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1137
|
-
bloomLevel: zod.z.string().optional(),
|
|
1138
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1139
|
-
contextCode: zod.z.string().optional(),
|
|
1140
|
-
gradeBand: zod.z.string().optional(),
|
|
1141
|
-
course: zod.z.string().optional(),
|
|
1142
|
-
category: zod.z.string().optional(),
|
|
1143
|
-
topic: zod.z.string().optional()
|
|
1144
|
-
});
|
|
1145
|
-
var TrueFalseQuestionSchema = zod.z.object({
|
|
1146
|
-
// Các trường bắt buộc
|
|
1147
|
-
id: zod.z.string().describe("Unique identifier for the question."),
|
|
1148
|
-
questionType: zod.z.literal("true_false").describe("The type of the question."),
|
|
1149
|
-
prompt: zod.z.string().describe("The main text or statement for the question."),
|
|
1150
|
-
correctAnswer: zod.z.boolean().describe("The correct answer for the statement (true if the statement is true, false if it is false)."),
|
|
1151
|
-
// Các trường tùy chọn
|
|
1152
|
-
points: zod.z.number().optional().describe("Points awarded for a correct answer."),
|
|
1153
|
-
explanation: zod.z.string().optional().describe("Explanation for why the answer is correct or incorrect."),
|
|
1154
|
-
learningObjective: zod.z.string().optional().describe("The learning objective this question addresses."),
|
|
1155
|
-
glossary: zod.z.array(zod.z.string()).optional().describe("List of related glossary terms."),
|
|
1156
|
-
bloomLevel: zod.z.string().optional().describe("Cognitive level based on Bloom's Taxonomy (e.g., 'Remembering', 'Applying')."),
|
|
1157
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional().describe("Difficulty level of the question."),
|
|
1158
|
-
contextCode: zod.z.string().optional().describe("Identifier for the context of the question."),
|
|
1159
|
-
gradeBand: zod.z.string().optional().describe("Target grade band for the question (e.g., 'K-2', 'Middle School')."),
|
|
1160
|
-
course: zod.z.string().optional().describe("Associated course name."),
|
|
1161
|
-
category: zod.z.string().optional().describe("General category of the question content (e.g., 'Mathematics', 'History')."),
|
|
1162
|
-
topic: zod.z.string().optional().describe("Specific topic within the category.")
|
|
1163
|
-
});
|
|
1164
|
-
var ShortAnswerQuestionSchema = zod.z.object({
|
|
1165
|
-
// Các trường bắt buộc
|
|
1166
|
-
id: zod.z.string(),
|
|
1167
|
-
questionType: zod.z.literal("short_answer"),
|
|
1168
|
-
prompt: zod.z.string(),
|
|
1169
|
-
acceptedAnswers: zod.z.array(zod.z.string()).min(1).describe("An array of acceptable short answers."),
|
|
1170
|
-
// Các trường tùy chọn
|
|
1171
|
-
isCaseSensitive: zod.z.boolean().optional().describe("Whether the answer evaluation should be case sensitive."),
|
|
1172
|
-
points: zod.z.number().optional(),
|
|
1173
|
-
explanation: zod.z.string().optional(),
|
|
1174
|
-
learningObjective: zod.z.string().optional(),
|
|
1175
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1176
|
-
bloomLevel: zod.z.string().optional(),
|
|
1177
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1178
|
-
contextCode: zod.z.string().optional(),
|
|
1179
|
-
gradeBand: zod.z.string().optional(),
|
|
1180
|
-
course: zod.z.string().optional(),
|
|
1181
|
-
category: zod.z.string().optional(),
|
|
1182
|
-
topic: zod.z.string().optional()
|
|
1183
|
-
});
|
|
1184
|
-
var NumericQuestionSchema = zod.z.object({
|
|
1185
|
-
// Các trường bắt buộc
|
|
1186
|
-
id: zod.z.string(),
|
|
1187
|
-
questionType: zod.z.literal("numeric"),
|
|
1188
|
-
prompt: zod.z.string(),
|
|
1189
|
-
answer: zod.z.number().describe("The precise numerical correct answer."),
|
|
1190
|
-
// Các trường tùy chọn
|
|
1191
|
-
tolerance: zod.z.number().optional().describe("The acceptable range of error (plus or minus)."),
|
|
1192
|
-
points: zod.z.number().optional(),
|
|
1193
|
-
explanation: zod.z.string().optional(),
|
|
1194
|
-
learningObjective: zod.z.string().optional(),
|
|
1195
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1196
|
-
bloomLevel: zod.z.string().optional(),
|
|
1197
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1198
|
-
contextCode: zod.z.string().optional(),
|
|
1199
|
-
gradeBand: zod.z.string().optional(),
|
|
1200
|
-
course: zod.z.string().optional(),
|
|
1201
|
-
category: zod.z.string().optional(),
|
|
1202
|
-
topic: zod.z.string().optional()
|
|
1203
|
-
});
|
|
1204
|
-
var SequenceItemSchema = zod.z.object({
|
|
1205
|
-
id: zod.z.string().describe("Unique ID for the item."),
|
|
1206
|
-
content: zod.z.string().describe("Text content of the item.")
|
|
1207
|
-
});
|
|
1208
|
-
var SequenceQuestionSchema = zod.z.object({
|
|
1209
|
-
// Các trường bắt buộc
|
|
1210
|
-
id: zod.z.string(),
|
|
1211
|
-
questionType: zod.z.literal("sequence"),
|
|
1212
|
-
prompt: zod.z.string(),
|
|
1213
|
-
items: zod.z.array(SequenceItemSchema).min(2).describe("Array of items to be sequenced."),
|
|
1214
|
-
correctOrder: zod.z.array(zod.z.string()).min(2).describe("Array of item IDs in the correct sequence."),
|
|
1215
|
-
// Các trường tùy chọn
|
|
1216
|
-
points: zod.z.number().optional(),
|
|
1217
|
-
explanation: zod.z.string().optional(),
|
|
1218
|
-
learningObjective: zod.z.string().optional(),
|
|
1219
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1220
|
-
bloomLevel: zod.z.string().optional(),
|
|
1221
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1222
|
-
contextCode: zod.z.string().optional(),
|
|
1223
|
-
gradeBand: zod.z.string().optional(),
|
|
1224
|
-
course: zod.z.string().optional(),
|
|
1225
|
-
category: zod.z.string().optional(),
|
|
1226
|
-
topic: zod.z.string().optional()
|
|
1227
|
-
});
|
|
1228
|
-
var MatchPromptItemSchema = zod.z.object({
|
|
1229
|
-
id: zod.z.string(),
|
|
1230
|
-
content: zod.z.string()
|
|
1231
|
-
});
|
|
1232
|
-
var MatchOptionItemSchema = zod.z.object({
|
|
1233
|
-
id: zod.z.string(),
|
|
1234
|
-
content: zod.z.string()
|
|
1235
|
-
});
|
|
1236
|
-
var CorrectAnswerMapSchema = zod.z.object({
|
|
1237
|
-
promptId: zod.z.string(),
|
|
1238
|
-
optionId: zod.z.string()
|
|
1239
|
-
});
|
|
1240
|
-
var MatchingQuestionSchema = zod.z.object({
|
|
1241
|
-
// Các trường bắt buộc
|
|
1242
|
-
id: zod.z.string(),
|
|
1243
|
-
questionType: zod.z.literal("matching"),
|
|
1244
|
-
prompt: zod.z.string(),
|
|
1245
|
-
prompts: zod.z.array(MatchPromptItemSchema).min(1).describe("Array of items to be matched (e.g., terms, questions)."),
|
|
1246
|
-
options: zod.z.array(MatchOptionItemSchema).min(1).describe("Array of choices to match with the prompts (e.g., definitions, answers)."),
|
|
1247
|
-
correctAnswerMap: zod.z.array(CorrectAnswerMapSchema).min(1).describe("Array defining the correct pairings between prompt IDs and option IDs."),
|
|
1248
|
-
// Các trường tùy chọn
|
|
1249
|
-
shuffleOptions: zod.z.boolean().optional().describe("Whether the display order of options should be shuffled for the user."),
|
|
1250
|
-
points: zod.z.number().optional(),
|
|
1251
|
-
explanation: zod.z.string().optional(),
|
|
1252
|
-
learningObjective: zod.z.string().optional(),
|
|
1253
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1254
|
-
bloomLevel: zod.z.string().optional(),
|
|
1255
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1256
|
-
contextCode: zod.z.string().optional(),
|
|
1257
|
-
gradeBand: zod.z.string().optional(),
|
|
1258
|
-
course: zod.z.string().optional(),
|
|
1259
|
-
category: zod.z.string().optional(),
|
|
1260
|
-
topic: zod.z.string().optional()
|
|
1261
|
-
});
|
|
1262
|
-
var HotspotRectSchema = zod.z.object({
|
|
1263
|
-
id: zod.z.string().describe("Unique ID for the hotspot."),
|
|
1264
|
-
shape: zod.z.literal("rect"),
|
|
1265
|
-
coords: zod.z.number().array().length(4).describe("Coordinates for the rect: [x, y, width, height]."),
|
|
1266
|
-
description: zod.z.string().optional().describe("Optional description for the hotspot (e.g., for tooltips).")
|
|
1267
|
-
});
|
|
1268
|
-
var HotspotCircleSchema = zod.z.object({
|
|
1269
|
-
id: zod.z.string().describe("Unique ID for the hotspot."),
|
|
1270
|
-
shape: zod.z.literal("circle"),
|
|
1271
|
-
coords: zod.z.number().array().length(3).describe("Coordinates for the circle: [centerX, centerY, radius]."),
|
|
1272
|
-
description: zod.z.string().optional().describe("Optional description for the hotspot (e.g., for tooltips).")
|
|
1273
|
-
});
|
|
1274
|
-
var HotspotAreaSchema = zod.z.discriminatedUnion("shape", [
|
|
1275
|
-
HotspotRectSchema,
|
|
1276
|
-
HotspotCircleSchema
|
|
1277
|
-
]);
|
|
1278
|
-
var HotspotQuestionSchema = zod.z.object({
|
|
1279
|
-
// Các trường bắt buộc
|
|
1280
|
-
id: zod.z.string(),
|
|
1281
|
-
questionType: zod.z.literal("hotspot"),
|
|
1282
|
-
prompt: zod.z.string(),
|
|
1283
|
-
imageUrl: zod.z.string().url().describe("URL of the image to be used."),
|
|
1284
|
-
hotspots: zod.z.array(HotspotAreaSchema).min(1).describe("Array of clickable hotspot areas on the image."),
|
|
1285
|
-
correctHotspotIds: zod.z.array(zod.z.string()).min(1).describe("Array of IDs of the correct hotspots."),
|
|
1286
|
-
// Các trường tùy chọn
|
|
1287
|
-
imageAltText: zod.z.string().optional().describe("Alternative text for the image, for accessibility."),
|
|
1288
|
-
points: zod.z.number().optional(),
|
|
1289
|
-
explanation: zod.z.string().optional(),
|
|
1290
|
-
learningObjective: zod.z.string().optional(),
|
|
1291
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1292
|
-
bloomLevel: zod.z.string().optional(),
|
|
1293
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1294
|
-
contextCode: zod.z.string().optional(),
|
|
1295
|
-
gradeBand: zod.z.string().optional(),
|
|
1296
|
-
course: zod.z.string().optional(),
|
|
1297
|
-
category: zod.z.string().optional(),
|
|
1298
|
-
topic: zod.z.string().optional()
|
|
1299
|
-
});
|
|
1300
|
-
var BlocklyProgrammingQuestionSchema = zod.z.object({
|
|
1301
|
-
// Các trường bắt buộc (required)
|
|
1302
|
-
id: zod.z.string(),
|
|
1303
|
-
questionType: zod.z.literal("blockly_programming"),
|
|
1304
|
-
prompt: zod.z.string(),
|
|
1305
|
-
toolboxDefinition: zod.z.string().describe("XML string defining the Blockly toolbox."),
|
|
1306
|
-
// Các trường tùy chọn (optional)
|
|
1307
|
-
initialWorkspace: zod.z.string().optional().describe("Optional XML string for the initial state of the Blockly workspace."),
|
|
1308
|
-
solutionWorkspaceXML: zod.z.string().optional().describe("Optional XML string representing the solution, for visual display."),
|
|
1309
|
-
solutionGeneratedCode: zod.z.string().optional().describe("The JavaScript code that a correct Blockly program should generate, used for grading."),
|
|
1310
|
-
points: zod.z.number().optional(),
|
|
1311
|
-
explanation: zod.z.string().optional(),
|
|
1312
|
-
learningObjective: zod.z.string().optional(),
|
|
1313
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1314
|
-
bloomLevel: zod.z.string().optional(),
|
|
1315
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1316
|
-
contextCode: zod.z.string().optional(),
|
|
1317
|
-
gradeBand: zod.z.string().optional(),
|
|
1318
|
-
course: zod.z.string().optional(),
|
|
1319
|
-
category: zod.z.string().optional(),
|
|
1320
|
-
topic: zod.z.string().optional()
|
|
1321
|
-
});
|
|
1322
|
-
var exampleData2 = {
|
|
1323
|
-
id: "blkq-001",
|
|
1324
|
-
questionType: "blockly_programming",
|
|
1325
|
-
prompt: "Create a program to say 'Hello!'",
|
|
1326
|
-
toolboxDefinition: "<xml>...</xml>",
|
|
1327
|
-
solutionGeneratedCode: "window.alert('Hello!');"
|
|
1328
|
-
};
|
|
1329
|
-
try {
|
|
1330
|
-
const validatedQuestion = BlocklyProgrammingQuestionSchema.parse(exampleData2);
|
|
1331
|
-
console.log("Validation successful:", validatedQuestion);
|
|
1332
|
-
} catch (error) {
|
|
1333
|
-
console.error("Validation failed:", error);
|
|
1334
|
-
}
|
|
1335
|
-
var ScratchProgrammingQuestionSchema = zod.z.object({
|
|
1336
|
-
// Các trường bắt buộc (required)
|
|
1337
|
-
id: zod.z.string(),
|
|
1338
|
-
questionType: zod.z.literal("scratch_programming"),
|
|
1339
|
-
prompt: zod.z.string(),
|
|
1340
|
-
toolboxDefinition: zod.z.string().describe("XML string defining the Blockly/Scratch toolbox."),
|
|
1341
|
-
// Các trường tùy chọn (optional)
|
|
1342
|
-
initialWorkspace: zod.z.string().optional().describe("Optional XML string for the initial state of the workspace."),
|
|
1343
|
-
solutionWorkspaceXML: zod.z.string().optional().describe("Optional XML string representing the solution blocks."),
|
|
1344
|
-
solutionGeneratedCode: zod.z.string().optional().describe("The code or logic representation that a correct program should achieve, used for grading."),
|
|
1345
|
-
points: zod.z.number().optional(),
|
|
1346
|
-
explanation: zod.z.string().optional(),
|
|
1347
|
-
learningObjective: zod.z.string().optional(),
|
|
1348
|
-
glossary: zod.z.array(zod.z.string()).optional(),
|
|
1349
|
-
bloomLevel: zod.z.string().optional(),
|
|
1350
|
-
difficulty: zod.z.enum(["easy", "medium", "hard"]).optional(),
|
|
1351
|
-
contextCode: zod.z.string().optional(),
|
|
1352
|
-
gradeBand: zod.z.string().optional(),
|
|
1353
|
-
course: zod.z.string().optional(),
|
|
1354
|
-
category: zod.z.string().optional(),
|
|
1355
|
-
topic: zod.z.string().optional()
|
|
1356
|
-
});
|
|
1357
|
-
var exampleData3 = {
|
|
1358
|
-
id: "scrq-001",
|
|
1359
|
-
questionType: "scratch_programming",
|
|
1360
|
-
prompt: "Make the cat move 10 steps.",
|
|
1361
|
-
toolboxDefinition: "<xml>...</xml>",
|
|
1362
|
-
solutionGeneratedCode: "move(10)"
|
|
1363
|
-
};
|
|
1364
|
-
try {
|
|
1365
|
-
const validatedQuestion = ScratchProgrammingQuestionSchema.parse(exampleData3);
|
|
1366
|
-
console.log("Validation successful:", validatedQuestion);
|
|
1367
|
-
} catch (error) {
|
|
1368
|
-
console.error("Validation failed:", error);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// src/services/QuestionImportService.ts
|
|
1372
|
-
var tsvQuestionTypeMap = {
|
|
1373
|
-
TF: "true_false",
|
|
1374
|
-
MC: "multiple_choice",
|
|
1375
|
-
MR: "multiple_response",
|
|
1376
|
-
TI: "short_answer",
|
|
1377
|
-
MG: "matching",
|
|
1378
|
-
SEQ: "sequence",
|
|
1379
|
-
NUMG: "numeric"
|
|
1380
|
-
// Add other mappings if needed
|
|
1381
|
-
};
|
|
1382
|
-
var tsvDifficultyMap = {
|
|
1383
|
-
"d\u1EC5": "easy",
|
|
1384
|
-
"easy": "easy",
|
|
1385
|
-
"trung b\xECnh": "medium",
|
|
1386
|
-
"medium": "medium",
|
|
1387
|
-
"kh\xF3": "hard",
|
|
1388
|
-
"hard": "hard"
|
|
1389
|
-
};
|
|
1390
|
-
var tsvBloomLevelMap = {
|
|
1391
|
-
"nh\u1EDB": "remembering",
|
|
1392
|
-
"remembering": "remembering",
|
|
1393
|
-
"hi\u1EC3u": "understanding",
|
|
1394
|
-
"understanding": "understanding",
|
|
1395
|
-
"\xE1p d\u1EE5ng": "applying",
|
|
1396
|
-
"applying": "applying",
|
|
1397
|
-
"v\u1EADn d\u1EE5ng": "applying"
|
|
1398
|
-
// Add other bloom levels if needed
|
|
1399
|
-
};
|
|
1400
|
-
var schemaMap = {
|
|
1401
|
-
multiple_choice: MultipleChoiceQuestionSchema,
|
|
1402
|
-
multiple_response: MultipleResponseQuestionSchema,
|
|
1403
|
-
fill_in_the_blanks: FillInTheBlanksQuestionSchema,
|
|
1404
|
-
drag_and_drop: DragAndDropQuestionSchema,
|
|
1405
|
-
true_false: TrueFalseQuestionSchema,
|
|
1406
|
-
short_answer: ShortAnswerQuestionSchema,
|
|
1407
|
-
numeric: NumericQuestionSchema,
|
|
1408
|
-
sequence: SequenceQuestionSchema,
|
|
1409
|
-
matching: MatchingQuestionSchema,
|
|
1410
|
-
hotspot: HotspotQuestionSchema,
|
|
1411
|
-
blockly_programming: BlocklyProgrammingQuestionSchema,
|
|
1412
|
-
scratch_programming: ScratchProgrammingQuestionSchema
|
|
1413
|
-
};
|
|
1414
|
-
var QuestionImportService = class {
|
|
1415
|
-
static processJSON(jsonString) {
|
|
1416
|
-
const validQuestions = [];
|
|
1417
|
-
const errors = [];
|
|
1418
|
-
let parsedData;
|
|
1419
|
-
try {
|
|
1420
|
-
parsedData = JSON.parse(jsonString);
|
|
1421
|
-
} catch (e) {
|
|
1422
|
-
errors.push({
|
|
1423
|
-
index: 0,
|
|
1424
|
-
message: `Invalid JSON format: ${e.message || "Could not parse the file/text."}`,
|
|
1425
|
-
data: jsonString.substring(0, 500)
|
|
1426
|
-
});
|
|
1427
|
-
return { validQuestions, errors };
|
|
1428
|
-
}
|
|
1429
|
-
if (!Array.isArray(parsedData)) {
|
|
1430
|
-
errors.push({
|
|
1431
|
-
index: 0,
|
|
1432
|
-
message: "Invalid format. The root of the JSON must be an array of question objects.",
|
|
1433
|
-
data: parsedData
|
|
1434
|
-
});
|
|
1435
|
-
return { validQuestions, errors };
|
|
1436
|
-
}
|
|
1437
|
-
parsedData.forEach((rawQuestion, index) => {
|
|
1438
|
-
if (typeof rawQuestion !== "object" || rawQuestion === null || !rawQuestion.questionType) {
|
|
1439
|
-
errors.push({
|
|
1440
|
-
index: index + 1,
|
|
1441
|
-
message: 'Invalid question object. Each item must be an object with a "questionType" property.',
|
|
1442
|
-
data: rawQuestion
|
|
1443
|
-
});
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
const schema = schemaMap[rawQuestion.questionType];
|
|
1447
|
-
if (!schema) {
|
|
1448
|
-
errors.push({
|
|
1449
|
-
index: index + 1,
|
|
1450
|
-
message: `Unsupported question type: "${rawQuestion.questionType}".`,
|
|
1451
|
-
data: rawQuestion
|
|
1452
|
-
});
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
const validationResult = schema.safeParse(rawQuestion);
|
|
1456
|
-
if (validationResult.success) {
|
|
1457
|
-
const sanitizedQuestion = this._sanitizeAndRegenerateIds(validationResult.data);
|
|
1458
|
-
validQuestions.push(sanitizedQuestion);
|
|
1459
|
-
} else {
|
|
1460
|
-
const errorMessages = validationResult.error.errors.map((e) => `${e.path.join(".")} - ${e.message}`).join("; ");
|
|
1461
|
-
errors.push({
|
|
1462
|
-
index: index + 1,
|
|
1463
|
-
message: `Validation failed: ${errorMessages}`,
|
|
1464
|
-
data: rawQuestion
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
});
|
|
1468
|
-
return { validQuestions, errors };
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Processes a TSV string to import questions.
|
|
1472
|
-
* @param tsvString The raw TSV string to process.
|
|
1473
|
-
* @returns An object containing arrays of valid questions and any errors encountered.
|
|
1474
|
-
*/
|
|
1475
|
-
static processTSV(tsvString) {
|
|
1476
|
-
const validQuestions = [];
|
|
1477
|
-
const errors = [];
|
|
1478
|
-
const lines = tsvString.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
1479
|
-
if (lines.length < 2) {
|
|
1480
|
-
errors.push({ index: 0, message: "TSV file must have a header row and at least one data row.", data: tsvString });
|
|
1481
|
-
return { validQuestions, errors };
|
|
1482
|
-
}
|
|
1483
|
-
const headerFields = lines[0].split(" ").map((h) => h.trim().toLowerCase());
|
|
1484
|
-
const headerMap = new Map(headerFields.map((header, index) => [header, index]));
|
|
1485
|
-
const requiredHeaders = ["question type", "question text"];
|
|
1486
|
-
for (const required of requiredHeaders) {
|
|
1487
|
-
if (!headerMap.has(required)) {
|
|
1488
|
-
errors.push({ index: 0, message: `Missing required header column: "${required}".`, data: lines[0] });
|
|
1489
|
-
return { validQuestions, errors };
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
const dataRows = lines.slice(1);
|
|
1493
|
-
dataRows.forEach((row, index) => {
|
|
1494
|
-
const rowNumber = index + 2;
|
|
1495
|
-
const columns = row.split(" ");
|
|
1496
|
-
try {
|
|
1497
|
-
const questionTypeAbbr = (columns[headerMap.get("question type")] || "").trim().toUpperCase();
|
|
1498
|
-
const questionType = tsvQuestionTypeMap[questionTypeAbbr];
|
|
1499
|
-
if (!questionType) {
|
|
1500
|
-
throw new Error(`Unknown or unsupported Question Type: "${questionTypeAbbr}".`);
|
|
1501
|
-
}
|
|
1502
|
-
let transformedQuestion = this._transformRowToQuestion(columns, headerMap, questionType);
|
|
1503
|
-
const schema = schemaMap[questionType];
|
|
1504
|
-
const validationResult = schema.safeParse(transformedQuestion);
|
|
1505
|
-
if (validationResult.success) {
|
|
1506
|
-
const sanitizedQuestion = this._sanitizeAndRegenerateIds(validationResult.data);
|
|
1507
|
-
validQuestions.push(sanitizedQuestion);
|
|
1508
|
-
} else {
|
|
1509
|
-
const errorMessages = validationResult.error.errors.map((e) => `${e.path.join(".")} - ${e.message}`).join("; ");
|
|
1510
|
-
throw new Error(`Validation failed: ${errorMessages}`);
|
|
1511
|
-
}
|
|
1512
|
-
} catch (e) {
|
|
1513
|
-
errors.push({
|
|
1514
|
-
index: rowNumber,
|
|
1515
|
-
message: e.message,
|
|
1516
|
-
data: row
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
});
|
|
1520
|
-
return { validQuestions, errors };
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Central dispatcher to transform a TSV row into a QuizQuestion object.
|
|
1524
|
-
* @private
|
|
1525
|
-
*/
|
|
1526
|
-
static _transformRowToQuestion(columns, headerMap, type) {
|
|
1527
|
-
const getColumn = (name) => {
|
|
1528
|
-
var _a;
|
|
1529
|
-
return ((_a = columns[headerMap.get(name.toLowerCase())]) == null ? void 0 : _a.trim()) || "";
|
|
1530
|
-
};
|
|
1531
|
-
const baseQuestion = {
|
|
1532
|
-
prompt: getColumn("Question Text"),
|
|
1533
|
-
explanation: getColumn("Explanation"),
|
|
1534
|
-
points: parseInt(getColumn("Points"), 10) || 10,
|
|
1535
|
-
difficulty: tsvDifficultyMap[getColumn("Difficulty").toLowerCase()] || "medium",
|
|
1536
|
-
bloomLevel: tsvBloomLevelMap[getColumn("Bloom Taxonomy").toLowerCase()],
|
|
1537
|
-
topic: getColumn("Concepts")
|
|
1538
|
-
// Add other base fields here if needed
|
|
1539
|
-
};
|
|
1540
|
-
const answerCols = Array.from({ length: 10 }, (_, i) => getColumn(`Answer ${i + 1}`)).filter(Boolean);
|
|
1541
|
-
switch (type) {
|
|
1542
|
-
case "multiple_choice":
|
|
1543
|
-
return this._transformMC(baseQuestion, answerCols);
|
|
1544
|
-
case "multiple_response":
|
|
1545
|
-
return this._transformMR(baseQuestion, answerCols);
|
|
1546
|
-
case "true_false":
|
|
1547
|
-
return this._transformTF(baseQuestion, answerCols);
|
|
1548
|
-
case "short_answer":
|
|
1549
|
-
return __spreadProps(__spreadValues({}, baseQuestion), { questionType: "short_answer", acceptedAnswers: answerCols, isCaseSensitive: false });
|
|
1550
|
-
case "numeric":
|
|
1551
|
-
const answer = parseFloat(answerCols[0]);
|
|
1552
|
-
if (isNaN(answer)) throw new Error("Numeric answer is not a valid number.");
|
|
1553
|
-
return __spreadProps(__spreadValues({}, baseQuestion), { questionType: "numeric", answer, tolerance: void 0 });
|
|
1554
|
-
case "matching":
|
|
1555
|
-
return this._transformMG(baseQuestion, answerCols);
|
|
1556
|
-
case "sequence":
|
|
1557
|
-
return this._transformSEQ(baseQuestion, answerCols);
|
|
1558
|
-
default:
|
|
1559
|
-
throw new Error(`Transformation logic for type "${type}" is not implemented.`);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
// --- Specific Transformation Helpers ---
|
|
1563
|
-
static _transformMC(base, answers) {
|
|
1564
|
-
let correctAnswerId = "";
|
|
1565
|
-
const options = answers.map((ans) => {
|
|
1566
|
-
const isCorrect = ans.startsWith("*");
|
|
1567
|
-
const text = isCorrect ? ans.substring(1).trim() : ans.trim();
|
|
1568
|
-
const id = generateUniqueId("opt_");
|
|
1569
|
-
if (isCorrect) {
|
|
1570
|
-
correctAnswerId = id;
|
|
1571
|
-
}
|
|
1572
|
-
return { id, text };
|
|
1573
|
-
});
|
|
1574
|
-
if (!correctAnswerId) throw new Error("Multiple Choice question must have one correct answer marked with *.");
|
|
1575
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "multiple_choice", options, correctAnswerId });
|
|
1576
|
-
}
|
|
1577
|
-
static _transformMR(base, answers) {
|
|
1578
|
-
const correctAnswerIds = [];
|
|
1579
|
-
const options = answers.map((ans) => {
|
|
1580
|
-
const isCorrect = ans.startsWith("*");
|
|
1581
|
-
const text = isCorrect ? ans.substring(1).trim() : ans.trim();
|
|
1582
|
-
const id = generateUniqueId("opt_mr_");
|
|
1583
|
-
if (isCorrect) {
|
|
1584
|
-
correctAnswerIds.push(id);
|
|
1585
|
-
}
|
|
1586
|
-
return { id, text };
|
|
1587
|
-
});
|
|
1588
|
-
if (correctAnswerIds.length === 0) throw new Error("Multiple Response question must have at least one correct answer marked with *.");
|
|
1589
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "multiple_response", options, correctAnswerIds });
|
|
1590
|
-
}
|
|
1591
|
-
static _transformTF(base, answers) {
|
|
1592
|
-
if (answers.length < 2) throw new Error("True/False question requires at least two answer columns.");
|
|
1593
|
-
const isTrueCorrect = answers[0].startsWith("*");
|
|
1594
|
-
const isFalseCorrect = answers[1].startsWith("*");
|
|
1595
|
-
if (isTrueCorrect === isFalseCorrect) throw new Error("True/False question must have exactly one correct answer marked with *.");
|
|
1596
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "true_false", correctAnswer: isTrueCorrect });
|
|
1597
|
-
}
|
|
1598
|
-
static _transformMG(base, answers) {
|
|
1599
|
-
const prompts = [];
|
|
1600
|
-
const options = [];
|
|
1601
|
-
const correctAnswerMap = [];
|
|
1602
|
-
answers.forEach((pair) => {
|
|
1603
|
-
const parts = pair.split("|");
|
|
1604
|
-
if (parts.length !== 2) throw new Error(`Invalid matching pair format: "${pair}". Must be "prompt|option".`);
|
|
1605
|
-
const promptItem = { id: generateUniqueId("m_p_"), content: parts[0].trim() };
|
|
1606
|
-
const optionItem = { id: generateUniqueId("m_o_"), content: parts[1].trim() };
|
|
1607
|
-
prompts.push(promptItem);
|
|
1608
|
-
options.push(optionItem);
|
|
1609
|
-
correctAnswerMap.push({ promptId: promptItem.id, optionId: optionItem.id });
|
|
1610
|
-
});
|
|
1611
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "matching", prompts, options, correctAnswerMap, shuffleOptions: true });
|
|
1612
|
-
}
|
|
1613
|
-
static _transformSEQ(base, answers) {
|
|
1614
|
-
const items = [];
|
|
1615
|
-
const correctOrder = [];
|
|
1616
|
-
answers.forEach((content) => {
|
|
1617
|
-
const item = { id: generateUniqueId("seqi_"), content: content.trim() };
|
|
1618
|
-
items.push(item);
|
|
1619
|
-
correctOrder.push(item.id);
|
|
1620
|
-
});
|
|
1621
|
-
return __spreadProps(__spreadValues({}, base), { questionType: "sequence", items, correctOrder });
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* @private
|
|
1625
|
-
*/
|
|
1626
|
-
static _sanitizeAndRegenerateIds(question) {
|
|
1627
|
-
const newQuestion = __spreadProps(__spreadValues({}, question), { id: generateUniqueId(`${question.questionType}_`) });
|
|
1628
|
-
switch (newQuestion.questionType) {
|
|
1629
|
-
case "multiple_choice": {
|
|
1630
|
-
const oldIdToNewIdMap = /* @__PURE__ */ new Map();
|
|
1631
|
-
newQuestion.options = newQuestion.options.map((opt) => {
|
|
1632
|
-
const newId = generateUniqueId("opt_");
|
|
1633
|
-
oldIdToNewIdMap.set(opt.id, newId);
|
|
1634
|
-
return __spreadProps(__spreadValues({}, opt), { id: newId });
|
|
1635
|
-
});
|
|
1636
|
-
newQuestion.correctAnswerId = oldIdToNewIdMap.get(newQuestion.correctAnswerId) || "";
|
|
1637
|
-
break;
|
|
1638
|
-
}
|
|
1639
|
-
case "multiple_response": {
|
|
1640
|
-
const oldIdToNewIdMap = /* @__PURE__ */ new Map();
|
|
1641
|
-
newQuestion.options = newQuestion.options.map((opt) => {
|
|
1642
|
-
const newId = generateUniqueId("opt_mr_");
|
|
1643
|
-
oldIdToNewIdMap.set(opt.id, newId);
|
|
1644
|
-
return __spreadProps(__spreadValues({}, opt), { id: newId });
|
|
1645
|
-
});
|
|
1646
|
-
newQuestion.correctAnswerIds = newQuestion.correctAnswerIds.map((oldId) => oldIdToNewIdMap.get(oldId)).filter((newId) => !!newId);
|
|
1647
|
-
break;
|
|
1648
|
-
}
|
|
1649
|
-
case "fill_in_the_blanks": {
|
|
1650
|
-
const oldBlankIdToNewIdMap = /* @__PURE__ */ new Map();
|
|
1651
|
-
newQuestion.segments = newQuestion.segments.map((seg) => {
|
|
1652
|
-
if (seg.type === "blank" && seg.id) {
|
|
1653
|
-
const newId = generateUniqueId("blank_");
|
|
1654
|
-
oldBlankIdToNewIdMap.set(seg.id, newId);
|
|
1655
|
-
return __spreadProps(__spreadValues({}, seg), { id: newId });
|
|
1656
|
-
}
|
|
1657
|
-
return seg;
|
|
1658
|
-
});
|
|
1659
|
-
newQuestion.answers = newQuestion.answers.map((ans) => {
|
|
1660
|
-
const newBlankId = oldBlankIdToNewIdMap.get(ans.blankId);
|
|
1661
|
-
return newBlankId ? __spreadProps(__spreadValues({}, ans), { blankId: newBlankId }) : ans;
|
|
1662
|
-
});
|
|
1663
|
-
break;
|
|
1664
|
-
}
|
|
1665
|
-
case "sequence": {
|
|
1666
|
-
const oldIdToNewIdMap = /* @__PURE__ */ new Map();
|
|
1667
|
-
newQuestion.items = newQuestion.items.map((item) => {
|
|
1668
|
-
const newId = generateUniqueId("seqi_");
|
|
1669
|
-
oldIdToNewIdMap.set(item.id, newId);
|
|
1670
|
-
return __spreadProps(__spreadValues({}, item), { id: newId });
|
|
1671
|
-
});
|
|
1672
|
-
newQuestion.correctOrder = newQuestion.correctOrder.map((oldId) => oldIdToNewIdMap.get(oldId)).filter((newId) => !!newId);
|
|
1673
|
-
break;
|
|
1674
|
-
}
|
|
1675
|
-
case "matching": {
|
|
1676
|
-
const oldPromptIdMap = /* @__PURE__ */ new Map();
|
|
1677
|
-
const oldOptionIdMap = /* @__PURE__ */ new Map();
|
|
1678
|
-
newQuestion.prompts = newQuestion.prompts.map((p) => {
|
|
1679
|
-
const newId = generateUniqueId("m_p_");
|
|
1680
|
-
oldPromptIdMap.set(p.id, newId);
|
|
1681
|
-
return __spreadProps(__spreadValues({}, p), { id: newId });
|
|
1682
|
-
});
|
|
1683
|
-
newQuestion.options = newQuestion.options.map((o) => {
|
|
1684
|
-
const newId = generateUniqueId("m_o_");
|
|
1685
|
-
oldOptionIdMap.set(o.id, newId);
|
|
1686
|
-
return __spreadProps(__spreadValues({}, o), { id: newId });
|
|
1687
|
-
});
|
|
1688
|
-
newQuestion.correctAnswerMap = newQuestion.correctAnswerMap.map((map) => ({
|
|
1689
|
-
promptId: oldPromptIdMap.get(map.promptId) || "",
|
|
1690
|
-
optionId: oldOptionIdMap.get(map.optionId) || ""
|
|
1691
|
-
})).filter((map) => map.promptId && map.optionId);
|
|
1692
|
-
break;
|
|
1693
|
-
}
|
|
1694
|
-
case "drag_and_drop": {
|
|
1695
|
-
const oldDraggableIdMap = /* @__PURE__ */ new Map();
|
|
1696
|
-
const oldDropZoneIdMap = /* @__PURE__ */ new Map();
|
|
1697
|
-
newQuestion.draggableItems = newQuestion.draggableItems.map((item) => {
|
|
1698
|
-
const newId = generateUniqueId("drag_");
|
|
1699
|
-
oldDraggableIdMap.set(item.id, newId);
|
|
1700
|
-
return __spreadProps(__spreadValues({}, item), { id: newId });
|
|
1701
|
-
});
|
|
1702
|
-
newQuestion.dropZones = newQuestion.dropZones.map((zone) => {
|
|
1703
|
-
const newId = generateUniqueId("zone_");
|
|
1704
|
-
oldDropZoneIdMap.set(zone.id, newId);
|
|
1705
|
-
return __spreadProps(__spreadValues({}, zone), { id: newId });
|
|
1706
|
-
});
|
|
1707
|
-
newQuestion.answerMap = newQuestion.answerMap.map((map) => ({
|
|
1708
|
-
draggableId: oldDraggableIdMap.get(map.draggableId) || "",
|
|
1709
|
-
dropZoneId: oldDropZoneIdMap.get(map.dropZoneId) || ""
|
|
1710
|
-
})).filter((map) => map.draggableId && map.dropZoneId);
|
|
1711
|
-
break;
|
|
1712
|
-
}
|
|
1713
|
-
case "hotspot": {
|
|
1714
|
-
const oldIdToNewIdMap = /* @__PURE__ */ new Map();
|
|
1715
|
-
newQuestion.hotspots = newQuestion.hotspots.map((spot) => {
|
|
1716
|
-
const newId = generateUniqueId("hs_");
|
|
1717
|
-
oldIdToNewIdMap.set(spot.id, newId);
|
|
1718
|
-
return __spreadProps(__spreadValues({}, spot), { id: newId });
|
|
1719
|
-
});
|
|
1720
|
-
newQuestion.correctHotspotIds = newQuestion.correctHotspotIds.map((oldId) => oldIdToNewIdMap.get(oldId)).filter((newId) => !!newId);
|
|
1721
|
-
break;
|
|
1722
|
-
}
|
|
1723
|
-
case "true_false":
|
|
1724
|
-
case "short_answer":
|
|
1725
|
-
case "numeric":
|
|
1726
|
-
case "blockly_programming":
|
|
1727
|
-
case "scratch_programming":
|
|
1728
|
-
break;
|
|
1729
|
-
default:
|
|
1730
|
-
const _exhaustiveCheck = newQuestion;
|
|
1731
|
-
console.warn("Unhandled question type in _sanitizeAndRegenerateIds:", _exhaustiveCheck);
|
|
1732
|
-
break;
|
|
1733
|
-
}
|
|
1734
|
-
return newQuestion;
|
|
1735
|
-
}
|
|
1736
|
-
};
|
|
1737
|
-
|
|
1738
|
-
// src/services/APIKeyService.ts
|
|
1739
|
-
var GEMINI_API_KEY_SERVICE_NAME = "gemini";
|
|
1740
|
-
var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
|
|
1741
|
-
function _encode(data) {
|
|
1742
|
-
if (typeof window !== "undefined" && typeof window.btoa === "function") {
|
|
1743
|
-
try {
|
|
1744
|
-
return window.btoa(data);
|
|
1745
|
-
} catch (e) {
|
|
1746
|
-
console.error("Base64 encoding (btoa) failed:", e);
|
|
1747
|
-
return data;
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
return data;
|
|
1751
|
-
}
|
|
1752
|
-
function _decode(data) {
|
|
1753
|
-
if (typeof window !== "undefined" && typeof window.atob === "function") {
|
|
1754
|
-
try {
|
|
1755
|
-
return window.atob(data);
|
|
1756
|
-
} catch (e) {
|
|
1757
|
-
console.error("Base64 decoding (atob) failed:", e);
|
|
1758
|
-
return data;
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
return data;
|
|
1762
|
-
}
|
|
1763
|
-
var APIKeyService = class {
|
|
1764
|
-
static getStorageKey(serviceName) {
|
|
1765
|
-
return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
|
|
1766
|
-
}
|
|
1767
|
-
/**
|
|
1768
|
-
* Saves an API key to localStorage. The key is mildly obfuscated using Base64.
|
|
1769
|
-
* @param serviceName - The name of the service (e.g., 'gemini').
|
|
1770
|
-
* @param apiKey - The API key to save.
|
|
1771
|
-
*/
|
|
1772
|
-
static saveAPIKey(serviceName, apiKey) {
|
|
1773
|
-
if (typeof window !== "undefined" && window.localStorage) {
|
|
1774
|
-
try {
|
|
1775
|
-
const encodedKey = _encode(apiKey);
|
|
1776
|
-
localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
|
|
1777
|
-
} catch (e) {
|
|
1778
|
-
console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
|
|
1779
|
-
}
|
|
1780
|
-
} else {
|
|
1781
|
-
console.warn("localStorage is not available. APIKeyService cannot save keys.");
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
/**
|
|
1785
|
-
* Retrieves an API key from localStorage.
|
|
1786
|
-
* @param serviceName - The name of the service.
|
|
1787
|
-
* @returns The decoded API key, or null if not found or if localStorage is unavailable.
|
|
1788
|
-
*/
|
|
1789
|
-
static getAPIKey(serviceName) {
|
|
1790
|
-
if (typeof window !== "undefined" && window.localStorage) {
|
|
1791
|
-
try {
|
|
1792
|
-
const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
|
|
1793
|
-
if (storedKey) {
|
|
1794
|
-
return _decode(storedKey);
|
|
1795
|
-
}
|
|
1796
|
-
} catch (e) {
|
|
1797
|
-
console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
return null;
|
|
1801
|
-
}
|
|
1802
|
-
/**
|
|
1803
|
-
* Removes an API key from localStorage.
|
|
1804
|
-
* @param serviceName - The name of the service.
|
|
1805
|
-
*/
|
|
1806
|
-
static removeAPIKey(serviceName) {
|
|
1807
|
-
if (typeof window !== "undefined" && window.localStorage) {
|
|
1808
|
-
try {
|
|
1809
|
-
localStorage.removeItem(this.getStorageKey(serviceName));
|
|
1810
|
-
} catch (e) {
|
|
1811
|
-
console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
/**
|
|
1816
|
-
* Checks if an API key exists in localStorage for the given service.
|
|
1817
|
-
* @param serviceName - The name of the service.
|
|
1818
|
-
* @returns True if a key exists, false otherwise.
|
|
1819
|
-
*/
|
|
1820
|
-
static hasAPIKey(serviceName) {
|
|
1821
|
-
return this.getAPIKey(serviceName) !== null;
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
|
|
1825
|
-
// src/services/HTMLLauncherGenerator.ts
|
|
1826
|
-
var escapeAttribute = (unsafe) => {
|
|
1827
|
-
if (typeof unsafe !== "string") return "";
|
|
1828
|
-
return unsafe.replace(/"/g, '"');
|
|
1829
|
-
};
|
|
1830
|
-
var generateLauncherHTML = (quizConfig, libraryJSPath, quizDataPath, blocklyCSSPath, mainCSSPath, title) => {
|
|
1831
|
-
const pageTitle = escapeAttribute(title || quizConfig.title || "Interactive Quiz");
|
|
1832
|
-
const relLibraryJSPath = libraryJSPath.startsWith("./") ? libraryJSPath : `./${libraryJSPath}`;
|
|
1833
|
-
const relQuizDataPath = quizDataPath.startsWith("./") ? quizDataPath : `./${quizDataPath}`;
|
|
1834
|
-
const relBlocklyCSSPath = blocklyCSSPath.startsWith("./") ? blocklyCSSPath : `./${blocklyCSSPath}`;
|
|
1835
|
-
const relMainCSSPath = mainCSSPath.startsWith("./") ? mainCSSPath : `./${mainCSSPath}`;
|
|
1836
|
-
return `<!DOCTYPE html>
|
|
1837
|
-
<html lang="en">
|
|
1838
|
-
<head>
|
|
1839
|
-
<meta charset="UTF-8">
|
|
1840
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1841
|
-
<title>${pageTitle}</title>
|
|
1842
|
-
<link rel="stylesheet" href="${escapeAttribute(relMainCSSPath)}">
|
|
1843
|
-
<link rel="stylesheet" href="${escapeAttribute(relBlocklyCSSPath)}">
|
|
1844
|
-
<style>
|
|
1845
|
-
/* --- CSS \u0110\u1EC2 C\u0102N GI\u1EEEA CARD --- */
|
|
1846
|
-
html, body {
|
|
1847
|
-
height: 100%; /* \u0110\u1EA3m b\u1EA3o body chi\u1EBFm to\xE0n b\u1ED9 chi\u1EC1u cao */
|
|
1848
|
-
margin: 0;
|
|
1849
|
-
}
|
|
1850
|
-
body {
|
|
1851
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
1852
|
-
background-color: #f0f2f5; /* M\xE0u n\u1EC1n x\xE1m */
|
|
1853
|
-
color: #1f2937;
|
|
1854
|
-
|
|
1855
|
-
/* S\u1EED d\u1EE5ng Flexbox \u0111\u1EC3 c\u0103n gi\u1EEFa */
|
|
1856
|
-
display: flex;
|
|
1857
|
-
justify-content: center; /* C\u0103n gi\u1EEFa theo chi\u1EC1u ngang */
|
|
1858
|
-
align-items: center; /* C\u0103n gi\u1EEFa theo chi\u1EC1u d\u1ECDc */
|
|
1859
|
-
|
|
1860
|
-
padding: 20px; /* Th\xEAm m\u1ED9t ch\xFAt l\u1EC1 cho \u0111\u1EB9p m\u1EAFt tr\xEAn m\xE0n h\xECnh nh\u1ECF */
|
|
1861
|
-
box-sizing: border-box;
|
|
1862
|
-
}
|
|
1863
|
-
#root {
|
|
1864
|
-
width: 100%;
|
|
1865
|
-
max-width: 900px; /* Gi\u1EDBi h\u1EA1n chi\u1EC1u r\u1ED9ng t\u1ED1i \u0111a c\u1EE7a card quiz */
|
|
1866
|
-
/* #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 */
|
|
1867
|
-
}
|
|
1868
|
-
/* C\xE1c style c\xF2n l\u1EA1i gi\u1EEF nguy\xEAn */
|
|
1869
|
-
.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; }
|
|
1870
|
-
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
1871
|
-
.status-message { text-align: center; padding: 20px; margin-top: 10px; color: #4b5563; }
|
|
1872
|
-
</style>
|
|
1873
|
-
</head>
|
|
1874
|
-
<body>
|
|
1875
|
-
<div id="root">
|
|
1876
|
-
<!-- N\u1ED9i dung ban \u0111\u1EA7u s\u1EBD \u0111\u01B0\u1EE3c React thay th\u1EBF -->
|
|
1877
|
-
<div class="loading-spinner" aria-label="Loading quiz content"></div>
|
|
1878
|
-
<p class="status-message" role="status">Loading Quiz...</p>
|
|
1879
|
-
</div>
|
|
1880
|
-
|
|
1881
|
-
<script type="module">
|
|
1882
|
-
// ... To\xE0n b\u1ED9 ph\u1EA7n script gi\u1EEF nguy\xEAn nh\u01B0 c\u0169 ...
|
|
1883
|
-
import { mountQuizPlayer } from '${escapeAttribute(relLibraryJSPath)}';
|
|
1884
|
-
|
|
1885
|
-
function showStatusMessage(message, isError = false) {
|
|
1886
|
-
const rootEl = document.getElementById('root');
|
|
1887
|
-
if (rootEl) {
|
|
1888
|
-
rootEl.innerHTML = '';
|
|
1889
|
-
const messageEl = document.createElement('p');
|
|
1890
|
-
messageEl.textContent = message;
|
|
1891
|
-
messageEl.className = 'status-message';
|
|
1892
|
-
if(isError) messageEl.style.color = '#ef4444';
|
|
1893
|
-
rootEl.appendChild(messageEl);
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
async function main() {
|
|
1898
|
-
let quizConfigData;
|
|
1899
|
-
try {
|
|
1900
|
-
const response = await fetch('${escapeAttribute(relQuizDataPath)}');
|
|
1901
|
-
if (!response.ok) {
|
|
1902
|
-
throw new Error('Failed to load quiz data: Status ' + response.status + ' - ' + response.statusText);
|
|
1903
|
-
}
|
|
1904
|
-
quizConfigData = await response.json();
|
|
1905
|
-
} catch (error) {
|
|
1906
|
-
console.error("Error loading quiz data:", error);
|
|
1907
|
-
showStatusMessage('Error: Could not load quiz configuration. ' + error.message, true);
|
|
1908
|
-
return;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
if (typeof mountQuizPlayer !== 'function') {
|
|
1912
|
-
showStatusMessage('Error: Quiz mount function not found in the library bundle. The build might be corrupted.', true);
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
const rootElement = document.getElementById('root');
|
|
1917
|
-
if (rootElement) {
|
|
1918
|
-
rootElement.innerHTML = '';
|
|
1919
|
-
mountQuizPlayer('root', quizConfigData);
|
|
1920
|
-
} else {
|
|
1921
|
-
console.error('Critical Error: Root element (#root) not found in the DOM.');
|
|
1922
|
-
document.body.innerHTML = '<p style="color: red; text-align: center; padding: 20px;">Critical Error: Root HTML element not found.</p>';
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
if (document.readyState === 'loading') {
|
|
1927
|
-
document.addEventListener('DOMContentLoaded', main);
|
|
1928
|
-
} else {
|
|
1929
|
-
main();
|
|
1930
|
-
}
|
|
1931
|
-
</script>
|
|
1932
|
-
</body>
|
|
1933
|
-
</html>`;
|
|
1934
|
-
};
|
|
1935
|
-
|
|
1936
|
-
// src/services/SCORMManifestGenerator.ts
|
|
1937
|
-
var escapeXML = (unsafe) => {
|
|
1938
|
-
if (typeof unsafe !== "string") return "";
|
|
1939
|
-
return unsafe.replace(/[<>&'"]/g, (c) => {
|
|
1940
|
-
switch (c) {
|
|
1941
|
-
case "<":
|
|
1942
|
-
return "<";
|
|
1943
|
-
case ">":
|
|
1944
|
-
return ">";
|
|
1945
|
-
case "&":
|
|
1946
|
-
return "&";
|
|
1947
|
-
case "'":
|
|
1948
|
-
return "'";
|
|
1949
|
-
case '"':
|
|
1950
|
-
return """;
|
|
1951
|
-
default:
|
|
1952
|
-
return c;
|
|
1953
|
-
}
|
|
1954
|
-
});
|
|
1955
|
-
};
|
|
1956
|
-
var generateSCORMManifest = (quizConfig, scormVersion, launcherFile = "index.html", libraryJSPath = "scorm-bundle/player.js", quizDataPath = "quiz_data.json", blocklyCSSPath = "blockly-styles.css", mainCSSPath = "styles.css") => {
|
|
1957
|
-
var _a;
|
|
1958
|
-
const uniqueId = `iqk_${quizConfig.id.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
1959
|
-
const organizationId = `ORG-${uniqueId}`;
|
|
1960
|
-
const itemId = `ITEM-${uniqueId}`;
|
|
1961
|
-
const resourceId = `RES-${uniqueId}`;
|
|
1962
|
-
const quizTitle = escapeXML(quizConfig.title);
|
|
1963
|
-
const passingScore = (_a = quizConfig.settings) == null ? void 0 : _a.passingScorePercent;
|
|
1964
|
-
const effectiveScormVersion = scormVersion;
|
|
1965
|
-
const schemaVersion = effectiveScormVersion === "2004" ? "2004 4th Edition" : "1.2";
|
|
1966
|
-
const adlcpNamespace = effectiveScormVersion === "2004" ? "http://www.adlnet.org/xsd/adlcp_v1p3" : "http://www.adlnet.org/xsd/adlcp_rootv1p2";
|
|
1967
|
-
const imsmdNamespace = effectiveScormVersion === "2004" ? "http://www.imsglobal.org/xsd/imsmd_v1p2" : "http://www.imsglobal.org/xsd/imsmd_rootv1p2p1";
|
|
1968
|
-
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";
|
|
1969
|
-
const files = [
|
|
1970
|
-
launcherFile,
|
|
1971
|
-
libraryJSPath,
|
|
1972
|
-
quizDataPath,
|
|
1973
|
-
blocklyCSSPath,
|
|
1974
|
-
mainCSSPath
|
|
1975
|
-
].map((file) => `<file href="${escapeXML(file)}"/>`).join("\n ");
|
|
1976
|
-
const manifestHeader = effectiveScormVersion === "2004" ? `<?xml version="1.0" encoding="UTF-8"?>
|
|
1977
|
-
<manifest identifier="${uniqueId}-MANIFEST" version="1.0"
|
|
1978
|
-
xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
|
|
1979
|
-
xmlns:adlcp="${adlcpNamespace}"
|
|
1980
|
-
xmlns:imsmd="${imsmdNamespace}"
|
|
1981
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
1982
|
-
xsi:schemaLocation="${xsiSchemaLocation}">` : (
|
|
1983
|
-
// SCORM 1.2
|
|
1984
|
-
`<?xml version="1.0" encoding="UTF-8"?>
|
|
1985
|
-
<manifest identifier="${uniqueId}-MANIFEST" version="1.2"
|
|
1986
|
-
xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
|
|
1987
|
-
xmlns:adlcp="${adlcpNamespace}"
|
|
1988
|
-
xmlns:imsmd="${imsmdNamespace}"
|
|
1989
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
1990
|
-
xsi:schemaLocation="${xsiSchemaLocation}">`
|
|
1991
|
-
);
|
|
1992
|
-
const organizationStructure = effectiveScormVersion === "2004" ? `<organizations default="${organizationId}">
|
|
1993
|
-
<organization identifier="${organizationId}" structure="hierarchical">
|
|
1994
|
-
<title>${quizTitle}</title>
|
|
1995
|
-
<item identifier="${itemId}" identifierref="${resourceId}">
|
|
1996
|
-
<title>${quizTitle}</title>
|
|
1997
|
-
${passingScore !== void 0 ? `<adlcp:masteryscore>${passingScore}</adlcp:masteryscore>` : ""}
|
|
1998
|
-
</item>
|
|
1999
|
-
</organization>
|
|
2000
|
-
</organizations>` : (
|
|
2001
|
-
// SCORM 1.2
|
|
2002
|
-
`<organizations default="${organizationId}">
|
|
2003
|
-
<organization identifier="${organizationId}">
|
|
2004
|
-
<title>${quizTitle}</title>
|
|
2005
|
-
<item identifier="${itemId}" identifierref="${resourceId}" isvisible="true">
|
|
2006
|
-
<title>${quizTitle}</title>
|
|
2007
|
-
${passingScore !== void 0 ? `<adlcp:masteryscore>${passingScore}</adlcp:masteryscore>` : ""}
|
|
2008
|
-
</item>
|
|
2009
|
-
</organization>
|
|
2010
|
-
</organizations>`
|
|
2011
|
-
);
|
|
2012
|
-
const resourceScormType = effectiveScormVersion === "2004" ? "sco" : "sco";
|
|
2013
|
-
return `${manifestHeader}
|
|
2014
|
-
<metadata>
|
|
2015
|
-
<schema>ADL SCORM</schema>
|
|
2016
|
-
<schemaversion>${schemaVersion}</schemaversion>
|
|
2017
|
-
<imsmd:lom>
|
|
2018
|
-
<imsmd:general>
|
|
2019
|
-
<imsmd:title>
|
|
2020
|
-
<imsmd:langstring xml:lang="en">${quizTitle}</imsmd:langstring>
|
|
2021
|
-
</imsmd:title>
|
|
2022
|
-
${quizConfig.description ? `<imsmd:description><imsmd:langstring xml:lang="en">${escapeXML(quizConfig.description)}</imsmd:langstring></imsmd:description>` : ""}
|
|
2023
|
-
</imsmd:general>
|
|
2024
|
-
</imsmd:lom>
|
|
2025
|
-
</metadata>
|
|
2026
|
-
${organizationStructure}
|
|
2027
|
-
<resources>
|
|
2028
|
-
<resource identifier="${resourceId}" type="webcontent" adlcp:scormtype="${resourceScormType}" href="${escapeXML(launcherFile)}">
|
|
2029
|
-
${files}
|
|
2030
|
-
</resource>
|
|
2031
|
-
</resources>
|
|
2032
|
-
</manifest>`;
|
|
2033
|
-
};
|
|
2034
|
-
var sanitizeFilename = (name) => {
|
|
2035
|
-
return name.replace(/[^a-z0-9_.-]/gi, "_").toLowerCase();
|
|
2036
|
-
};
|
|
2037
|
-
var exportQuizAsSCORMZip = async (quiz, options) => {
|
|
2038
|
-
try {
|
|
2039
|
-
const zip = new JSZip__default.default();
|
|
2040
|
-
const playerJSUrlToFetch = "/static/scorm-bundle/player.js";
|
|
2041
|
-
const mainCSSUrlToFetch = "/static/scorm-bundle/styles.css";
|
|
2042
|
-
const blocklyCSSUrlToFetch = "/blockly-styles.css";
|
|
2043
|
-
const libraryJSPathInZip = "player.js";
|
|
2044
|
-
const mainCSSPathInZip = "styles.css";
|
|
2045
|
-
const blocklyCSSPathInZip = "blockly-styles.css";
|
|
2046
|
-
const quizDataPathInZip = "quiz_data.json";
|
|
2047
|
-
console.log(`Fetching Player JS from: ${playerJSUrlToFetch}`);
|
|
2048
|
-
console.log(`Fetching Main CSS from: ${mainCSSUrlToFetch}`);
|
|
2049
|
-
const [playerJSContent, mainCSSContent, blocklyCSSContent] = await Promise.all([
|
|
2050
|
-
// Fetch file JS của player
|
|
2051
|
-
fetch(playerJSUrlToFetch).then((res) => {
|
|
2052
|
-
if (!res.ok) throw new Error(`Could not fetch Player JS at ${playerJSUrlToFetch}. Make sure the file exists in your app's public folder.`);
|
|
2053
|
-
return res.text();
|
|
2054
|
-
}),
|
|
2055
|
-
// Fetch file CSS chính
|
|
2056
|
-
fetch(mainCSSUrlToFetch).then((res) => {
|
|
2057
|
-
if (!res.ok) throw new Error(`Could not fetch Main CSS at ${mainCSSUrlToFetch}. Make sure the file exists in your app's public folder.`);
|
|
2058
|
-
return res.text();
|
|
2059
|
-
}),
|
|
2060
|
-
// Fetch file CSS của Blockly (tùy chọn)
|
|
2061
|
-
fetch(blocklyCSSUrlToFetch).then((res) => {
|
|
2062
|
-
if (!res.ok) {
|
|
2063
|
-
console.warn(`Could not fetch ${blocklyCSSUrlToFetch}. This is okay if you don't use Blockly/Scratch questions.`);
|
|
2064
|
-
return "";
|
|
2065
|
-
}
|
|
2066
|
-
return res.text();
|
|
2067
|
-
})
|
|
2068
|
-
]);
|
|
2069
|
-
zip.file(libraryJSPathInZip, playerJSContent);
|
|
2070
|
-
zip.file(mainCSSPathInZip, mainCSSContent);
|
|
2071
|
-
if (blocklyCSSContent) {
|
|
2072
|
-
zip.file(blocklyCSSPathInZip, blocklyCSSContent);
|
|
2073
|
-
}
|
|
2074
|
-
const quizDataString = JSON.stringify(quiz, null, 2);
|
|
2075
|
-
zip.file(quizDataPathInZip, quizDataString);
|
|
2076
|
-
const manifestContent = generateSCORMManifest(
|
|
2077
|
-
quiz,
|
|
2078
|
-
options.scormVersion,
|
|
2079
|
-
"index.html",
|
|
2080
|
-
libraryJSPathInZip,
|
|
2081
|
-
quizDataPathInZip,
|
|
2082
|
-
blocklyCSSPathInZip,
|
|
2083
|
-
mainCSSPathInZip
|
|
2084
|
-
);
|
|
2085
|
-
zip.file("imsmanifest.xml", manifestContent);
|
|
2086
|
-
const launcherContent = generateLauncherHTML(
|
|
2087
|
-
quiz,
|
|
2088
|
-
libraryJSPathInZip,
|
|
2089
|
-
quizDataPathInZip,
|
|
2090
|
-
blocklyCSSPathInZip,
|
|
2091
|
-
mainCSSPathInZip,
|
|
2092
|
-
quiz.title
|
|
2093
|
-
);
|
|
2094
|
-
zip.file("index.html", launcherContent);
|
|
2095
|
-
const blob = await zip.generateAsync({ type: "blob" });
|
|
2096
|
-
const fileName = `${sanitizeFilename(quiz.title || "quiz")}_scorm_${options.scormVersion.replace(".", "_")}.zip`;
|
|
2097
|
-
const link = document.createElement("a");
|
|
2098
|
-
link.href = URL.createObjectURL(blob);
|
|
2099
|
-
link.download = fileName;
|
|
2100
|
-
document.body.appendChild(link);
|
|
2101
|
-
link.click();
|
|
2102
|
-
document.body.removeChild(link);
|
|
2103
|
-
URL.revokeObjectURL(link.href);
|
|
2104
|
-
return { success: true, fileName };
|
|
2105
|
-
} catch (err) {
|
|
2106
|
-
console.error("Error creating SCORM ZIP:", err);
|
|
2107
|
-
return { success: false, error: err instanceof Error ? err.message : "Unknown error during ZIP creation." };
|
|
2108
|
-
}
|
|
2109
|
-
};
|
|
2110
|
-
|
|
2111
|
-
// src/services/sampleQuiz.ts
|
|
2112
|
-
var trueFalseQ1 = {
|
|
2113
|
-
id: generateUniqueId("tfq_"),
|
|
2114
|
-
questionType: "true_false",
|
|
2115
|
-
prompt: "B\u1EA7u tr\u1EDDi c\xF3 m\xE0u xanh do hi\u1EC7n t\u01B0\u1EE3ng t\xE1n x\u1EA1 Rayleigh.",
|
|
2116
|
-
correctAnswer: true,
|
|
2117
|
-
points: 10,
|
|
2118
|
-
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.",
|
|
2119
|
-
difficulty: "easy",
|
|
2120
|
-
topic: "V\u1EADt l\xFD",
|
|
2121
|
-
category: "Khoa h\u1ECDc",
|
|
2122
|
-
learningObjective: "Hi\u1EC3u v\u1EC1 quang h\u1ECDc kh\xED quy\u1EC3n c\u01A1 b\u1EA3n."
|
|
2123
|
-
};
|
|
2124
|
-
var mcq1 = {
|
|
2125
|
-
id: generateUniqueId("mcq_"),
|
|
2126
|
-
questionType: "multiple_choice",
|
|
2127
|
-
prompt: "Th\u1EE7 \u0111\xF4 c\u1EE7a Ph\xE1p l\xE0 g\xEC?",
|
|
2128
|
-
options: [
|
|
2129
|
-
{ id: generateUniqueId("opt_"), text: "Berlin" },
|
|
2130
|
-
{ id: generateUniqueId("opt_"), text: "Madrid" },
|
|
2131
|
-
{ id: generateUniqueId("opt_"), text: "Paris" },
|
|
2132
|
-
{ id: generateUniqueId("opt_"), text: "Rome" }
|
|
2133
|
-
],
|
|
2134
|
-
correctAnswerId: "",
|
|
2135
|
-
points: 15,
|
|
2136
|
-
difficulty: "easy",
|
|
2137
|
-
topic: "\u0110\u1ECBa l\xFD",
|
|
2138
|
-
category: "Khoa h\u1ECDc X\xE3 h\u1ED9i"
|
|
2139
|
-
};
|
|
2140
|
-
var parisOption = mcq1.options.find((opt) => opt.text === "Paris");
|
|
2141
|
-
if (parisOption) {
|
|
2142
|
-
mcq1.correctAnswerId = parisOption.id;
|
|
2143
|
-
}
|
|
2144
|
-
var mrq1_opt1_id = generateUniqueId("opt_");
|
|
2145
|
-
var mrq1_opt2_id = generateUniqueId("opt_");
|
|
2146
|
-
var mrq1_opt3_id = generateUniqueId("opt_");
|
|
2147
|
-
var mrq1_opt4_id = generateUniqueId("opt_");
|
|
2148
|
-
var mrq1_opt5_id = generateUniqueId("opt_");
|
|
2149
|
-
var mrq1 = {
|
|
2150
|
-
id: generateUniqueId("mrq_"),
|
|
2151
|
-
questionType: "multiple_response",
|
|
2152
|
-
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)?",
|
|
2153
|
-
options: [
|
|
2154
|
-
{ id: mrq1_opt1_id, text: "Sao Th\u1ED5 (Saturn)" },
|
|
2155
|
-
{ id: mrq1_opt2_id, text: "Sao M\u1ED9c (Jupiter)" },
|
|
2156
|
-
{ id: mrq1_opt3_id, text: "Sao Thi\xEAn V\u01B0\u01A1ng (Uranus)" },
|
|
2157
|
-
{ id: mrq1_opt4_id, text: "Sao H\u1EA3i V\u01B0\u01A1ng (Neptune)" },
|
|
2158
|
-
{ id: mrq1_opt5_id, text: "Tr\xE1i \u0110\u1EA5t (Earth)" }
|
|
2159
|
-
],
|
|
2160
|
-
correctAnswerIds: [mrq1_opt1_id, mrq1_opt2_id, mrq1_opt3_id, mrq1_opt4_id],
|
|
2161
|
-
points: 20,
|
|
2162
|
-
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.",
|
|
2163
|
-
difficulty: "medium",
|
|
2164
|
-
topic: "Thi\xEAn v\u0103n h\u1ECDc",
|
|
2165
|
-
category: "Khoa h\u1ECDc"
|
|
2166
|
-
};
|
|
2167
|
-
var shortAnswerQ1 = {
|
|
2168
|
-
id: generateUniqueId("saq_"),
|
|
2169
|
-
questionType: "short_answer",
|
|
2170
|
-
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?",
|
|
2171
|
-
acceptedAnswers: ["JavaScript", "Javascript", "javascript", "JS", "js"],
|
|
2172
|
-
points: 10,
|
|
2173
|
-
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.",
|
|
2174
|
-
difficulty: "easy",
|
|
2175
|
-
topic: "Ph\xE1t tri\u1EC3n Web",
|
|
2176
|
-
category: "C\xF4ng ngh\u1EC7",
|
|
2177
|
-
isCaseSensitive: false
|
|
2178
|
-
};
|
|
2179
|
-
var numericQ1 = {
|
|
2180
|
-
id: generateUniqueId("nq_"),
|
|
2181
|
-
questionType: "numeric",
|
|
2182
|
-
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?",
|
|
2183
|
-
answer: 100,
|
|
2184
|
-
tolerance: 1,
|
|
2185
|
-
points: 10,
|
|
2186
|
-
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.",
|
|
2187
|
-
difficulty: "easy",
|
|
2188
|
-
topic: "H\xF3a h\u1ECDc",
|
|
2189
|
-
category: "Khoa h\u1ECDc"
|
|
2190
|
-
};
|
|
2191
|
-
var fillInTheBlanksQ1 = {
|
|
2192
|
-
id: generateUniqueId("fitb_"),
|
|
2193
|
-
questionType: "fill_in_the_blanks",
|
|
2194
|
-
prompt: "\u0110i\u1EC1n v\xE0o ch\u1ED7 tr\u1ED1ng \u0111\u1EC3 ho\xE0n th\xE0nh c\xE2u sau:",
|
|
2195
|
-
segments: [
|
|
2196
|
-
{ type: "text", content: "N\u01B0\u1EDBc \u0111\u01B0\u1EE3c c\u1EA5u t\u1EA1o t\u1EEB hai nguy\xEAn t\u1ED1 l\xE0 " },
|
|
2197
|
-
{ type: "blank", id: "fitb_h" },
|
|
2198
|
-
{ type: "text", content: " v\xE0 " },
|
|
2199
|
-
{ type: "blank", id: "fitb_o" },
|
|
2200
|
-
{ type: "text", content: "." }
|
|
2201
|
-
],
|
|
2202
|
-
answers: [
|
|
2203
|
-
{ blankId: "fitb_h", acceptedValues: ["Hydro", "Hydrogen", "H"] },
|
|
2204
|
-
{ blankId: "fitb_o", acceptedValues: ["Oxy", "Oxygen", "O"] }
|
|
2205
|
-
],
|
|
2206
|
-
isCaseSensitive: false,
|
|
2207
|
-
points: 15,
|
|
2208
|
-
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.",
|
|
2209
|
-
difficulty: "easy",
|
|
2210
|
-
topic: "H\xF3a h\u1ECDc C\u01A1 b\u1EA3n",
|
|
2211
|
-
category: "Khoa h\u1ECDc"
|
|
2212
|
-
};
|
|
2213
|
-
var sequenceQ1_item1_id = generateUniqueId("seqi_");
|
|
2214
|
-
var sequenceQ1_item2_id = generateUniqueId("seqi_");
|
|
2215
|
-
var sequenceQ1_item3_id = generateUniqueId("seqi_");
|
|
2216
|
-
var sequenceQ1_item4_id = generateUniqueId("seqi_");
|
|
2217
|
-
var sequenceQ1 = {
|
|
2218
|
-
id: generateUniqueId("seqq_"),
|
|
2219
|
-
questionType: "sequence",
|
|
2220
|
-
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:",
|
|
2221
|
-
items: [
|
|
2222
|
-
{ id: sequenceQ1_item1_id, content: "Sao H\u1ECFa (Mars)" },
|
|
2223
|
-
{ id: sequenceQ1_item2_id, content: "Tr\xE1i \u0110\u1EA5t (Earth)" },
|
|
2224
|
-
{ id: sequenceQ1_item3_id, content: "Sao Th\u1EE7y (Mercury)" },
|
|
2225
|
-
{ id: sequenceQ1_item4_id, content: "Sao Kim (Venus)" }
|
|
2226
|
-
],
|
|
2227
|
-
correctOrder: [sequenceQ1_item3_id, sequenceQ1_item4_id, sequenceQ1_item2_id, sequenceQ1_item1_id],
|
|
2228
|
-
points: 20,
|
|
2229
|
-
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.",
|
|
2230
|
-
difficulty: "medium",
|
|
2231
|
-
topic: "Thi\xEAn v\u0103n h\u1ECDc",
|
|
2232
|
-
category: "Khoa h\u1ECDc"
|
|
2233
|
-
};
|
|
2234
|
-
var matchingQ1_prompt_vn = generateUniqueId("matp_");
|
|
2235
|
-
var matchingQ1_prompt_jp = generateUniqueId("matp_");
|
|
2236
|
-
var matchingQ1_prompt_us = generateUniqueId("matp_");
|
|
2237
|
-
var matchingQ1_opt_hanoi = generateUniqueId("mato_");
|
|
2238
|
-
var matchingQ1_opt_tokyo = generateUniqueId("mato_");
|
|
2239
|
-
var matchingQ1_opt_dc = generateUniqueId("mato_");
|
|
2240
|
-
var matchingQ1 = {
|
|
2241
|
-
id: generateUniqueId("matq_"),
|
|
2242
|
-
questionType: "matching",
|
|
2243
|
-
prompt: "H\xE3y gh\xE9p m\u1ED7i qu\u1ED1c gia v\u1EDBi th\u1EE7 \u0111\xF4 t\u01B0\u01A1ng \u1EE9ng.",
|
|
2244
|
-
prompts: [
|
|
2245
|
-
{ id: matchingQ1_prompt_vn, content: "Vi\u1EC7t Nam" },
|
|
2246
|
-
{ id: matchingQ1_prompt_jp, content: "Nh\u1EADt B\u1EA3n" },
|
|
2247
|
-
{ id: matchingQ1_prompt_us, content: "Hoa K\u1EF3" }
|
|
2248
|
-
],
|
|
2249
|
-
options: [
|
|
2250
|
-
{ id: matchingQ1_opt_tokyo, content: "Tokyo" },
|
|
2251
|
-
{ id: matchingQ1_opt_hanoi, content: "H\xE0 N\u1ED9i" },
|
|
2252
|
-
{ id: matchingQ1_opt_dc, content: "Washington D.C." }
|
|
2253
|
-
],
|
|
2254
|
-
correctAnswerMap: [
|
|
2255
|
-
{ promptId: matchingQ1_prompt_vn, optionId: matchingQ1_opt_hanoi },
|
|
2256
|
-
{ promptId: matchingQ1_prompt_jp, optionId: matchingQ1_opt_tokyo },
|
|
2257
|
-
{ promptId: matchingQ1_prompt_us, optionId: matchingQ1_opt_dc }
|
|
2258
|
-
],
|
|
2259
|
-
points: 15,
|
|
2260
|
-
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.",
|
|
2261
|
-
difficulty: "easy",
|
|
2262
|
-
topic: "\u0110\u1ECBa l\xFD Th\u1EBF gi\u1EDBi",
|
|
2263
|
-
shuffleOptions: true
|
|
2264
|
-
};
|
|
2265
|
-
var dndQ1_drag_apple = generateUniqueId("dndi_");
|
|
2266
|
-
var dndQ1_drag_banana = generateUniqueId("dndi_");
|
|
2267
|
-
var dndQ1_drag_orange = generateUniqueId("dndi_");
|
|
2268
|
-
var dndQ1_drop_red = generateUniqueId("dndz_");
|
|
2269
|
-
var dndQ1_drop_yellow = generateUniqueId("dndz_");
|
|
2270
|
-
var dndQ1_drop_orange_color = generateUniqueId("dndz_");
|
|
2271
|
-
var dragAndDropQ1 = {
|
|
2272
|
-
id: generateUniqueId("dndq_"),
|
|
2273
|
-
questionType: "drag_and_drop",
|
|
2274
|
-
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).",
|
|
2275
|
-
draggableItems: [
|
|
2276
|
-
{ id: dndQ1_drag_apple, content: "T\xE1o" },
|
|
2277
|
-
{ id: dndQ1_drag_banana, content: "Chu\u1ED1i" },
|
|
2278
|
-
{ id: dndQ1_drag_orange, content: "Cam" }
|
|
2279
|
-
],
|
|
2280
|
-
dropZones: [
|
|
2281
|
-
{ id: dndQ1_drop_red, label: "Gi\u1ECF \u0110\u1ECF" },
|
|
2282
|
-
{ id: dndQ1_drop_yellow, label: "Gi\u1ECF V\xE0ng" },
|
|
2283
|
-
{ id: dndQ1_drop_orange_color, label: "Gi\u1ECF Cam" }
|
|
2284
|
-
],
|
|
2285
|
-
answerMap: [
|
|
2286
|
-
{ draggableId: dndQ1_drag_apple, dropZoneId: dndQ1_drop_red },
|
|
2287
|
-
{ draggableId: dndQ1_drag_banana, dropZoneId: dndQ1_drop_yellow },
|
|
2288
|
-
{ draggableId: dndQ1_drag_orange, dropZoneId: dndQ1_drop_orange_color }
|
|
2289
|
-
],
|
|
2290
|
-
points: 15,
|
|
2291
|
-
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).",
|
|
2292
|
-
difficulty: "easy",
|
|
2293
|
-
topic: "M\xE0u s\u1EAFc v\xE0 V\u1EADt th\u1EC3",
|
|
2294
|
-
backgroundImageUrl: "https://placehold.co/600x200.png",
|
|
2295
|
-
imageAltText: "colored baskets"
|
|
2296
|
-
};
|
|
2297
|
-
var hotspotQ1_engine_left = generateUniqueId("hs_");
|
|
2298
|
-
var hotspotQ1_engine_right = generateUniqueId("hs_");
|
|
2299
|
-
var hotspotQ1_cockpit = generateUniqueId("hs_");
|
|
2300
|
-
var hotspotQ1 = {
|
|
2301
|
-
id: generateUniqueId("hsq_"),
|
|
2302
|
-
questionType: "hotspot",
|
|
2303
|
-
prompt: "Nh\u1EA5p v\xE0o (c\xE1c) \u0111\u1ED9ng c\u01A1 c\u1EE7a m\xE1y bay trong h\xECnh.",
|
|
2304
|
-
imageUrl: "https://placehold.co/600x400.png",
|
|
2305
|
-
imageAltText: "airplane diagram",
|
|
2306
|
-
hotspots: [
|
|
2307
|
-
{ id: hotspotQ1_engine_left, shape: "rect", coords: [150, 200, 80, 60], description: "\u0110\u1ED9ng c\u01A1 b\xEAn tr\xE1i" },
|
|
2308
|
-
{ id: hotspotQ1_engine_right, shape: "rect", coords: [370, 200, 80, 60], description: "\u0110\u1ED9ng c\u01A1 b\xEAn ph\u1EA3i" },
|
|
2309
|
-
{ id: hotspotQ1_cockpit, shape: "rect", coords: [250, 120, 100, 70], description: "Bu\u1ED3ng l\xE1i" }
|
|
2310
|
-
],
|
|
2311
|
-
correctHotspotIds: [hotspotQ1_engine_left, hotspotQ1_engine_right],
|
|
2312
|
-
points: 15,
|
|
2313
|
-
explanation: "M\xE1y bay n\xE0y c\xF3 hai \u0111\u1ED9ng c\u01A1 ch\xEDnh, n\u1EB1m d\u01B0\u1EDBi c\xE1nh.",
|
|
2314
|
-
difficulty: "medium",
|
|
2315
|
-
topic: "H\xE0ng kh\xF4ng",
|
|
2316
|
-
category: "K\u1EF9 thu\u1EADt"
|
|
2317
|
-
};
|
|
2318
|
-
var blocklyQ1 = {
|
|
2319
|
-
id: generateUniqueId("blkq_"),
|
|
2320
|
-
questionType: "blockly_programming",
|
|
2321
|
-
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.",
|
|
2322
|
-
points: 25,
|
|
2323
|
-
difficulty: "easy",
|
|
2324
|
-
topic: "L\u1EADp tr\xECnh C\u01A1 b\u1EA3n",
|
|
2325
|
-
category: "C\xF4ng ngh\u1EC7 Th\xF4ng tin",
|
|
2326
|
-
toolboxDefinition: `
|
|
2327
|
-
<xml xmlns="https://developers.google.com/blockly/xml">
|
|
2328
|
-
<category name="Text" colour="%{BKY_TEXTS_HUE}">
|
|
2329
|
-
<block type="text"></block>
|
|
2330
|
-
<block type="text_print"></block>
|
|
2331
|
-
</category>
|
|
2332
|
-
</xml>
|
|
2333
|
-
`,
|
|
2334
|
-
initialWorkspace: `
|
|
2335
|
-
<xml xmlns="https://developers.google.com/blockly/xml">
|
|
2336
|
-
<block type="text_print" id="${generateUniqueId("blki_")}" x="70" y="70">
|
|
2337
|
-
<value name="TEXT">
|
|
2338
|
-
<shadow type="text" id="${generateUniqueId("blki_")}">
|
|
2339
|
-
<field name="TEXT">abc</field>
|
|
2340
|
-
</shadow>
|
|
2341
|
-
</value>
|
|
2342
|
-
</block>
|
|
2343
|
-
</xml>
|
|
2344
|
-
`,
|
|
2345
|
-
solutionWorkspaceXML: `
|
|
2346
|
-
<xml xmlns="https://developers.google.com/blockly/xml">
|
|
2347
|
-
<block type="text_print" id="${generateUniqueId("blki_solution_")}" x="70" y="70">
|
|
2348
|
-
<value name="TEXT">
|
|
2349
|
-
<block type="text" id="${generateUniqueId("blki_text_solution_")}">
|
|
2350
|
-
<field name="TEXT">Hello, World!</field>
|
|
2351
|
-
</block>
|
|
2352
|
-
</value>
|
|
2353
|
-
</block>
|
|
2354
|
-
</xml>
|
|
2355
|
-
`,
|
|
2356
|
-
solutionGeneratedCode: "window.alert('Hello, World!');",
|
|
2357
|
-
// Normalized JS code
|
|
2358
|
-
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!'."
|
|
2359
|
-
};
|
|
2360
|
-
var scratchQ1 = {
|
|
2361
|
-
id: generateUniqueId("scrq_"),
|
|
2362
|
-
questionType: "scratch_programming",
|
|
2363
|
-
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.",
|
|
2364
|
-
points: 20,
|
|
2365
|
-
difficulty: "easy",
|
|
2366
|
-
topic: "L\u1EADp tr\xECnh Scratch",
|
|
2367
|
-
category: "C\xF4ng ngh\u1EC7 Th\xF4ng tin",
|
|
2368
|
-
toolboxDefinition: `
|
|
2369
|
-
<xml xmlns="https://developers.google.com/blockly/xml">
|
|
2370
|
-
<category name="Motion" colour="#4C97FF">
|
|
2371
|
-
<block type="motion_movesteps"></block>
|
|
2372
|
-
</category>
|
|
2373
|
-
<category name="Events" colour="#FFBF00">
|
|
2374
|
-
<block type="event_whenflagclicked"></block>
|
|
2375
|
-
</category>
|
|
2376
|
-
</xml>
|
|
2377
|
-
`,
|
|
2378
|
-
initialWorkspace: `
|
|
2379
|
-
<xml xmlns="https://developers.google.com/blockly/xml"></xml>
|
|
2380
|
-
`,
|
|
2381
|
-
solutionWorkspaceXML: `
|
|
2382
|
-
<xml xmlns="https://developers.google.com/blockly/xml">
|
|
2383
|
-
<block type="event_whenflagclicked" id="${generateUniqueId("scr_event_")}" x="50" y="50">
|
|
2384
|
-
<next>
|
|
2385
|
-
<block type="motion_movesteps" id="${generateUniqueId("scr_motion_")}">
|
|
2386
|
-
<value name="STEPS">
|
|
2387
|
-
<shadow type="math_number">
|
|
2388
|
-
<field name="NUM">10</field>
|
|
2389
|
-
</shadow>
|
|
2390
|
-
</value>
|
|
2391
|
-
</block>
|
|
2392
|
-
</next>
|
|
2393
|
-
</block>
|
|
2394
|
-
</xml>
|
|
2395
|
-
`,
|
|
2396
|
-
solutionGeneratedCode: "whenGreenFlagClicked(() => { move(10); });",
|
|
2397
|
-
// Example pseudo-code or JS representation
|
|
2398
|
-
explanation: "S\u1EED d\u1EE5ng kh\u1ED1i 'when green flag clicked' t\u1EEB Events v\xE0 kh\u1ED1i 'move 10 steps' t\u1EEB Motion."
|
|
2399
|
-
};
|
|
2400
|
-
var sampleQuiz = {
|
|
2401
|
-
id: "sample-quiz-001",
|
|
2402
|
-
title: "Sample Quiz for Testers",
|
|
2403
|
-
description: "A short quiz with a few different question types to test the QuizKit functionality.",
|
|
2404
|
-
questions: [
|
|
2405
|
-
trueFalseQ1,
|
|
2406
|
-
mcq1,
|
|
2407
|
-
mrq1,
|
|
2408
|
-
shortAnswerQ1,
|
|
2409
|
-
numericQ1,
|
|
2410
|
-
fillInTheBlanksQ1,
|
|
2411
|
-
sequenceQ1,
|
|
2412
|
-
matchingQ1,
|
|
2413
|
-
dragAndDropQ1,
|
|
2414
|
-
hotspotQ1,
|
|
2415
|
-
blocklyQ1,
|
|
2416
|
-
scratchQ1
|
|
2417
|
-
// Added Scratch question
|
|
2418
|
-
],
|
|
2419
|
-
settings: {
|
|
2420
|
-
shuffleQuestions: true,
|
|
2421
|
-
shuffleOptions: true,
|
|
2422
|
-
showCorrectAnswers: "end_of_quiz",
|
|
2423
|
-
passingScorePercent: 70,
|
|
2424
|
-
timeLimitMinutes: 25
|
|
2425
|
-
}
|
|
2426
|
-
};
|
|
2427
|
-
var emptyQuiz = {
|
|
2428
|
-
id: generateUniqueId("quiz_"),
|
|
2429
|
-
title: "New Quiz",
|
|
2430
|
-
description: "",
|
|
2431
|
-
questions: [],
|
|
2432
|
-
settings: {
|
|
2433
|
-
language: "English",
|
|
2434
|
-
// <-- ĐÃ THÊM
|
|
2435
|
-
shuffleQuestions: false,
|
|
2436
|
-
shuffleOptions: false,
|
|
2437
|
-
showCorrectAnswers: "end_of_quiz",
|
|
2438
|
-
passingScorePercent: 0,
|
|
2439
|
-
timeLimitMinutes: 0
|
|
2440
|
-
}
|
|
2441
|
-
};
|
|
2442
|
-
function cn(...inputs) {
|
|
2443
|
-
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
exports.APIKeyService = APIKeyService;
|
|
2447
|
-
exports.GEMINI_API_KEY_SERVICE_NAME = GEMINI_API_KEY_SERVICE_NAME;
|
|
2448
|
-
exports.QuestionImportService = QuestionImportService;
|
|
2449
|
-
exports.QuizEditorService = QuizEditorService;
|
|
2450
|
-
exports.QuizEngine = QuizEngine;
|
|
2451
|
-
exports.SCORMService = SCORMService;
|
|
2452
|
-
exports.cn = cn;
|
|
2453
|
-
exports.emptyQuiz = emptyQuiz;
|
|
2454
|
-
exports.exportQuizAsSCORMZip = exportQuizAsSCORMZip;
|
|
2455
|
-
exports.generateLauncherHTML = generateLauncherHTML;
|
|
2456
|
-
exports.generateSCORMManifest = generateSCORMManifest;
|
|
2457
|
-
exports.generateUniqueId = generateUniqueId;
|
|
2458
|
-
exports.sampleQuiz = sampleQuiz;
|