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

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.
@@ -0,0 +1,2942 @@
1
+ 'use strict';
2
+
3
+ var React25 = require('react');
4
+ var ReactDOM = require('react-dom/client');
5
+ var RadioGroupPrimitive = require('@radix-ui/react-radio-group');
6
+ var lucideReact = require('lucide-react');
7
+ var clsx = require('clsx');
8
+ var tailwindMerge = require('tailwind-merge');
9
+ var LabelPrimitive = require('@radix-ui/react-label');
10
+ var classVarianceAuthority = require('class-variance-authority');
11
+ var ReactMarkdown = require('react-markdown');
12
+ var remarkGfm = require('remark-gfm');
13
+ var rehypeHighlight = require('rehype-highlight');
14
+ var remarkMath = require('remark-math');
15
+ var rehypeKatex = require('rehype-katex');
16
+ var CheckboxPrimitive = require('@radix-ui/react-checkbox');
17
+ var reactSlot = require('@radix-ui/react-slot');
18
+ var SelectPrimitive = require('@radix-ui/react-select');
19
+ var ProgressPrimitive = require('@radix-ui/react-progress');
20
+ var AccordionPrimitive = require('@radix-ui/react-accordion');
21
+ var ScrollAreaPrimitive = require('@radix-ui/react-scroll-area');
22
+
23
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
24
+
25
+ function _interopNamespace(e) {
26
+ if (e && e.__esModule) return e;
27
+ var n = Object.create(null);
28
+ if (e) {
29
+ Object.keys(e).forEach(function (k) {
30
+ if (k !== 'default') {
31
+ var d = Object.getOwnPropertyDescriptor(e, k);
32
+ Object.defineProperty(n, k, d.get ? d : {
33
+ enumerable: true,
34
+ get: function () { return e[k]; }
35
+ });
36
+ }
37
+ });
38
+ }
39
+ n.default = e;
40
+ return Object.freeze(n);
41
+ }
42
+
43
+ var React25__namespace = /*#__PURE__*/_interopNamespace(React25);
44
+ var ReactDOM__default = /*#__PURE__*/_interopDefault(ReactDOM);
45
+ var RadioGroupPrimitive__namespace = /*#__PURE__*/_interopNamespace(RadioGroupPrimitive);
46
+ var LabelPrimitive__namespace = /*#__PURE__*/_interopNamespace(LabelPrimitive);
47
+ var ReactMarkdown__default = /*#__PURE__*/_interopDefault(ReactMarkdown);
48
+ var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
49
+ var rehypeHighlight__default = /*#__PURE__*/_interopDefault(rehypeHighlight);
50
+ var remarkMath__default = /*#__PURE__*/_interopDefault(remarkMath);
51
+ var rehypeKatex__default = /*#__PURE__*/_interopDefault(rehypeKatex);
52
+ var CheckboxPrimitive__namespace = /*#__PURE__*/_interopNamespace(CheckboxPrimitive);
53
+ var SelectPrimitive__namespace = /*#__PURE__*/_interopNamespace(SelectPrimitive);
54
+ var ProgressPrimitive__namespace = /*#__PURE__*/_interopNamespace(ProgressPrimitive);
55
+ var AccordionPrimitive__namespace = /*#__PURE__*/_interopNamespace(AccordionPrimitive);
56
+ var ScrollAreaPrimitive__namespace = /*#__PURE__*/_interopNamespace(ScrollAreaPrimitive);
57
+
58
+ var __defProp = Object.defineProperty;
59
+ var __defProps = Object.defineProperties;
60
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
61
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
62
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
63
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
64
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
65
+ var __spreadValues = (a, b) => {
66
+ for (var prop in b || (b = {}))
67
+ if (__hasOwnProp.call(b, prop))
68
+ __defNormalProp(a, prop, b[prop]);
69
+ if (__getOwnPropSymbols)
70
+ for (var prop of __getOwnPropSymbols(b)) {
71
+ if (__propIsEnum.call(b, prop))
72
+ __defNormalProp(a, prop, b[prop]);
73
+ }
74
+ return a;
75
+ };
76
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
77
+ var __objRest = (source, exclude) => {
78
+ var target = {};
79
+ for (var prop in source)
80
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
81
+ target[prop] = source[prop];
82
+ if (source != null && __getOwnPropSymbols)
83
+ for (var prop of __getOwnPropSymbols(source)) {
84
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
85
+ target[prop] = source[prop];
86
+ }
87
+ return target;
88
+ };
89
+
90
+ // src/services/SCORMService.ts
91
+ var SCORM_TRUE = "true";
92
+ var SCORM_NO_ERROR = "0";
93
+ var CMI_CORE_LESSON_STATUS_PASSED = "passed";
94
+ var CMI_CORE_LESSON_STATUS_FAILED = "failed";
95
+ var CMI_CORE_LESSON_STATUS_COMPLETED = "completed";
96
+ var CMI_CORE_LESSON_STATUS_INCOMPLETE = "incomplete";
97
+ var CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED = "not attempted";
98
+ var CMI_COMPLETION_STATUS_COMPLETED = "completed";
99
+ var CMI_COMPLETION_STATUS_INCOMPLETE = "incomplete";
100
+ var CMI_SUCCESS_STATUS_PASSED = "passed";
101
+ var CMI_SUCCESS_STATUS_FAILED = "failed";
102
+ var SCORMService = class {
103
+ constructor(settings) {
104
+ this.scormAPI = null;
105
+ this.scormVersionFound = null;
106
+ this.isInitialized = false;
107
+ this.isTerminated = false;
108
+ this.studentName = null;
109
+ this.settings = __spreadValues({
110
+ setCompletionOnFinish: true,
111
+ setSuccessOnPass: true,
112
+ autoCommit: true
113
+ }, settings);
114
+ if (typeof window !== "undefined") {
115
+ this._findAPI();
116
+ }
117
+ }
118
+ _findAPIRecursive(win) {
119
+ if (win === null) return null;
120
+ if (win.API_1484_11) {
121
+ this.scormVersionFound = "2004";
122
+ return win.API_1484_11;
123
+ }
124
+ if (win.API) {
125
+ this.scormVersionFound = "1.2";
126
+ return win.API;
127
+ }
128
+ if (win.parent && win.parent !== win) {
129
+ return this._findAPIRecursive(win.parent);
130
+ }
131
+ if (win.opener && typeof win.opener !== "undefined" && win.opener !== win && win.opener !== win.parent) {
132
+ try {
133
+ if (win.opener.document) {
134
+ return this._findAPIRecursive(win.opener);
135
+ }
136
+ } catch (e) {
137
+ console.warn("SCORMService: Could not access win.opener for API search due to cross-origin restrictions.");
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ _findAPI() {
143
+ try {
144
+ this.scormAPI = this._findAPIRecursive(window);
145
+ if (this.scormAPI) {
146
+ if (!this.scormVersionFound) this.scormVersionFound = this.settings.version;
147
+ console.log(`SCORMService: API Found. Version determined: ${this.scormVersionFound}`);
148
+ } else {
149
+ console.warn("SCORMService: SCORM API not found in window hierarchy.");
150
+ }
151
+ } catch (e) {
152
+ console.error("SCORMService: Error finding SCORM API", e);
153
+ this.scormAPI = null;
154
+ }
155
+ }
156
+ hasAPI() {
157
+ return this.scormAPI !== null;
158
+ }
159
+ getSCORMVersion() {
160
+ return this.scormVersionFound;
161
+ }
162
+ initialize() {
163
+ if (!this.hasAPI()) return { success: false, error: "SCORM API not found." };
164
+ if (this.isInitialized) return { success: true, studentName: this.studentName || void 0 };
165
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.Initialize("") : this.scormAPI.LMSInitialize("");
166
+ if (result.toString() === SCORM_TRUE || result === true) {
167
+ this.isInitialized = true;
168
+ this.isTerminated = false;
169
+ const studentNameVar = this.settings.studentNameVar || (this.scormVersionFound === "2004" ? "cmi.learner_name" : "cmi.core.student_name");
170
+ this.studentName = this.getValue(studentNameVar);
171
+ if (this.scormVersionFound === "2004") {
172
+ const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
173
+ if (this.getValue(completionStatusVar) === "not attempted") {
174
+ this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
175
+ }
176
+ } else {
177
+ const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
178
+ if (this.getValue(lessonStatusVar) === CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED) {
179
+ this.setValue(lessonStatusVar, CMI_CORE_LESSON_STATUS_INCOMPLETE);
180
+ }
181
+ }
182
+ if (this.settings.autoCommit) this.commit();
183
+ return { success: true, studentName: this.studentName || void 0 };
184
+ } else {
185
+ const error = this.getLastError();
186
+ return { success: false, error: `Initialization failed: ${error.message}` };
187
+ }
188
+ }
189
+ terminate() {
190
+ if (!this.hasAPI() || !this.isInitialized || this.isTerminated) {
191
+ const reason = !this.hasAPI() ? "API not found" : !this.isInitialized ? "Not initialized" : "Already terminated";
192
+ return { success: !this.hasAPI() || this.isTerminated, error: this.isTerminated ? void 0 : reason };
193
+ }
194
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.Terminate("") : this.scormAPI.LMSFinish("");
195
+ if (result.toString() === SCORM_TRUE || result === true) {
196
+ this.isTerminated = true;
197
+ this.isInitialized = false;
198
+ return { success: true };
199
+ } else {
200
+ const error = this.getLastError();
201
+ return { success: false, error: `Termination failed: ${error.message}` };
202
+ }
203
+ }
204
+ setValue(element, value) {
205
+ if (!this.hasAPI() || !this.isInitialized) {
206
+ return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
207
+ }
208
+ const valStr = value.toString();
209
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.SetValue(element, valStr) : this.scormAPI.LMSSetValue(element, valStr);
210
+ if (result.toString() === SCORM_TRUE || result === true) {
211
+ if (this.settings.autoCommit) this.commit();
212
+ return { success: true };
213
+ } else {
214
+ const error = this.getLastError();
215
+ return { success: false, error: `SetValue failed for ${element}: ${error.message}` };
216
+ }
217
+ }
218
+ getValue(element) {
219
+ var _a;
220
+ if (!this.hasAPI() || !this.isInitialized) return null;
221
+ const value = this.scormVersionFound === "2004" ? this.scormAPI.GetValue(element) : this.scormAPI.LMSGetValue(element);
222
+ const error = this.getLastError();
223
+ if (error.code !== SCORM_NO_ERROR && error.code !== "403" && error.code !== "0") {
224
+ console.warn(`SCORMService: GetValue for ${element} produced an error ${error.code}: ${error.message}. Returning raw value:`, value);
225
+ }
226
+ return (_a = value == null ? void 0 : value.toString()) != null ? _a : null;
227
+ }
228
+ commit() {
229
+ if (!this.hasAPI() || !this.isInitialized) {
230
+ return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
231
+ }
232
+ const result = this.scormVersionFound === "2004" ? this.scormAPI.Commit("") : this.scormAPI.LMSCommit("");
233
+ if (result.toString() === SCORM_TRUE || result === true) {
234
+ return { success: true };
235
+ } else {
236
+ const error = this.getLastError();
237
+ return { success: false, error: `Commit failed: ${error.message}` };
238
+ }
239
+ }
240
+ setScore(rawScore, maxScore, minScore = 0) {
241
+ if (!this.hasAPI() || !this.isInitialized) return;
242
+ if (this.scormVersionFound === "2004") {
243
+ const scoreRawVar = this.settings.scoreRawVar_2004 || this.settings.scoreRawVar || "cmi.score.raw";
244
+ const scoreMaxVar = this.settings.scoreMaxVar_2004 || this.settings.scoreMaxVar || "cmi.score.max";
245
+ const scoreMinVar = this.settings.scoreMinVar_2004 || this.settings.scoreMinVar || "cmi.score.min";
246
+ const scoreScaledVar = this.settings.scoreScaledVar_2004 || "cmi.score.scaled";
247
+ this.setValue(scoreMinVar, minScore);
248
+ this.setValue(scoreMaxVar, maxScore);
249
+ this.setValue(scoreRawVar, rawScore);
250
+ if (maxScore > minScore) {
251
+ const scaledScore = (rawScore - minScore) / (maxScore - minScore);
252
+ this.setValue(scoreScaledVar, parseFloat(scaledScore.toFixed(4)));
253
+ } else if (maxScore === minScore && maxScore !== 0) {
254
+ this.setValue(scoreScaledVar, rawScore >= maxScore ? 1 : 0);
255
+ } else {
256
+ this.setValue(scoreScaledVar, 0);
257
+ }
258
+ } else {
259
+ const scoreRawVar = this.settings.scoreRawVar_1_2 || this.settings.scoreRawVar || "cmi.core.score.raw";
260
+ const scoreMaxVar = this.settings.scoreMaxVar_1_2 || this.settings.scoreMaxVar || "cmi.core.score.max";
261
+ const scoreMinVar = this.settings.scoreMinVar_1_2 || this.settings.scoreMinVar || "cmi.core.score.min";
262
+ this.setValue(scoreMinVar, minScore);
263
+ this.setValue(scoreMaxVar, maxScore);
264
+ this.setValue(scoreRawVar, rawScore);
265
+ }
266
+ }
267
+ setLessonStatus(status, passed) {
268
+ if (!this.hasAPI() || !this.isInitialized) return;
269
+ if (this.scormVersionFound === "2004") {
270
+ const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
271
+ const successStatusVar = this.settings.successStatusVar_2004 || "cmi.success_status";
272
+ if (this.settings.setCompletionOnFinish && (status === "completed" || status === "passed" || status === "failed")) {
273
+ this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_COMPLETED);
274
+ } else if (status === "incomplete" || status === "browsed") {
275
+ this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
276
+ }
277
+ if (this.settings.setSuccessOnPass && passed !== void 0) {
278
+ this.setValue(successStatusVar, passed ? CMI_SUCCESS_STATUS_PASSED : CMI_SUCCESS_STATUS_FAILED);
279
+ }
280
+ } else {
281
+ const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
282
+ let finalStatus = status;
283
+ if (this.settings.setCompletionOnFinish) {
284
+ if (this.settings.setSuccessOnPass && passed !== void 0) {
285
+ finalStatus = passed ? CMI_CORE_LESSON_STATUS_PASSED : CMI_CORE_LESSON_STATUS_FAILED;
286
+ } else {
287
+ finalStatus = CMI_CORE_LESSON_STATUS_COMPLETED;
288
+ }
289
+ } else {
290
+ if (status === CMI_CORE_LESSON_STATUS_PASSED || status === CMI_CORE_LESSON_STATUS_FAILED) ; else {
291
+ finalStatus = CMI_CORE_LESSON_STATUS_INCOMPLETE;
292
+ }
293
+ }
294
+ this.setValue(lessonStatusVar, finalStatus);
295
+ }
296
+ }
297
+ getLastError() {
298
+ var _a, _b;
299
+ if (!this.hasAPI()) return { code: "-1", message: "SCORM API not found." };
300
+ const errorCode = this.scormVersionFound === "2004" ? this.scormAPI.GetLastError() : this.scormAPI.LMSGetLastError();
301
+ if (errorCode === SCORM_NO_ERROR || errorCode === 0 || errorCode === "0") {
302
+ return { code: SCORM_NO_ERROR, message: "No error." };
303
+ }
304
+ const errorMessage = this.scormVersionFound === "2004" ? this.scormAPI.GetErrorString(errorCode.toString()) : this.scormAPI.LMSGetErrorString(errorCode.toString());
305
+ const diagnostic = this.scormVersionFound === "2004" ? this.scormAPI.GetDiagnostic(errorCode.toString()) : this.scormAPI.LMSGetDiagnostic(errorCode.toString());
306
+ return {
307
+ code: errorCode.toString(),
308
+ message: (_a = errorMessage == null ? void 0 : errorMessage.toString()) != null ? _a : "Unknown error.",
309
+ diagnostic: (_b = diagnostic == null ? void 0 : diagnostic.toString()) != null ? _b : void 0
310
+ };
311
+ }
312
+ formatCMITime(totalSeconds) {
313
+ const pad = (num, size = 2) => num.toString().padStart(size, "0");
314
+ if (this.scormVersionFound === "2004") {
315
+ const hours = Math.floor(totalSeconds / 3600);
316
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
317
+ const seconds = parseFloat((totalSeconds % 60).toFixed(2));
318
+ let timeString = "PT";
319
+ if (hours > 0) timeString += `${hours}H`;
320
+ if (minutes > 0 || hours > 0 && seconds > 0) {
321
+ timeString += `${minutes}M`;
322
+ }
323
+ if (seconds > 0 || timeString === "PT") {
324
+ timeString += `${seconds}S`;
325
+ }
326
+ return timeString === "PT" ? "PT0S" : timeString;
327
+ } else {
328
+ const hours = Math.floor(totalSeconds / 3600);
329
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
330
+ const secondsOnly = Math.floor(totalSeconds % 60);
331
+ const centiseconds = Math.floor((totalSeconds - Math.floor(totalSeconds)) * 100);
332
+ return `${pad(hours, 4)}:${pad(minutes)}:${pad(secondsOnly)}.${pad(centiseconds)}`;
333
+ }
334
+ }
335
+ };
336
+
337
+ // src/services/QuizEngine.ts
338
+ var QuizEngine = class {
339
+ constructor(options) {
340
+ this.userAnswers = /* @__PURE__ */ new Map();
341
+ this.currentQuestionIndex = 0;
342
+ this.timerId = null;
343
+ this.timeLeftInSeconds = null;
344
+ this.scormService = null;
345
+ this.quizResultState = { scormStatus: "idle" };
346
+ this.questionStartTime = null;
347
+ this.questionTimings = /* @__PURE__ */ new Map();
348
+ var _a, _b, _c, _d, _e;
349
+ this.config = options.config;
350
+ this.callbacks = options.callbacks || {};
351
+ this.questions = ((_a = this.config.settings) == null ? void 0 : _a.shuffleQuestions) ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
352
+ this.overallStartTime = Date.now();
353
+ if (((_b = this.config.settings) == null ? void 0 : _b.timeLimitMinutes) && this.config.settings.timeLimitMinutes > 0) {
354
+ this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
355
+ }
356
+ if ((_c = this.config.settings) == null ? void 0 : _c.scorm) {
357
+ this.quizResultState.scormStatus = "initializing";
358
+ this.scormService = new SCORMService(this.config.settings.scorm);
359
+ if (this.scormService.hasAPI()) {
360
+ const initResult = this.scormService.initialize();
361
+ if (initResult.success) {
362
+ this.quizResultState.scormStatus = "initialized";
363
+ this.quizResultState.studentName = initResult.studentName;
364
+ } else {
365
+ this.quizResultState.scormStatus = "error";
366
+ this.quizResultState.scormError = initResult.error || "SCORM initialization failed.";
367
+ }
368
+ } else {
369
+ this.quizResultState.scormStatus = "no_api";
370
+ }
371
+ }
372
+ const initialQ = this.getCurrentQuestion();
373
+ if (initialQ) {
374
+ this.questionStartTime = Date.now();
375
+ }
376
+ if (this.callbacks.onQuizStart) {
377
+ this.callbacks.onQuizStart({
378
+ initialQuestion: initialQ,
379
+ currentQuestionNumber: this.getCurrentQuestionNumber(),
380
+ totalQuestions: this.getTotalQuestions(),
381
+ timeLimitInSeconds: this.timeLeftInSeconds,
382
+ scormStatus: this.quizResultState.scormStatus,
383
+ studentName: this.quizResultState.studentName
384
+ });
385
+ }
386
+ if (this.timeLeftInSeconds !== null) {
387
+ this.startTimer();
388
+ }
389
+ (_e = (_d = this.callbacks).onQuestionChange) == null ? void 0 : _e.call(_d, initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
390
+ }
391
+ _recordCurrentQuestionTime() {
392
+ if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
393
+ const currentQId = this.questions[this.currentQuestionIndex].id;
394
+ const elapsedMs = Date.now() - this.questionStartTime;
395
+ const currentTotalTime = this.questionTimings.get(currentQId) || 0;
396
+ this.questionTimings.set(currentQId, currentTotalTime + elapsedMs / 1e3);
397
+ }
398
+ this.questionStartTime = null;
399
+ }
400
+ startTimer() {
401
+ if (this.timerId !== null) clearInterval(this.timerId);
402
+ this.timerId = setInterval(() => this.handleTick(), 1e3);
403
+ }
404
+ stopTimer() {
405
+ if (this.timerId !== null) {
406
+ clearInterval(this.timerId);
407
+ this.timerId = null;
408
+ }
409
+ }
410
+ handleTick() {
411
+ var _a, _b, _c, _d;
412
+ if (this.timeLeftInSeconds === null) return;
413
+ if (this.timeLeftInSeconds > 0) {
414
+ this.timeLeftInSeconds--;
415
+ (_b = (_a = this.callbacks).onTimeTick) == null ? void 0 : _b.call(_a, this.timeLeftInSeconds);
416
+ }
417
+ if (this.timeLeftInSeconds <= 0) {
418
+ this.stopTimer();
419
+ (_d = (_c = this.callbacks).onQuizTimeUp) == null ? void 0 : _d.call(_c);
420
+ this.calculateResults();
421
+ }
422
+ }
423
+ getTimeLeftInSeconds() {
424
+ return this.timeLeftInSeconds;
425
+ }
426
+ getCurrentQuestion() {
427
+ return this.questions[this.currentQuestionIndex] || null;
428
+ }
429
+ getCurrentQuestionNumber() {
430
+ return this.currentQuestionIndex + 1;
431
+ }
432
+ getTotalQuestions() {
433
+ return this.questions.length;
434
+ }
435
+ getUserAnswer(questionId) {
436
+ return this.userAnswers.get(questionId);
437
+ }
438
+ isQuizFinished() {
439
+ return this.quizResultState.score !== void 0;
440
+ }
441
+ submitAnswer(questionId, answer) {
442
+ var _a, _b;
443
+ this.userAnswers.set(questionId, answer);
444
+ const question = this.questions.find((q) => q.id === questionId);
445
+ if (question) (_b = (_a = this.callbacks).onAnswerSubmit) == null ? void 0 : _b.call(_a, question, answer);
446
+ }
447
+ nextQuestion() {
448
+ var _a, _b;
449
+ this._recordCurrentQuestionTime();
450
+ if (this.currentQuestionIndex < this.questions.length - 1) {
451
+ this.currentQuestionIndex++;
452
+ const currentQ = this.getCurrentQuestion();
453
+ this.questionStartTime = Date.now();
454
+ (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
455
+ return currentQ;
456
+ }
457
+ return null;
458
+ }
459
+ previousQuestion() {
460
+ var _a, _b;
461
+ this._recordCurrentQuestionTime();
462
+ if (this.currentQuestionIndex > 0) {
463
+ this.currentQuestionIndex--;
464
+ const currentQ = this.getCurrentQuestion();
465
+ this.questionStartTime = Date.now();
466
+ (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
467
+ return currentQ;
468
+ }
469
+ return null;
470
+ }
471
+ goToQuestion(index) {
472
+ var _a, _b;
473
+ if (index >= 0 && index < this.questions.length && index !== this.currentQuestionIndex) {
474
+ this._recordCurrentQuestionTime();
475
+ this.currentQuestionIndex = index;
476
+ const currentQ = this.getCurrentQuestion();
477
+ this.questionStartTime = Date.now();
478
+ (_b = (_a = this.callbacks).onQuestionChange) == null ? void 0 : _b.call(_a, currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
479
+ return currentQ;
480
+ }
481
+ return this.getCurrentQuestion();
482
+ }
483
+ evaluateQuestion(question, answer) {
484
+ var _a, _b, _c;
485
+ let isCorrect = false;
486
+ let correctAnswerDetail = null;
487
+ const points = (_a = question.points) != null ? _a : 0;
488
+ const findOptionText = (q, id) => {
489
+ var _a2;
490
+ return ((_a2 = q.options.find((opt) => opt.id === id)) == null ? void 0 : _a2.text) || "";
491
+ };
492
+ switch (question.questionType) {
493
+ case "multiple_choice": {
494
+ const q = question;
495
+ const correctAnswerId = q.correctAnswerId;
496
+ const correctValue = findOptionText(q, correctAnswerId);
497
+ correctAnswerDetail = { id: correctAnswerId, value: correctValue };
498
+ isCorrect = answer === correctAnswerId;
499
+ break;
500
+ }
501
+ case "multiple_response": {
502
+ const q = question;
503
+ const correctAnswerIds = q.correctAnswerIds;
504
+ const correctValues = correctAnswerIds.map((id) => findOptionText(q, id));
505
+ correctAnswerDetail = { id: correctAnswerIds, value: correctValues };
506
+ if (Array.isArray(answer)) {
507
+ const userAnswerSet = new Set(answer);
508
+ const correctAnswerSet = new Set(correctAnswerIds);
509
+ isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
510
+ }
511
+ break;
512
+ }
513
+ case "true_false": {
514
+ const q = question;
515
+ correctAnswerDetail = { id: null, value: q.correctAnswer };
516
+ let tfAnswer = answer;
517
+ if (typeof answer === "string") tfAnswer = answer.toLowerCase() === "true";
518
+ isCorrect = typeof tfAnswer === "boolean" && tfAnswer === q.correctAnswer;
519
+ break;
520
+ }
521
+ case "short_answer": {
522
+ const q = question;
523
+ correctAnswerDetail = { id: null, value: q.acceptedAnswers };
524
+ if (typeof answer === "string") {
525
+ const userAnswerTrimmed = answer.trim();
526
+ const caseSensitive = (_b = q.isCaseSensitive) != null ? _b : false;
527
+ isCorrect = q.acceptedAnswers.some((accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase());
528
+ }
529
+ break;
530
+ }
531
+ case "numeric": {
532
+ const q = question;
533
+ correctAnswerDetail = { id: null, value: q.answer };
534
+ if (typeof answer === "string" || typeof answer === "number") {
535
+ const userAnswerNum = parseFloat(String(answer));
536
+ if (!isNaN(userAnswerNum)) {
537
+ isCorrect = q.tolerance != null ? Math.abs(userAnswerNum - q.answer) <= q.tolerance : userAnswerNum === q.answer;
538
+ }
539
+ }
540
+ break;
541
+ }
542
+ case "sequence": {
543
+ const q = question;
544
+ const correctValues = q.correctOrder.map((id) => {
545
+ var _a2;
546
+ return ((_a2 = q.items.find((item) => item.id === id)) == null ? void 0 : _a2.content) || "";
547
+ });
548
+ correctAnswerDetail = { id: q.correctOrder, value: correctValues };
549
+ if (Array.isArray(answer) && answer.length === q.correctOrder.length) {
550
+ isCorrect = answer.every((itemId, index) => itemId === q.correctOrder[index]);
551
+ }
552
+ break;
553
+ }
554
+ case "matching": {
555
+ const q = question;
556
+ const correctMap = q.correctAnswerMap.reduce((acc, curr) => {
557
+ var _a2, _b2;
558
+ const promptText = ((_a2 = q.prompts.find((p) => p.id === curr.promptId)) == null ? void 0 : _a2.content) || "";
559
+ const optionText = ((_b2 = q.options.find((o) => o.id === curr.optionId)) == null ? void 0 : _b2.content) || "";
560
+ acc[promptText] = optionText;
561
+ return acc;
562
+ }, {});
563
+ correctAnswerDetail = { id: null, value: correctMap };
564
+ if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
565
+ const userAnswerMap = answer;
566
+ isCorrect = q.correctAnswerMap.length === Object.keys(userAnswerMap).length && q.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
567
+ }
568
+ break;
569
+ }
570
+ case "fill_in_the_blanks": {
571
+ const q = question;
572
+ const correctMap = q.answers.reduce((acc, curr) => {
573
+ acc[curr.blankId] = curr.acceptedValues.join(" | ");
574
+ return acc;
575
+ }, {});
576
+ correctAnswerDetail = { id: null, value: correctMap };
577
+ if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
578
+ const userAnswerMap = answer;
579
+ isCorrect = q.answers.every((correctAnsDef) => {
580
+ var _a2, _b2;
581
+ const userValForBlank = (_a2 = userAnswerMap[correctAnsDef.blankId]) == null ? void 0 : _a2.trim();
582
+ if (userValForBlank === void 0) return false;
583
+ const caseSensitive = (_b2 = q.isCaseSensitive) != null ? _b2 : false;
584
+ return correctAnsDef.acceptedValues.some((accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase());
585
+ });
586
+ }
587
+ break;
588
+ }
589
+ case "drag_and_drop": {
590
+ const q = question;
591
+ const correctMap = q.answerMap.reduce((acc, curr) => {
592
+ var _a2, _b2;
593
+ const draggableText = ((_a2 = q.draggableItems.find((d) => d.id === curr.draggableId)) == null ? void 0 : _a2.content) || "";
594
+ const dropZoneText = ((_b2 = q.dropZones.find((z) => z.id === curr.dropZoneId)) == null ? void 0 : _b2.label) || "";
595
+ acc[draggableText] = dropZoneText;
596
+ return acc;
597
+ }, {});
598
+ correctAnswerDetail = { id: null, value: correctMap };
599
+ if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
600
+ const userAnswerMap = answer;
601
+ isCorrect = q.answerMap.length === Object.keys(userAnswerMap).length && q.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
602
+ }
603
+ break;
604
+ }
605
+ case "hotspot": {
606
+ const q = question;
607
+ const correctValues = q.correctHotspotIds.map((id) => {
608
+ var _a2;
609
+ return ((_a2 = q.hotspots.find((h) => h.id === id)) == null ? void 0 : _a2.description) || id;
610
+ });
611
+ correctAnswerDetail = { id: q.correctHotspotIds, value: correctValues };
612
+ if (Array.isArray(answer)) {
613
+ const userAnswerSet = new Set(answer);
614
+ const correctAnswerSet = new Set(q.correctHotspotIds);
615
+ isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
616
+ }
617
+ break;
618
+ }
619
+ case "blockly_programming":
620
+ case "scratch_programming": {
621
+ const q = question;
622
+ correctAnswerDetail = { id: null, value: q.solutionGeneratedCode || "" };
623
+ if (typeof answer === "string" && typeof q.solutionGeneratedCode === "string") {
624
+ if (typeof window !== "undefined" && ((_c = window.Blockly) == null ? void 0 : _c.JavaScript)) {
625
+ const LocalBlockly = window.Blockly;
626
+ let generatedUserCode = "";
627
+ try {
628
+ const tempWorkspace = new LocalBlockly.Workspace();
629
+ const dom = LocalBlockly.Xml.textToDom(answer);
630
+ LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
631
+ generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
632
+ const normalize = (code) => code.replace(/\s+/g, " ").trim();
633
+ isCorrect = normalize(generatedUserCode) === normalize(q.solutionGeneratedCode);
634
+ tempWorkspace.dispose();
635
+ } catch (e) {
636
+ console.error(`Error generating code from user's ${q.questionType} XML for evaluation:`, e);
637
+ isCorrect = false;
638
+ }
639
+ } else {
640
+ console.warn(`Blockly library not available in QuizEngine for ${q.questionType} code generation during evaluation. Skipping code comparison.`);
641
+ isCorrect = false;
642
+ }
643
+ }
644
+ break;
645
+ }
646
+ default: {
647
+ const _exhaustiveCheck = question;
648
+ console.warn("Unsupported question type in QuizEngine evaluation:", _exhaustiveCheck);
649
+ isCorrect = false;
650
+ correctAnswerDetail = { id: null, value: "Evaluation not implemented." };
651
+ }
652
+ }
653
+ return { isCorrect, correctAnswer: correctAnswerDetail, pointsEarned: isCorrect ? points : 0 };
654
+ }
655
+ async calculateResults() {
656
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
657
+ this.stopTimer();
658
+ this._recordCurrentQuestionTime();
659
+ let totalScore = 0;
660
+ let maxScore = 0;
661
+ const questionResultsArray = [];
662
+ let accumulatedTotalTimeSpent = 0;
663
+ for (const question of this.questions) {
664
+ const userAnswerRaw = this.userAnswers.get(question.id) || null;
665
+ maxScore += (_a = question.points) != null ? _a : 0;
666
+ const { isCorrect, correctAnswer: correctAnswerDetail, pointsEarned } = this.evaluateQuestion(question, userAnswerRaw);
667
+ totalScore += pointsEarned;
668
+ const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
669
+ accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
670
+ let userAnswerDetail = null;
671
+ let allOptions = void 0;
672
+ if (userAnswerRaw !== null) {
673
+ switch (question.questionType) {
674
+ case "multiple_choice": {
675
+ const q = question;
676
+ allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
677
+ const id = userAnswerRaw;
678
+ userAnswerDetail = { id, value: ((_b = allOptions.find((opt) => opt.id === id)) == null ? void 0 : _b.value) || "" };
679
+ break;
680
+ }
681
+ case "multiple_response": {
682
+ const q = question;
683
+ allOptions = q.options.map((opt) => ({ id: opt.id, value: opt.text }));
684
+ const ids = userAnswerRaw;
685
+ const values = ids.map((id) => {
686
+ var _a2;
687
+ return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
688
+ });
689
+ userAnswerDetail = { id: ids, value: values };
690
+ break;
691
+ }
692
+ case "true_false":
693
+ case "short_answer":
694
+ case "numeric":
695
+ userAnswerDetail = { id: null, value: userAnswerRaw };
696
+ break;
697
+ case "sequence": {
698
+ const q = question;
699
+ allOptions = q.items.map((item) => ({ id: item.id, value: item.content }));
700
+ const ids = userAnswerRaw;
701
+ const values = ids.map((id) => {
702
+ var _a2;
703
+ return ((_a2 = allOptions == null ? void 0 : allOptions.find((opt) => opt.id === id)) == null ? void 0 : _a2.value) || "";
704
+ });
705
+ userAnswerDetail = { id: ids, value: values };
706
+ break;
707
+ }
708
+ case "matching": {
709
+ const q = question;
710
+ const userAnswerMap = userAnswerRaw;
711
+ const valueMap = {};
712
+ for (const promptId in userAnswerMap) {
713
+ const optionId = userAnswerMap[promptId];
714
+ const promptText = ((_c = q.prompts.find((p) => p.id === promptId)) == null ? void 0 : _c.content) || "";
715
+ const optionText = ((_d = q.options.find((o) => o.id === optionId)) == null ? void 0 : _d.content) || "";
716
+ valueMap[promptText] = optionText;
717
+ }
718
+ userAnswerDetail = { id: null, value: valueMap };
719
+ break;
720
+ }
721
+ // --- LOGIC MỚI ĐƯỢC THÊM VÀO ---
722
+ case "fill_in_the_blanks": {
723
+ if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
724
+ userAnswerDetail = { id: null, value: userAnswerRaw };
725
+ }
726
+ break;
727
+ }
728
+ case "drag_and_drop": {
729
+ const q = question;
730
+ if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
731
+ const userAnswerMapByIds = userAnswerRaw;
732
+ const enrichedUserAnswerMap = {};
733
+ for (const draggableId in userAnswerMapByIds) {
734
+ const dropZoneId = userAnswerMapByIds[draggableId];
735
+ const draggableText = ((_e = q.draggableItems.find((d) => d.id === draggableId)) == null ? void 0 : _e.content) || `(ID: ${draggableId})`;
736
+ const dropZoneText = ((_f = q.dropZones.find((z) => z.id === dropZoneId)) == null ? void 0 : _f.label) || `(ID: ${dropZoneId})`;
737
+ enrichedUserAnswerMap[draggableText] = dropZoneText;
738
+ }
739
+ userAnswerDetail = { id: null, value: enrichedUserAnswerMap };
740
+ }
741
+ break;
742
+ }
743
+ // ------------------------------------
744
+ // Các loại câu hỏi còn lại vẫn giữ fallback
745
+ case "hotspot":
746
+ case "blockly_programming":
747
+ case "scratch_programming":
748
+ userAnswerDetail = { id: null, value: userAnswerRaw };
749
+ break;
750
+ }
751
+ }
752
+ questionResultsArray.push({
753
+ questionId: question.id,
754
+ questionType: question.questionType,
755
+ prompt: question.prompt,
756
+ isCorrect,
757
+ pointsEarned,
758
+ userAnswer: userAnswerDetail,
759
+ correctAnswer: correctAnswerDetail,
760
+ allOptions,
761
+ timeSpentSeconds: timeSpentOnThisQuestion
762
+ });
763
+ }
764
+ const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
765
+ let passed = void 0;
766
+ if (((_g = this.config.settings) == null ? void 0 : _g.passingScorePercent) != null) {
767
+ passed = percentage >= this.config.settings.passingScorePercent;
768
+ }
769
+ const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
770
+ const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
771
+ const metadataPerformance = this._calculateMetadataPerformance();
772
+ const finalResults = __spreadValues({
773
+ score: totalScore,
774
+ maxScore,
775
+ percentage,
776
+ answers: this.userAnswers,
777
+ questionResults: questionResultsArray,
778
+ passed,
779
+ webhookStatus: "idle",
780
+ scormStatus: this.quizResultState.scormStatus || "idle",
781
+ scormError: this.quizResultState.scormError,
782
+ studentName: this.quizResultState.studentName,
783
+ totalTimeSpentSeconds: totalQuizTimeSpentSeconds,
784
+ averageTimePerQuestionSeconds
785
+ }, metadataPerformance);
786
+ this.quizResultState = __spreadValues(__spreadValues({}, this.quizResultState), finalResults);
787
+ if ((_h = this.config.settings) == null ? void 0 : _h.scorm) this._sendResultsToSCORM(finalResults);
788
+ await this._sendResultsToWebhook(finalResults);
789
+ (_j = (_i = this.callbacks).onQuizFinish) == null ? void 0 : _j.call(_i, finalResults);
790
+ return finalResults;
791
+ }
792
+ async _sendResultsToWebhook(results) {
793
+ var _a;
794
+ if (!((_a = this.config.settings) == null ? void 0 : _a.webhookUrl)) {
795
+ results.webhookStatus = "idle";
796
+ return;
797
+ }
798
+ results.webhookStatus = "sending";
799
+ try {
800
+ const response = await fetch(this.config.settings.webhookUrl, {
801
+ method: "POST",
802
+ headers: { "Content-Type": "application/json" },
803
+ body: JSON.stringify(results)
804
+ });
805
+ if (response.ok) {
806
+ results.webhookStatus = "success";
807
+ } else {
808
+ results.webhookStatus = "error";
809
+ results.webhookError = `Webhook returned status: ${response.status} ${response.statusText}`;
810
+ try {
811
+ const errorBody = await response.text();
812
+ results.webhookError += ` - Body: ${errorBody.substring(0, 200)}`;
813
+ } catch (e) {
814
+ }
815
+ }
816
+ } catch (error) {
817
+ results.webhookStatus = "error";
818
+ results.webhookError = error instanceof Error ? `Fetch error: ${error.message}` : "Unknown webhook error.";
819
+ }
820
+ }
821
+ _sendResultsToSCORM(results) {
822
+ var _a, _b, _c, _d, _e, _f, _g;
823
+ if (!this.scormService || !this.scormService.hasAPI() || this.quizResultState.scormStatus === "no_api") {
824
+ results.scormStatus = this.quizResultState.scormStatus || "idle";
825
+ return;
826
+ }
827
+ if (this.quizResultState.scormStatus === "error" && ((_a = this.quizResultState.scormError) == null ? void 0 : _a.includes("initialization failed"))) {
828
+ results.scormStatus = "error";
829
+ results.scormError = this.quizResultState.scormError;
830
+ return;
831
+ }
832
+ results.scormStatus = "sending_data";
833
+ try {
834
+ this.scormService.setScore(results.score, results.maxScore, 0);
835
+ let lessonStatusSetting = "completed";
836
+ if (((_b = this.config.settings) == null ? void 0 : _b.passingScorePercent) !== void 0 && ((_c = this.config.settings) == null ? void 0 : _c.passingScorePercent) !== null) {
837
+ lessonStatusSetting = results.passed ? "passed" : "failed";
838
+ } else if ((_e = (_d = this.config.settings) == null ? void 0 : _d.scorm) == null ? void 0 : _e.setCompletionOnFinish) {
839
+ lessonStatusSetting = "completed";
840
+ }
841
+ this.scormService.setLessonStatus(lessonStatusSetting, results.passed);
842
+ if (results.totalTimeSpentSeconds !== void 0 && this.scormService.formatCMITime) {
843
+ const cmiTime = this.scormService.formatCMITime(results.totalTimeSpentSeconds);
844
+ 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");
845
+ if (sessionTimeVar) this.scormService.setValue(sessionTimeVar, cmiTime);
846
+ }
847
+ const commitResult = this.scormService.commit();
848
+ if (commitResult.success) {
849
+ results.scormStatus = "committed";
850
+ } else {
851
+ results.scormStatus = "error";
852
+ results.scormError = commitResult.error || "SCORM commit failed.";
853
+ }
854
+ } catch (e) {
855
+ results.scormStatus = "error";
856
+ results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
857
+ }
858
+ }
859
+ _calculateMetadataPerformance() {
860
+ const loPerformanceMap = /* @__PURE__ */ new Map();
861
+ const categoryPerformanceMap = /* @__PURE__ */ new Map();
862
+ const topicPerformanceMap = /* @__PURE__ */ new Map();
863
+ const difficultyPerformanceMap = /* @__PURE__ */ new Map();
864
+ const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
865
+ const updateMap = (map, key, points, isCorrect) => {
866
+ if (!key) return;
867
+ const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
868
+ current.totalQuestions++;
869
+ current.maxPoints += points;
870
+ if (isCorrect) {
871
+ current.correctQuestions++;
872
+ current.pointsEarned += points;
873
+ }
874
+ map.set(key, current);
875
+ };
876
+ this.questions.forEach((q) => {
877
+ var _a;
878
+ const qResult = this.userAnswers.get(q.id);
879
+ const { isCorrect } = this.evaluateQuestion(q, qResult || null);
880
+ const pointsForThisQuestion = (_a = q.points) != null ? _a : 0;
881
+ updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
882
+ updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
883
+ updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
884
+ updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
885
+ updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
886
+ });
887
+ const formatPerformanceArray = (map, keyName) => {
888
+ return Array.from(map.entries()).map(([key, data]) => ({
889
+ [keyName]: key,
890
+ totalQuestions: data.totalQuestions,
891
+ correctQuestions: data.correctQuestions,
892
+ pointsEarned: data.pointsEarned,
893
+ maxPoints: data.maxPoints,
894
+ percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
895
+ }));
896
+ };
897
+ return {
898
+ performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
899
+ performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
900
+ performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
901
+ performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
902
+ performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
903
+ };
904
+ }
905
+ getElapsedTime() {
906
+ return Date.now() - this.overallStartTime;
907
+ }
908
+ destroy() {
909
+ this.stopTimer();
910
+ this._recordCurrentQuestionTime();
911
+ if (this.scormService && this.scormService.hasAPI()) {
912
+ if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
913
+ const termResult = this.scormService.terminate();
914
+ if (termResult.success) {
915
+ this.quizResultState.scormStatus = "terminated";
916
+ } else {
917
+ this.quizResultState.scormStatus = "error";
918
+ this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
919
+ }
920
+ }
921
+ }
922
+ this.scormService = null;
923
+ }
924
+ };
925
+ function cn(...inputs) {
926
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
927
+ }
928
+
929
+ // src/react-ui/components/elements/radio-group.tsx
930
+ var RadioGroup = React25__namespace.forwardRef((_a, ref) => {
931
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
932
+ return /* @__PURE__ */ React25__namespace.createElement(
933
+ RadioGroupPrimitive__namespace.Root,
934
+ __spreadProps(__spreadValues({
935
+ className: cn("grid gap-2", className)
936
+ }, props), {
937
+ ref
938
+ })
939
+ );
940
+ });
941
+ RadioGroup.displayName = RadioGroupPrimitive__namespace.Root.displayName;
942
+ var RadioGroupItem = React25__namespace.forwardRef((_a, ref) => {
943
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
944
+ return /* @__PURE__ */ React25__namespace.createElement(
945
+ RadioGroupPrimitive__namespace.Item,
946
+ __spreadValues({
947
+ ref,
948
+ className: cn(
949
+ "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
950
+ className
951
+ )
952
+ }, props),
953
+ /* @__PURE__ */ React25__namespace.createElement(RadioGroupPrimitive__namespace.Indicator, { className: "flex items-center justify-center" }, /* @__PURE__ */ React25__namespace.createElement(lucideReact.Circle, { className: "h-2.5 w-2.5 fill-current text-current" }))
954
+ );
955
+ });
956
+ RadioGroupItem.displayName = RadioGroupPrimitive__namespace.Item.displayName;
957
+ var labelVariants = classVarianceAuthority.cva(
958
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
959
+ );
960
+ var Label = React25__namespace.forwardRef((_a, ref) => {
961
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
962
+ return /* @__PURE__ */ React25__namespace.createElement(
963
+ LabelPrimitive__namespace.Root,
964
+ __spreadValues({
965
+ ref,
966
+ className: cn(labelVariants(), className)
967
+ }, props)
968
+ );
969
+ });
970
+ Label.displayName = LabelPrimitive__namespace.Root.displayName;
971
+ var Card = React25__namespace.forwardRef((_a, ref) => {
972
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
973
+ return /* @__PURE__ */ React25__namespace.createElement(
974
+ "div",
975
+ __spreadValues({
976
+ ref,
977
+ className: cn(
978
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
979
+ className
980
+ )
981
+ }, props)
982
+ );
983
+ });
984
+ Card.displayName = "Card";
985
+ var CardHeader = React25__namespace.forwardRef((_a, ref) => {
986
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
987
+ return /* @__PURE__ */ React25__namespace.createElement(
988
+ "div",
989
+ __spreadValues({
990
+ ref,
991
+ className: cn("flex flex-col space-y-1.5 p-6", className)
992
+ }, props)
993
+ );
994
+ });
995
+ CardHeader.displayName = "CardHeader";
996
+ var CardTitle = React25__namespace.forwardRef((_a, ref) => {
997
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
998
+ return /* @__PURE__ */ React25__namespace.createElement(
999
+ "div",
1000
+ __spreadValues({
1001
+ ref,
1002
+ className: cn(
1003
+ "text-2xl font-semibold leading-none tracking-tight",
1004
+ className
1005
+ )
1006
+ }, props)
1007
+ );
1008
+ });
1009
+ CardTitle.displayName = "CardTitle";
1010
+ var CardDescription = React25__namespace.forwardRef((_a, ref) => {
1011
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1012
+ return /* @__PURE__ */ React25__namespace.createElement(
1013
+ "div",
1014
+ __spreadValues({
1015
+ ref,
1016
+ className: cn("text-sm text-muted-foreground", className)
1017
+ }, props)
1018
+ );
1019
+ });
1020
+ CardDescription.displayName = "CardDescription";
1021
+ var CardContent = React25__namespace.forwardRef((_a, ref) => {
1022
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1023
+ return /* @__PURE__ */ React25__namespace.createElement("div", __spreadValues({ ref, className: cn("p-6 pt-0", className) }, props));
1024
+ });
1025
+ CardContent.displayName = "CardContent";
1026
+ var CardFooter = React25__namespace.forwardRef((_a, ref) => {
1027
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1028
+ return /* @__PURE__ */ React25__namespace.createElement(
1029
+ "div",
1030
+ __spreadValues({
1031
+ ref,
1032
+ className: cn("flex items-center p-6 pt-0", className)
1033
+ }, props)
1034
+ );
1035
+ });
1036
+ CardFooter.displayName = "CardFooter";
1037
+ var MarkdownRenderer = ({
1038
+ content,
1039
+ className
1040
+ }) => {
1041
+ if (!content) {
1042
+ return null;
1043
+ }
1044
+ const getVideoId = (url) => {
1045
+ try {
1046
+ const urlObj = new URL(url);
1047
+ if (urlObj.hostname.includes("youtube.com") || urlObj.hostname.includes("youtu.be")) {
1048
+ const videoId = urlObj.hostname.includes("youtu.be") ? urlObj.pathname.split("/").pop() : urlObj.searchParams.get("v");
1049
+ return { platform: "youtube", id: videoId ? videoId : null };
1050
+ }
1051
+ if (urlObj.hostname.includes("vimeo.com")) {
1052
+ const videoId = urlObj.pathname.split("/").pop();
1053
+ return { platform: "vimeo", id: videoId ? videoId : null };
1054
+ }
1055
+ } catch (e) {
1056
+ }
1057
+ return { platform: null, id: null };
1058
+ };
1059
+ const processContentForVideos = (text) => {
1060
+ const videoUrlRegex = /(^|\s)((https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/|vimeo\.com\/)[^\s]+)($|\s)/g;
1061
+ return text.replace(
1062
+ videoUrlRegex,
1063
+ (match, preWhitespace, url, _p3, _p4, postWhitespace) => {
1064
+ const replacement = `
1065
+
1066
+ ![Embedded Video](${url.trim()})
1067
+
1068
+ `;
1069
+ return `${preWhitespace.replace(
1070
+ /\n/g,
1071
+ ""
1072
+ )}${replacement}${postWhitespace.replace(/\n/g, "")}`;
1073
+ }
1074
+ );
1075
+ };
1076
+ const processedContent = processContentForVideos(content);
1077
+ return (
1078
+ // Using Tailwind Typography for beautiful default styling of markdown content
1079
+ /* @__PURE__ */ React25__namespace.default.createElement("div", { className: `prose dark:prose-invert max-w-none ${className}` }, /* @__PURE__ */ React25__namespace.default.createElement(
1080
+ ReactMarkdown__default.default,
1081
+ {
1082
+ remarkPlugins: [remarkGfm__default.default, remarkMath__default.default],
1083
+ rehypePlugins: [rehypeHighlight__default.default, rehypeKatex__default.default],
1084
+ components: {
1085
+ // Override the default image component to handle videos and responsive images
1086
+ img: (_a) => {
1087
+ var _b = _a, { node } = _b, props = __objRest(_b, ["node"]);
1088
+ const src = props.src || "";
1089
+ const { platform, id } = getVideoId(src);
1090
+ if (platform && id) {
1091
+ const videoSrc = platform === "youtube" ? `https://www.youtube.com/embed/${id}` : `https://player.vimeo.com/video/${id}`;
1092
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "aspect-w-16 aspect-h-9 my-4" }, /* @__PURE__ */ React25__namespace.default.createElement(
1093
+ "iframe",
1094
+ {
1095
+ src: videoSrc,
1096
+ title: props.alt || "Embedded video",
1097
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
1098
+ allowFullScreen: true,
1099
+ className: "w-full h-full rounded-md"
1100
+ }
1101
+ ));
1102
+ }
1103
+ return (
1104
+ // eslint-disable-next-line @next/next/no-img-element
1105
+ /* @__PURE__ */ React25__namespace.default.createElement(
1106
+ "img",
1107
+ __spreadProps(__spreadValues({}, props), {
1108
+ style: {
1109
+ maxWidth: "100%",
1110
+ height: "auto",
1111
+ borderRadius: "0.5rem",
1112
+ margin: "1rem 0"
1113
+ },
1114
+ alt: props.alt || ""
1115
+ })
1116
+ )
1117
+ );
1118
+ },
1119
+ // Override the default table to add responsive wrapper
1120
+ table: (_c) => {
1121
+ var _d = _c, { node } = _d, props = __objRest(_d, ["node"]);
1122
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React25__namespace.default.createElement("table", __spreadProps(__spreadValues({}, props), { className: "my-4 w-full text-sm" })));
1123
+ },
1124
+ // Override default blockquote for better styling
1125
+ blockquote: (_e) => {
1126
+ var _f = _e, { node } = _f, props = __objRest(_f, ["node"]);
1127
+ return /* @__PURE__ */ React25__namespace.default.createElement(
1128
+ "blockquote",
1129
+ __spreadProps(__spreadValues({}, props), {
1130
+ className: "border-l-4 border-primary bg-muted/50 p-4 my-4 italic"
1131
+ })
1132
+ );
1133
+ }
1134
+ }
1135
+ },
1136
+ processedContent
1137
+ ))
1138
+ );
1139
+ };
1140
+
1141
+ // src/react-ui/components/ui/MultipleChoiceQuestionUI.tsx
1142
+ var MultipleChoiceQuestionUI = ({
1143
+ question,
1144
+ onAnswerChange,
1145
+ userAnswer,
1146
+ showCorrectAnswer = false
1147
+ }) => {
1148
+ const { prompt, options, points, explanation, correctAnswerId, id: questionId } = question;
1149
+ const handleSelection = (value) => {
1150
+ onAnswerChange(value);
1151
+ };
1152
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React25__namespace.default.createElement(
1153
+ RadioGroup,
1154
+ {
1155
+ value: userAnswer || void 0,
1156
+ onValueChange: handleSelection,
1157
+ className: "space-y-3",
1158
+ "aria-labelledby": `question-prompt-${questionId}`
1159
+ },
1160
+ options.map((option) => {
1161
+ const isSelected = userAnswer === option.id;
1162
+ const isCorrect = option.id === correctAnswerId;
1163
+ let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary";
1164
+ if (showCorrectAnswer) {
1165
+ if (isCorrect) {
1166
+ itemClassName += " border-green-500 bg-green-500/10";
1167
+ } else if (isSelected && !isCorrect) {
1168
+ itemClassName += " border-destructive bg-destructive/10";
1169
+ } else {
1170
+ itemClassName += " border-muted";
1171
+ }
1172
+ } else {
1173
+ itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
1174
+ }
1175
+ return /* @__PURE__ */ React25__namespace.default.createElement(
1176
+ Label,
1177
+ {
1178
+ key: option.id,
1179
+ htmlFor: option.id,
1180
+ className: itemClassName
1181
+ },
1182
+ /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(RadioGroupItem, { value: option.id, id: option.id, className: "mr-3" }), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "text-base flex-1" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: option.text })))
1183
+ );
1184
+ })
1185
+ ), showCorrectAnswer && explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
1186
+ };
1187
+ var TrueFalseQuestionUI = ({
1188
+ question,
1189
+ onAnswerChange,
1190
+ userAnswer,
1191
+ showCorrectAnswer = false
1192
+ }) => {
1193
+ const { prompt, points, explanation, correctAnswer, id: questionId } = question;
1194
+ const options = [
1195
+ { id: `true-${questionId}`, label: "True", value: "true" },
1196
+ { id: `false-${questionId}`, label: "False", value: "false" }
1197
+ ];
1198
+ const handleSelection = (value) => {
1199
+ onAnswerChange(value);
1200
+ };
1201
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React25__namespace.default.createElement(
1202
+ RadioGroup,
1203
+ {
1204
+ value: userAnswer || void 0,
1205
+ onValueChange: handleSelection,
1206
+ className: "space-y-3",
1207
+ "aria-labelledby": `question-prompt-${questionId}`
1208
+ },
1209
+ options.map((option) => {
1210
+ const isSelected = userAnswer === option.value;
1211
+ const isOptionCorrect = option.value === "true" && correctAnswer === true || option.value === "false" && correctAnswer === false;
1212
+ let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary";
1213
+ if (showCorrectAnswer) {
1214
+ if (isOptionCorrect) {
1215
+ itemClassName += " border-green-500 bg-green-500/10";
1216
+ } else if (isSelected && !isOptionCorrect) {
1217
+ itemClassName += " border-destructive bg-destructive/10";
1218
+ } else {
1219
+ itemClassName += " border-muted";
1220
+ }
1221
+ } else {
1222
+ itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
1223
+ }
1224
+ return /* @__PURE__ */ React25__namespace.default.createElement(
1225
+ Label,
1226
+ {
1227
+ key: option.id,
1228
+ htmlFor: option.id,
1229
+ className: itemClassName
1230
+ },
1231
+ /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(RadioGroupItem, { value: option.value, id: option.id, className: "mr-3" }), /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "text-base" }, option.label))
1232
+ );
1233
+ })
1234
+ ), showCorrectAnswer && explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
1235
+ };
1236
+ var Checkbox = React25__namespace.forwardRef((_a, ref) => {
1237
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1238
+ return /* @__PURE__ */ React25__namespace.createElement(
1239
+ CheckboxPrimitive__namespace.Root,
1240
+ __spreadValues({
1241
+ ref,
1242
+ className: cn(
1243
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
1244
+ className
1245
+ )
1246
+ }, props),
1247
+ /* @__PURE__ */ React25__namespace.createElement(
1248
+ CheckboxPrimitive__namespace.Indicator,
1249
+ {
1250
+ className: cn("flex items-center justify-center text-current")
1251
+ },
1252
+ /* @__PURE__ */ React25__namespace.createElement(lucideReact.Check, { className: "h-4 w-4" })
1253
+ )
1254
+ );
1255
+ });
1256
+ Checkbox.displayName = CheckboxPrimitive__namespace.Root.displayName;
1257
+
1258
+ // src/react-ui/components/ui/MultipleResponseQuestionUI.tsx
1259
+ var MultipleResponseQuestionUI = ({
1260
+ question,
1261
+ onAnswerChange,
1262
+ userAnswer,
1263
+ showCorrectAnswer = false
1264
+ }) => {
1265
+ const { prompt, options, points, explanation, correctAnswerIds, id: questionId } = question;
1266
+ const handleSelectionChange = (optionId, checked) => {
1267
+ const currentAnswers = Array.isArray(userAnswer) ? [...userAnswer] : [];
1268
+ let newAnswers;
1269
+ if (checked) {
1270
+ if (!currentAnswers.includes(optionId)) {
1271
+ newAnswers = [...currentAnswers, optionId];
1272
+ } else {
1273
+ newAnswers = currentAnswers;
1274
+ }
1275
+ } else {
1276
+ newAnswers = currentAnswers.filter((id) => id !== optionId);
1277
+ }
1278
+ onAnswerChange(newAnswers.length > 0 ? newAnswers : null);
1279
+ };
1280
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "space-y-3", role: "group", "aria-labelledby": `question-prompt-${questionId}` }, options.map((option) => {
1281
+ const isSelected = Array.isArray(userAnswer) && userAnswer.includes(option.id);
1282
+ const isCorrectOption = correctAnswerIds.includes(option.id);
1283
+ let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary flex items-center";
1284
+ if (showCorrectAnswer) {
1285
+ if (isCorrectOption) {
1286
+ itemClassName += isSelected ? " border-green-500 bg-green-500/10" : " border-green-500";
1287
+ } else {
1288
+ itemClassName += isSelected ? " border-destructive bg-destructive/10" : " border-muted";
1289
+ }
1290
+ } else {
1291
+ itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
1292
+ }
1293
+ return /* @__PURE__ */ React25__namespace.default.createElement(
1294
+ Label,
1295
+ {
1296
+ key: option.id,
1297
+ htmlFor: option.id,
1298
+ className: itemClassName
1299
+ },
1300
+ /* @__PURE__ */ React25__namespace.default.createElement(
1301
+ Checkbox,
1302
+ {
1303
+ id: option.id,
1304
+ checked: isSelected,
1305
+ onCheckedChange: (checked) => handleSelectionChange(option.id, !!checked),
1306
+ className: "mr-3",
1307
+ "aria-label": option.text.replace(/<[^>]*>?/gm, "")
1308
+ }
1309
+ ),
1310
+ /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "text-base flex-1" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: option.text }))
1311
+ );
1312
+ })), showCorrectAnswer && explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" }))));
1313
+ };
1314
+ var Input = React25__namespace.forwardRef(
1315
+ (_a, ref) => {
1316
+ var _b = _a, { className, type } = _b, props = __objRest(_b, ["className", "type"]);
1317
+ return /* @__PURE__ */ React25__namespace.createElement(
1318
+ "input",
1319
+ __spreadValues({
1320
+ type,
1321
+ className: cn(
1322
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
1323
+ className
1324
+ ),
1325
+ ref
1326
+ }, props)
1327
+ );
1328
+ }
1329
+ );
1330
+ Input.displayName = "Input";
1331
+
1332
+ // src/react-ui/components/ui/ShortAnswerQuestionUI.tsx
1333
+ var ShortAnswerQuestionUI = ({
1334
+ question,
1335
+ onAnswerChange,
1336
+ userAnswer,
1337
+ showCorrectAnswer = false
1338
+ }) => {
1339
+ const { prompt, points, explanation, acceptedAnswers, id: questionId, isCaseSensitive } = question;
1340
+ const handleInputChange = (event) => {
1341
+ onAnswerChange(event.target.value || null);
1342
+ };
1343
+ const displayUserAnswer = userAnswer || "";
1344
+ let isActuallyCorrect = false;
1345
+ if (showCorrectAnswer && userAnswer) {
1346
+ const userAnswerTrimmed = userAnswer.trim();
1347
+ isActuallyCorrect = acceptedAnswers.some(
1348
+ (accAns) => isCaseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
1349
+ );
1350
+ }
1351
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React25__namespace.default.createElement(Label, { htmlFor: `short-answer-input-${questionId}`, className: "sr-only" }, "Your Answer"), /* @__PURE__ */ React25__namespace.default.createElement(
1352
+ Input,
1353
+ {
1354
+ id: `short-answer-input-${questionId}`,
1355
+ type: "text",
1356
+ value: displayUserAnswer,
1357
+ onChange: handleInputChange,
1358
+ placeholder: "Type your answer here...",
1359
+ "aria-describedby": explanation ? `explanation-${questionId}` : void 0,
1360
+ className: `
1361
+ ${showCorrectAnswer && userAnswer ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
1362
+ `
1363
+ }
1364
+ ), showCorrectAnswer && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, userAnswer && !isActuallyCorrect && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-destructive" }, "Your answer was marked incorrect."), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Accepted Answers:"), /* @__PURE__ */ React25__namespace.default.createElement("ul", { className: "list-disc list-inside text-sm text-accent-foreground/80" }, acceptedAnswers.map((ans, idx) => /* @__PURE__ */ React25__namespace.default.createElement("li", { key: idx }, ans))), isCaseSensitive && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs text-muted-foreground mt-1" }, "(Case-sensitive)")), explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
1365
+ };
1366
+ var NumericQuestionUI = ({
1367
+ question,
1368
+ onAnswerChange,
1369
+ userAnswer,
1370
+ showCorrectAnswer = false
1371
+ }) => {
1372
+ const { prompt, points, explanation, answer: correctAnswerValue, tolerance, id: questionId } = question;
1373
+ const handleInputChange = (event) => {
1374
+ const value = event.target.value;
1375
+ if (value === "" || /^-?\d*\.?\d*$/.test(value)) {
1376
+ onAnswerChange(value === "" ? null : value);
1377
+ }
1378
+ };
1379
+ const displayUserAnswer = userAnswer || "";
1380
+ let isActuallyCorrect = false;
1381
+ if (showCorrectAnswer && userAnswer !== null && userAnswer !== "") {
1382
+ const userAnswerNum = parseFloat(String(userAnswer));
1383
+ if (!isNaN(userAnswerNum)) {
1384
+ isActuallyCorrect = tolerance !== void 0 && tolerance !== null ? Math.abs(userAnswerNum - correctAnswerValue) <= tolerance : userAnswerNum === correctAnswerValue;
1385
+ }
1386
+ }
1387
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React25__namespace.default.createElement(Label, { htmlFor: `numeric-input-${questionId}`, className: "sr-only" }, "Your Answer"), /* @__PURE__ */ React25__namespace.default.createElement(
1388
+ Input,
1389
+ {
1390
+ id: `numeric-input-${questionId}`,
1391
+ type: "text",
1392
+ inputMode: "numeric",
1393
+ pattern: "[0-9]*\\.?[0-9]*",
1394
+ value: displayUserAnswer,
1395
+ onChange: handleInputChange,
1396
+ placeholder: "Enter a number",
1397
+ "aria-describedby": explanation ? `explanation-${questionId}` : void 0,
1398
+ className: `
1399
+ ${showCorrectAnswer && userAnswer !== null && userAnswer !== "" ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
1400
+ `
1401
+ }
1402
+ ), showCorrectAnswer && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, userAnswer !== null && userAnswer !== "" && !isActuallyCorrect && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-destructive" }, "Your answer was marked incorrect."), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Correct Answer:"), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-accent-foreground/80" }, correctAnswerValue, tolerance !== void 0 && tolerance !== null && tolerance > 0 && ` (Tolerance: \xB1${tolerance}, Accepted range: ${correctAnswerValue - tolerance} to ${correctAnswerValue + tolerance})`)), explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
1403
+ };
1404
+ var FillInTheBlanksQuestionUI = ({
1405
+ question,
1406
+ onAnswerChange,
1407
+ userAnswer,
1408
+ showCorrectAnswer = false
1409
+ }) => {
1410
+ const { prompt, segments, answers: correctAnswersMap, points, explanation, id: questionId, isCaseSensitive } = question;
1411
+ const [userInputs, setUserInputs] = React25.useState({});
1412
+ React25.useEffect(() => {
1413
+ if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
1414
+ setUserInputs(userAnswer);
1415
+ } else {
1416
+ const initialInputs = {};
1417
+ segments.forEach((segment) => {
1418
+ if (segment.type === "blank" && segment.id) {
1419
+ initialInputs[segment.id] = "";
1420
+ }
1421
+ });
1422
+ setUserInputs(initialInputs);
1423
+ }
1424
+ }, [segments, userAnswer]);
1425
+ const handleInputChange = (blankId, value) => {
1426
+ const newInputs = __spreadProps(__spreadValues({}, userInputs), { [blankId]: value });
1427
+ setUserInputs(newInputs);
1428
+ const hasValue = Object.values(newInputs).some((val) => val.trim() !== "");
1429
+ onAnswerChange(hasValue ? newInputs : null);
1430
+ };
1431
+ const getCorrectnessForBlank = (blankId) => {
1432
+ var _a;
1433
+ if (!showCorrectAnswer || !userInputs[blankId]) return null;
1434
+ const userAnswerForBlank = (_a = userInputs[blankId]) == null ? void 0 : _a.trim();
1435
+ const correctAnswerDef = correctAnswersMap.find((a) => a.blankId === blankId);
1436
+ if (!correctAnswerDef || !userAnswerForBlank) return false;
1437
+ const caseSensitive = isCaseSensitive === void 0 ? false : isCaseSensitive;
1438
+ return correctAnswerDef.acceptedValues.some(
1439
+ (accVal) => caseSensitive ? accVal.trim() === userAnswerForBlank : accVal.trim().toLowerCase() === userAnswerForBlank.toLowerCase()
1440
+ );
1441
+ };
1442
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "text-base leading-relaxed flex flex-wrap items-center gap-x-1", "aria-labelledby": `question-prompt-${questionId}` }, segments.map((segment, index) => {
1443
+ var _a;
1444
+ if (segment.type === "text") {
1445
+ return /* @__PURE__ */ React25__namespace.default.createElement(
1446
+ MarkdownRenderer,
1447
+ {
1448
+ key: `text-${index}`,
1449
+ content: segment.content || "",
1450
+ className: "inline"
1451
+ }
1452
+ );
1453
+ }
1454
+ if (segment.type === "blank" && segment.id) {
1455
+ const blankId = segment.id;
1456
+ const isCorrect = getCorrectnessForBlank(blankId);
1457
+ let inputClassName = "inline-block w-auto min-w-[100px] max-w-[200px] h-8 mx-1 align-baseline text-base";
1458
+ if (showCorrectAnswer && ((_a = userInputs[blankId]) == null ? void 0 : _a.trim())) {
1459
+ inputClassName += isCorrect ? " border-green-500 focus-visible:ring-green-500" : " border-destructive focus-visible:ring-destructive";
1460
+ } else {
1461
+ inputClassName += " border-input";
1462
+ }
1463
+ return /* @__PURE__ */ React25__namespace.default.createElement(
1464
+ Input,
1465
+ {
1466
+ key: blankId,
1467
+ id: blankId,
1468
+ type: "text",
1469
+ value: userInputs[blankId] || "",
1470
+ onChange: (e) => handleInputChange(blankId, e.target.value),
1471
+ placeholder: "\u0110i\u1EC1n...",
1472
+ className: inputClassName,
1473
+ "aria-label": `Blank ${index + 1}`,
1474
+ disabled: showCorrectAnswer
1475
+ }
1476
+ );
1477
+ }
1478
+ return null;
1479
+ })), showCorrectAnswer && explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch chung:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 space-y-3" }, correctAnswersMap.map((ansDef) => {
1480
+ var _a;
1481
+ const isBlankCorrect = getCorrectnessForBlank(ansDef.blankId);
1482
+ const userAnswerDisplay = userInputs[ansDef.blankId] || "Ch\u01B0a tr\u1EA3 l\u1EDDi";
1483
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { key: `feedback-${ansDef.blankId}`, className: `p-2 border rounded-md ${isBlankCorrect ? "border-green-500/50 bg-green-500/10" : "border-destructive/50 bg-destructive/10"}` }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-semibold" }, "\xD4 tr\u1ED1ng '", ((_a = segments.find((s) => s.id === ansDef.blankId && s.type === "blank")) == null ? void 0 : _a.id) || ansDef.blankId, "':"), ' B\u1EA1n \u0111\xE3 \u0111i\u1EC1n: "', userAnswerDisplay, '".'), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs" }, "\u0110\xE1p \xE1n ch\u1EA5p nh\u1EADn: ", ansDef.acceptedValues.join(", ")));
1484
+ }))));
1485
+ };
1486
+ var buttonVariants = classVarianceAuthority.cva(
1487
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
1488
+ {
1489
+ variants: {
1490
+ variant: {
1491
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
1492
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
1493
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
1494
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
1495
+ ghost: "hover:bg-accent hover:text-accent-foreground",
1496
+ link: "text-primary underline-offset-4 hover:underline"
1497
+ },
1498
+ size: {
1499
+ default: "h-10 px-4 py-2",
1500
+ sm: "h-9 rounded-md px-3",
1501
+ lg: "h-11 rounded-md px-8",
1502
+ icon: "h-10 w-10"
1503
+ }
1504
+ },
1505
+ defaultVariants: {
1506
+ variant: "default",
1507
+ size: "default"
1508
+ }
1509
+ }
1510
+ );
1511
+ var Button = React25__namespace.forwardRef(
1512
+ (_a, ref) => {
1513
+ var _b = _a, { className, variant, size, asChild = false } = _b, props = __objRest(_b, ["className", "variant", "size", "asChild"]);
1514
+ const Comp = asChild ? reactSlot.Slot : "button";
1515
+ return /* @__PURE__ */ React25__namespace.createElement(
1516
+ Comp,
1517
+ __spreadValues({
1518
+ className: cn(buttonVariants({ variant, size, className })),
1519
+ ref
1520
+ }, props)
1521
+ );
1522
+ }
1523
+ );
1524
+ Button.displayName = "Button";
1525
+ var SequenceQuestionUI = ({
1526
+ question,
1527
+ onAnswerChange,
1528
+ userAnswer,
1529
+ showCorrectAnswer = false
1530
+ }) => {
1531
+ const { prompt, items, points, explanation, id: questionId, correctOrder } = question;
1532
+ const [selectedSequence, setSelectedSequence] = React25.useState([]);
1533
+ const [availableItems, setAvailableItems] = React25.useState([]);
1534
+ React25.useEffect(() => {
1535
+ const initialUserOrder = Array.isArray(userAnswer) ? userAnswer : [];
1536
+ const initialSelected = [];
1537
+ const initialAvailable = [...items];
1538
+ initialUserOrder.forEach((itemId) => {
1539
+ const item = items.find((i) => i.id === itemId);
1540
+ if (item) {
1541
+ initialSelected.push(item);
1542
+ const itemIndexInAvailable = initialAvailable.findIndex((i) => i.id === itemId);
1543
+ if (itemIndexInAvailable > -1) {
1544
+ initialAvailable.splice(itemIndexInAvailable, 1);
1545
+ }
1546
+ }
1547
+ });
1548
+ setSelectedSequence(initialSelected);
1549
+ setAvailableItems(initialAvailable);
1550
+ }, [items, userAnswer]);
1551
+ const handleSelectItem = (item) => {
1552
+ if (showCorrectAnswer) return;
1553
+ const newSelectedSequence = [...selectedSequence, item];
1554
+ setSelectedSequence(newSelectedSequence);
1555
+ setAvailableItems(availableItems.filter((i) => i.id !== item.id));
1556
+ onAnswerChange(newSelectedSequence.map((i) => i.id));
1557
+ };
1558
+ const handleRemoveFromSequence = (itemToRemove, index) => {
1559
+ if (showCorrectAnswer) return;
1560
+ const newSelectedSequence = selectedSequence.filter((_, i) => i !== index);
1561
+ setSelectedSequence(newSelectedSequence);
1562
+ setAvailableItems([...availableItems, itemToRemove].sort((a, b) => items.findIndex((i) => i.id === a.id) - items.findIndex((i) => i.id === b.id)));
1563
+ onAnswerChange(newSelectedSequence.length > 0 ? newSelectedSequence.map((i) => i.id) : null);
1564
+ };
1565
+ const handleResetSequence = () => {
1566
+ if (showCorrectAnswer) return;
1567
+ setSelectedSequence([]);
1568
+ setAvailableItems([...items]);
1569
+ onAnswerChange(null);
1570
+ };
1571
+ const getFeedbackIcon = (index) => {
1572
+ if (!showCorrectAnswer || !Array.isArray(userAnswer) || userAnswer.length <= index) return null;
1573
+ const userItemId = userAnswer[index];
1574
+ const correctItemId = correctOrder[index];
1575
+ return userItemId === correctItemId ? /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-4 w-4 text-green-500 ml-2 flex-shrink-0" }) : /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.XCircle, { className: "h-4 w-4 text-destructive ml-2 flex-shrink-0" });
1576
+ };
1577
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0 space-y-6" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React25__namespace.default.createElement(Label, { className: "font-semibold" }, "S\u1EAFp x\u1EBFp c\xE1c m\u1EE5c sau theo \u0111\xFAng th\u1EE9 t\u1EF1:"), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2" }, availableItems.map((item) => /* @__PURE__ */ React25__namespace.default.createElement(
1578
+ Button,
1579
+ {
1580
+ key: item.id,
1581
+ variant: "outline",
1582
+ onClick: () => handleSelectItem(item),
1583
+ className: "justify-start text-left h-auto py-2 px-3",
1584
+ disabled: showCorrectAnswer
1585
+ },
1586
+ /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: item.content })
1587
+ ))), availableItems.length === 0 && selectedSequence.length > 0 && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, 'T\u1EA5t c\u1EA3 c\xE1c m\u1EE5c \u0111\xE3 \u0111\u01B0\u1EE3c ch\u1ECDn. Nh\u1EA5p v\xE0o m\u1EE5c trong "Th\u1EE9 t\u1EF1 b\u1EA1n \u0111\xE3 ch\u1ECDn" \u0111\u1EC3 b\u1ECF ch\u1ECDn.')), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(Label, { className: "font-semibold" }, "Th\u1EE9 t\u1EF1 b\u1EA1n \u0111\xE3 ch\u1ECDn:"), /* @__PURE__ */ React25__namespace.default.createElement(Button, { variant: "ghost", size: "sm", onClick: handleResetSequence, disabled: showCorrectAnswer || selectedSequence.length === 0 }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.RotateCcw, { className: "mr-2 h-3.5 w-3.5" }), " \u0110\u1EB7t l\u1EA1i")), selectedSequence.length === 0 ? /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-muted-foreground p-3 border border-dashed rounded-md" }, "Ch\u01B0a ch\u1ECDn m\u1EE5c n\xE0o. Nh\u1EA5p v\xE0o c\xE1c m\u1EE5c \u1EDF tr\xEAn \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u.") : /* @__PURE__ */ React25__namespace.default.createElement("ul", { className: "space-y-2" }, selectedSequence.map((item, index) => /* @__PURE__ */ React25__namespace.default.createElement(
1588
+ "li",
1589
+ {
1590
+ key: item.id,
1591
+ onClick: () => handleRemoveFromSequence(item, index),
1592
+ className: `flex items-center justify-between p-3 border rounded-md ${showCorrectAnswer ? (userAnswer == null ? void 0 : userAnswer[index]) === correctOrder[index] ? "border-green-500 bg-green-500/10" : "border-destructive bg-destructive/10" : "bg-muted/30 cursor-pointer hover:border-destructive/50"} transition-colors`
1593
+ },
1594
+ /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex-grow flex items-center" }, /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-semibold mr-2" }, index + 1, "."), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: item.content })),
1595
+ showCorrectAnswer ? getFeedbackIcon(index) : /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.XCircle, { className: "h-4 w-4 text-muted-foreground hover:text-destructive flex-shrink-0" })
1596
+ )))), showCorrectAnswer && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Th\u1EE9 t\u1EF1 \u0111\xFAng:"), /* @__PURE__ */ React25__namespace.default.createElement("ol", { className: "list-decimal list-inside text-sm text-accent-foreground/80 space-y-1 mt-1" }, correctOrder.map((itemId) => {
1597
+ const item = items.find((i) => i.id === itemId);
1598
+ return /* @__PURE__ */ React25__namespace.default.createElement("li", { key: itemId }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: item ? item.content : "Kh\xF4ng t\xECm th\u1EA5y m\u1EE5c" }));
1599
+ }))), explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })))));
1600
+ };
1601
+ var Select = SelectPrimitive__namespace.Root;
1602
+ var SelectValue = SelectPrimitive__namespace.Value;
1603
+ var SelectTrigger = React25__namespace.forwardRef((_a, ref) => {
1604
+ var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
1605
+ return /* @__PURE__ */ React25__namespace.createElement(
1606
+ SelectPrimitive__namespace.Trigger,
1607
+ __spreadValues({
1608
+ ref,
1609
+ className: cn(
1610
+ "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
1611
+ className
1612
+ )
1613
+ }, props),
1614
+ children,
1615
+ /* @__PURE__ */ React25__namespace.createElement(SelectPrimitive__namespace.Icon, { asChild: true }, /* @__PURE__ */ React25__namespace.createElement(lucideReact.ChevronDown, { className: "h-4 w-4 opacity-50" }))
1616
+ );
1617
+ });
1618
+ SelectTrigger.displayName = SelectPrimitive__namespace.Trigger.displayName;
1619
+ var SelectScrollUpButton = React25__namespace.forwardRef((_a, ref) => {
1620
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1621
+ return /* @__PURE__ */ React25__namespace.createElement(
1622
+ SelectPrimitive__namespace.ScrollUpButton,
1623
+ __spreadValues({
1624
+ ref,
1625
+ className: cn(
1626
+ "flex cursor-default items-center justify-center py-1",
1627
+ className
1628
+ )
1629
+ }, props),
1630
+ /* @__PURE__ */ React25__namespace.createElement(lucideReact.ChevronUp, { className: "h-4 w-4" })
1631
+ );
1632
+ });
1633
+ SelectScrollUpButton.displayName = SelectPrimitive__namespace.ScrollUpButton.displayName;
1634
+ var SelectScrollDownButton = React25__namespace.forwardRef((_a, ref) => {
1635
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1636
+ return /* @__PURE__ */ React25__namespace.createElement(
1637
+ SelectPrimitive__namespace.ScrollDownButton,
1638
+ __spreadValues({
1639
+ ref,
1640
+ className: cn(
1641
+ "flex cursor-default items-center justify-center py-1",
1642
+ className
1643
+ )
1644
+ }, props),
1645
+ /* @__PURE__ */ React25__namespace.createElement(lucideReact.ChevronDown, { className: "h-4 w-4" })
1646
+ );
1647
+ });
1648
+ SelectScrollDownButton.displayName = SelectPrimitive__namespace.ScrollDownButton.displayName;
1649
+ var SelectContent = React25__namespace.forwardRef((_a, ref) => {
1650
+ var _b = _a, { className, children, position = "popper" } = _b, props = __objRest(_b, ["className", "children", "position"]);
1651
+ return /* @__PURE__ */ React25__namespace.createElement(SelectPrimitive__namespace.Portal, null, /* @__PURE__ */ React25__namespace.createElement(
1652
+ SelectPrimitive__namespace.Content,
1653
+ __spreadValues({
1654
+ ref,
1655
+ className: cn(
1656
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
1657
+ position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
1658
+ className
1659
+ ),
1660
+ position
1661
+ }, props),
1662
+ /* @__PURE__ */ React25__namespace.createElement(SelectScrollUpButton, null),
1663
+ /* @__PURE__ */ React25__namespace.createElement(
1664
+ SelectPrimitive__namespace.Viewport,
1665
+ {
1666
+ className: cn(
1667
+ "p-1",
1668
+ position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
1669
+ )
1670
+ },
1671
+ children
1672
+ ),
1673
+ /* @__PURE__ */ React25__namespace.createElement(SelectScrollDownButton, null)
1674
+ ));
1675
+ });
1676
+ SelectContent.displayName = SelectPrimitive__namespace.Content.displayName;
1677
+ var SelectLabel = React25__namespace.forwardRef((_a, ref) => {
1678
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1679
+ return /* @__PURE__ */ React25__namespace.createElement(
1680
+ SelectPrimitive__namespace.Label,
1681
+ __spreadValues({
1682
+ ref,
1683
+ className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)
1684
+ }, props)
1685
+ );
1686
+ });
1687
+ SelectLabel.displayName = SelectPrimitive__namespace.Label.displayName;
1688
+ var SelectItem = React25__namespace.forwardRef((_a, ref) => {
1689
+ var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
1690
+ return /* @__PURE__ */ React25__namespace.createElement(
1691
+ SelectPrimitive__namespace.Item,
1692
+ __spreadValues({
1693
+ ref,
1694
+ className: cn(
1695
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
1696
+ className
1697
+ )
1698
+ }, props),
1699
+ /* @__PURE__ */ React25__namespace.createElement("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center" }, /* @__PURE__ */ React25__namespace.createElement(SelectPrimitive__namespace.ItemIndicator, null, /* @__PURE__ */ React25__namespace.createElement(lucideReact.Check, { className: "h-4 w-4" }))),
1700
+ /* @__PURE__ */ React25__namespace.createElement(SelectPrimitive__namespace.ItemText, null, children)
1701
+ );
1702
+ });
1703
+ SelectItem.displayName = SelectPrimitive__namespace.Item.displayName;
1704
+ var SelectSeparator = React25__namespace.forwardRef((_a, ref) => {
1705
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
1706
+ return /* @__PURE__ */ React25__namespace.createElement(
1707
+ SelectPrimitive__namespace.Separator,
1708
+ __spreadValues({
1709
+ ref,
1710
+ className: cn("-mx-1 my-1 h-px bg-muted", className)
1711
+ }, props)
1712
+ );
1713
+ });
1714
+ SelectSeparator.displayName = SelectPrimitive__namespace.Separator.displayName;
1715
+ var MatchingQuestionUI = ({
1716
+ question,
1717
+ onAnswerChange,
1718
+ userAnswer,
1719
+ showCorrectAnswer = false
1720
+ }) => {
1721
+ const { prompt, prompts, options: initialOptions, points, explanation, correctAnswerMap, id: questionId } = question;
1722
+ const [currentAnswers, setCurrentAnswers] = React25.useState({});
1723
+ const [shuffledOptions, setShuffledOptions] = React25.useState(initialOptions);
1724
+ React25.useEffect(() => {
1725
+ if (question.shuffleOptions) {
1726
+ setShuffledOptions([...initialOptions].sort(() => Math.random() - 0.5));
1727
+ } else {
1728
+ setShuffledOptions(initialOptions);
1729
+ }
1730
+ }, [initialOptions, question.shuffleOptions]);
1731
+ React25.useEffect(() => {
1732
+ if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
1733
+ setCurrentAnswers(userAnswer);
1734
+ } else {
1735
+ const initial = {};
1736
+ prompts.forEach((p) => initial[p.id] = "");
1737
+ setCurrentAnswers(initial);
1738
+ }
1739
+ }, [userAnswer, prompts]);
1740
+ const handleSelectChange = (promptId, optionId) => {
1741
+ const newAnswers = __spreadProps(__spreadValues({}, currentAnswers), { [promptId]: optionId });
1742
+ setCurrentAnswers(newAnswers);
1743
+ const hasSelection = Object.values(newAnswers).some((val) => val && val !== "");
1744
+ onAnswerChange(hasSelection ? newAnswers : null);
1745
+ };
1746
+ const getCorrectOptionIdForPrompt = (promptId) => {
1747
+ var _a;
1748
+ return (_a = correctAnswerMap.find((map) => map.promptId === promptId)) == null ? void 0 : _a.optionId;
1749
+ };
1750
+ const getPlainText = (htmlString) => {
1751
+ if (!htmlString) return "";
1752
+ if (typeof document !== "undefined") {
1753
+ const tempDiv = document.createElement("div");
1754
+ tempDiv.innerHTML = htmlString;
1755
+ return tempDiv.textContent || tempDiv.innerText || "";
1756
+ }
1757
+ return htmlString.replace(/<[^>]*>?/gm, "");
1758
+ };
1759
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4" }, prompts.map((promptItem) => {
1760
+ var _a;
1761
+ const selectedOptionId = currentAnswers[promptItem.id] || "";
1762
+ const correctOptionId = getCorrectOptionIdForPrompt(promptItem.id);
1763
+ const isSelectionCorrect = showCorrectAnswer && selectedOptionId ? selectedOptionId === correctOptionId : null;
1764
+ let borderColor = "border-muted";
1765
+ if (showCorrectAnswer && selectedOptionId) {
1766
+ borderColor = isSelectionCorrect ? "border-green-500" : "border-destructive";
1767
+ }
1768
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { key: promptItem.id, className: `p-3 border rounded-md ${borderColor} transition-colors bg-background` }, /* @__PURE__ */ React25__namespace.default.createElement(Label, { htmlFor: `select-prompt-${promptItem.id}`, className: "font-medium text-base block mb-2" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: promptItem.content })), /* @__PURE__ */ React25__namespace.default.createElement(
1769
+ Select,
1770
+ {
1771
+ value: selectedOptionId,
1772
+ onValueChange: (value) => handleSelectChange(promptItem.id, value),
1773
+ disabled: showCorrectAnswer
1774
+ },
1775
+ /* @__PURE__ */ React25__namespace.default.createElement(SelectTrigger, { id: `select-prompt-${promptItem.id}` }, /* @__PURE__ */ React25__namespace.default.createElement(SelectValue, { placeholder: "Ch\u1ECDn \u0111\xE1p \xE1n..." })),
1776
+ /* @__PURE__ */ React25__namespace.default.createElement(SelectContent, null, shuffledOptions.map((option) => /* @__PURE__ */ React25__namespace.default.createElement(SelectItem, { key: option.id, value: option.id }, getPlainText(option.content))))
1777
+ ), showCorrectAnswer && selectedOptionId && (isSelectionCorrect ? /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-5 w-5 text-green-500 mt-2 inline-block" }) : /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.XCircle, { className: "h-5 w-5 text-destructive mt-2 inline-block" })), showCorrectAnswer && !selectedOptionId && correctOptionId && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs text-muted-foreground mt-1" }, "Ch\u01B0a ch\u1ECDn. \u0110\xE1p \xE1n \u0111\xFAng: ", getPlainText((_a = shuffledOptions.find((o) => o.id === correctOptionId)) == null ? void 0 : _a.content)));
1778
+ })), showCorrectAnswer && explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "font-semibold text-md" }, "\u0110\xE1p \xE1n \u0111\xFAng:"), /* @__PURE__ */ React25__namespace.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, correctAnswerMap.map((map) => {
1779
+ var _a, _b;
1780
+ const promptText = (_a = prompts.find((p) => p.id === map.promptId)) == null ? void 0 : _a.content;
1781
+ const optionText = (_b = initialOptions.find((o) => o.id === map.optionId)) == null ? void 0 : _b.content;
1782
+ return /* @__PURE__ */ React25__namespace.default.createElement("li", { key: map.promptId }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: `<strong>${getPlainText(promptText) || "N/A"}</strong> gh\xE9p v\u1EDBi <strong>${getPlainText(optionText) || "N/A"}</strong>` }));
1783
+ })))));
1784
+ };
1785
+ var DragAndDropQuestionUI = ({
1786
+ question,
1787
+ onAnswerChange,
1788
+ userAnswer,
1789
+ showCorrectAnswer = false
1790
+ }) => {
1791
+ const { prompt, draggableItems, dropZones, points, explanation, answerMap, id: questionId, backgroundImageUrl } = question;
1792
+ const [currentAnswers, setCurrentAnswers] = React25.useState({});
1793
+ React25.useEffect(() => {
1794
+ if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
1795
+ setCurrentAnswers(userAnswer);
1796
+ } else {
1797
+ const initial = {};
1798
+ draggableItems.forEach((item) => initial[item.id] = "");
1799
+ setCurrentAnswers(initial);
1800
+ }
1801
+ }, [userAnswer, draggableItems]);
1802
+ const handleSelectChange = (draggableItemId, dropZoneId) => {
1803
+ const newAnswers = __spreadProps(__spreadValues({}, currentAnswers), { [draggableItemId]: dropZoneId });
1804
+ setCurrentAnswers(newAnswers);
1805
+ const hasSelection = Object.values(newAnswers).some((val) => val && val !== "");
1806
+ onAnswerChange(hasSelection ? newAnswers : null);
1807
+ };
1808
+ const getCorrectDropZoneIdForDraggable = (draggableItemId) => {
1809
+ var _a;
1810
+ return (_a = answerMap.find((map) => map.draggableId === draggableItemId)) == null ? void 0 : _a.dropZoneId;
1811
+ };
1812
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0 space-y-4" }, backgroundImageUrl && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mb-4 overflow-hidden rounded-md border" }, /* @__PURE__ */ React25__namespace.default.createElement("img", { src: backgroundImageUrl, alt: question.imageAltText || "Drag and drop background", className: "w-full h-auto object-contain max-h-[300px]", "data-ai-hint": "abstract pattern" })), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React25__namespace.default.createElement(Label, { className: "font-semibold" }, "Gh\xE9p c\xE1c m\u1EE5c sau v\xE0o \u0111\xFAng v\u1ECB tr\xED:"), draggableItems.map((item) => {
1813
+ const selectedDropZoneId = currentAnswers[item.id] || "";
1814
+ const correctDropZoneId = getCorrectDropZoneIdForDraggable(item.id);
1815
+ const isSelectionCorrect = showCorrectAnswer && selectedDropZoneId ? selectedDropZoneId === correctDropZoneId : null;
1816
+ let itemStyle = "flex flex-col sm:flex-row items-start sm:items-center justify-between p-3 border rounded-md transition-colors bg-background";
1817
+ if (showCorrectAnswer && selectedDropZoneId) {
1818
+ itemStyle += isSelectionCorrect ? " border-green-500" : " border-destructive";
1819
+ } else {
1820
+ itemStyle += " border-muted";
1821
+ }
1822
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { key: item.id, className: itemStyle }, /* @__PURE__ */ React25__namespace.default.createElement(Label, { htmlFor: `select-draggable-${item.id}`, className: "font-medium text-base mb-2 sm:mb-0 sm:mr-4 flex-1" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: item.content })), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex items-center space-x-2 w-full sm:w-auto" }, /* @__PURE__ */ React25__namespace.default.createElement(
1823
+ Select,
1824
+ {
1825
+ value: selectedDropZoneId,
1826
+ onValueChange: (value) => handleSelectChange(item.id, value),
1827
+ disabled: showCorrectAnswer
1828
+ },
1829
+ /* @__PURE__ */ React25__namespace.default.createElement(SelectTrigger, { id: `select-draggable-${item.id}`, className: "w-full sm:min-w-[200px]" }, /* @__PURE__ */ React25__namespace.default.createElement(SelectValue, { placeholder: "Ch\u1ECDn v\u1ECB tr\xED..." })),
1830
+ /* @__PURE__ */ React25__namespace.default.createElement(SelectContent, null, dropZones.map((zone) => /* @__PURE__ */ React25__namespace.default.createElement(SelectItem, { key: zone.id, value: zone.id }, zone.label.replace(/<[^>]*>?/gm, ""))))
1831
+ ), showCorrectAnswer && selectedDropZoneId && (isSelectionCorrect ? /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-5 w-5 text-green-500 flex-shrink-0" }) : /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.XCircle, { className: "h-5 w-5 text-destructive flex-shrink-0" }))));
1832
+ })), showCorrectAnswer && explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Gi\u1EA3i th\xEDch:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "font-semibold text-md" }, "\u0110\xE1p \xE1n \u0111\xFAng:"), /* @__PURE__ */ React25__namespace.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, answerMap.map((map) => {
1833
+ var _a, _b;
1834
+ const draggableText = (_a = draggableItems.find((d) => d.id === map.draggableId)) == null ? void 0 : _a.content;
1835
+ const dropZoneText = (_b = dropZones.find((z) => z.id === map.dropZoneId)) == null ? void 0 : _b.label;
1836
+ return /* @__PURE__ */ React25__namespace.default.createElement("li", { key: map.draggableId }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: `<strong>${draggableText || "N/A"}</strong> v\xE0o <strong>${dropZoneText || "N/A"}</strong>` }));
1837
+ })))));
1838
+ };
1839
+ var HotspotQuestionUI = ({
1840
+ question,
1841
+ onAnswerChange,
1842
+ userAnswer,
1843
+ showCorrectAnswer = false
1844
+ }) => {
1845
+ const { prompt, imageUrl, imageAltText, hotspots, points, explanation, correctAnswerIds, id: questionId } = question;
1846
+ const [selectedIds, setSelectedIds] = React25.useState([]);
1847
+ React25.useEffect(() => {
1848
+ if (Array.isArray(userAnswer)) {
1849
+ setSelectedIds(userAnswer);
1850
+ } else {
1851
+ setSelectedIds([]);
1852
+ }
1853
+ }, [userAnswer]);
1854
+ const handleHotspotClick = (hotspotId) => {
1855
+ if (showCorrectAnswer) return;
1856
+ const newSelectedIds = selectedIds.includes(hotspotId) ? selectedIds.filter((id) => id !== hotspotId) : [...selectedIds, hotspotId];
1857
+ setSelectedIds(newSelectedIds);
1858
+ onAnswerChange(newSelectedIds.length > 0 ? newSelectedIds : null);
1859
+ };
1860
+ const getHotspotStyle = (hotspot) => {
1861
+ const style = {
1862
+ position: "absolute",
1863
+ border: "2px dashed transparent",
1864
+ cursor: showCorrectAnswer ? "default" : "pointer",
1865
+ transition: "border-color 0.2s, background-color 0.2s"
1866
+ };
1867
+ if (hotspot.shape === "rect") {
1868
+ style.left = `${hotspot.coords[0]}px`;
1869
+ style.top = `${hotspot.coords[1]}px`;
1870
+ style.width = `${hotspot.coords[2]}px`;
1871
+ style.height = `${hotspot.coords[3]}px`;
1872
+ } else if (hotspot.shape === "circle") {
1873
+ style.left = `${hotspot.coords[0] - hotspot.coords[2]}px`;
1874
+ style.top = `${hotspot.coords[1] - hotspot.coords[2]}px`;
1875
+ style.width = `${hotspot.coords[2] * 2}px`;
1876
+ style.height = `${hotspot.coords[2] * 2}px`;
1877
+ style.borderRadius = "50%";
1878
+ }
1879
+ const isSelected = selectedIds.includes(hotspot.id);
1880
+ const safeCorrectAnswerIds = correctAnswerIds || [];
1881
+ const isCorrect = safeCorrectAnswerIds.includes(hotspot.id);
1882
+ if (showCorrectAnswer) {
1883
+ if (isCorrect && isSelected) {
1884
+ style.borderColor = "hsl(var(--success-foreground, 142.1 70.6% 45.3%))";
1885
+ style.backgroundColor = "hsla(var(--success-foreground, 142.1 70.6% 45.3%), 0.2)";
1886
+ } else if (!isCorrect && isSelected) {
1887
+ style.borderColor = "hsl(var(--destructive))";
1888
+ style.backgroundColor = "hsla(var(--destructive), 0.2)";
1889
+ } else if (isCorrect && !isSelected) {
1890
+ style.borderColor = "hsl(var(--primary))";
1891
+ style.borderStyle = "solid";
1892
+ }
1893
+ } else if (isSelected) {
1894
+ style.borderColor = "hsl(var(--primary))";
1895
+ style.backgroundColor = "hsla(var(--primary), 0.1)";
1896
+ } else {
1897
+ style.borderColor = "hsla(var(--muted-foreground), 0.7)";
1898
+ }
1899
+ return style;
1900
+ };
1901
+ const getPlainText = (htmlString) => {
1902
+ if (!htmlString) return "";
1903
+ const tempDiv = document.createElement("div");
1904
+ tempDiv.innerHTML = htmlString;
1905
+ return tempDiv.textContent || tempDiv.innerText || "";
1906
+ };
1907
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: prompt })), points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0 space-y-4" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "relative w-full border border-muted rounded-md overflow-hidden", style: { maxWidth: "100%", maxHeight: "500px" } }, /* @__PURE__ */ React25__namespace.default.createElement(
1908
+ "img",
1909
+ {
1910
+ src: imageUrl,
1911
+ alt: imageAltText || "Hotspot image",
1912
+ className: "block max-w-full max-h-full object-contain",
1913
+ "data-ai-hint": question.imageAltText ? question.imageAltText.split(" ").slice(0, 2).join(" ") : "diagram illustration"
1914
+ }
1915
+ ), hotspots.map((hotspot) => /* @__PURE__ */ React25__namespace.default.createElement(
1916
+ "div",
1917
+ {
1918
+ key: hotspot.id,
1919
+ title: getPlainText(hotspot.description) || `Hotspot ${hotspot.id}`,
1920
+ style: getHotspotStyle(hotspot),
1921
+ onClick: () => handleHotspotClick(hotspot.id),
1922
+ "aria-pressed": selectedIds.includes(hotspot.id),
1923
+ role: "button",
1924
+ tabIndex: showCorrectAnswer ? -1 : 0,
1925
+ onKeyDown: (e) => {
1926
+ if (e.key === "Enter" || e.key === " ") handleHotspotClick(hotspot.id);
1927
+ }
1928
+ }
1929
+ ))), showCorrectAnswer && explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })), showCorrectAnswer && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "font-semibold text-md" }, "Correct Hotspots:"), /* @__PURE__ */ React25__namespace.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm" }, (correctAnswerIds || []).map((id) => {
1930
+ const hotspot = hotspots.find((h) => h.id === id);
1931
+ return /* @__PURE__ */ React25__namespace.default.createElement("li", { key: id }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: (hotspot == null ? void 0 : hotspot.description) || (hotspot == null ? void 0 : hotspot.id) || "N/A", className: "inline" }));
1932
+ })))));
1933
+ };
1934
+ var loadScript = (src, async = true) => {
1935
+ return new Promise((resolve, reject) => {
1936
+ const existingScript = document.querySelector(`script[src="${src}"]`);
1937
+ if (existingScript) {
1938
+ const readyState = existingScript.readyState;
1939
+ if (readyState && readyState !== "loaded" && readyState !== "complete") {
1940
+ existingScript.addEventListener("load", () => resolve());
1941
+ existingScript.addEventListener("error", () => reject(new Error(`Error event for existing script: ${src}`)));
1942
+ } else if (window.Blockly && src.includes("blockly.min.js") && window.Blockly.Blocks && window.Blockly.JavaScript) {
1943
+ resolve();
1944
+ } else if (window.Blockly && src.includes("blockly.min.js") && !window.Blockly.Blocks && src.includes("blocks.min.js")) {
1945
+ existingScript.addEventListener("load", () => resolve());
1946
+ existingScript.addEventListener("error", () => reject(new Error(`Error event for existing script (blocks): ${src}`)));
1947
+ } else if (window.Blockly && src.includes("blockly.min.js") && !window.Blockly.JavaScript && src.includes("javascript.min.js")) {
1948
+ existingScript.addEventListener("load", () => resolve());
1949
+ existingScript.addEventListener("error", () => reject(new Error(`Error event for existing script (JS gen): ${src}`)));
1950
+ } else {
1951
+ resolve();
1952
+ }
1953
+ return;
1954
+ }
1955
+ const script = document.createElement("script");
1956
+ script.src = src;
1957
+ script.async = async;
1958
+ script.onload = () => resolve();
1959
+ script.onerror = () => reject(new Error(`Failed to load new script: ${src}`));
1960
+ document.head.appendChild(script);
1961
+ });
1962
+ };
1963
+ var loadBlocklyScript = () => {
1964
+ return new Promise((resolve, reject) => {
1965
+ var _a, _b;
1966
+ if (typeof ((_a = window.Blockly) == null ? void 0 : _a.Blocks) !== "undefined" && typeof ((_b = window.Blockly) == null ? void 0 : _b.JavaScript) !== "undefined") {
1967
+ resolve();
1968
+ return;
1969
+ }
1970
+ const cdnOptions = [
1971
+ {
1972
+ name: "cdnjs",
1973
+ mainSrc: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/blockly.min.js",
1974
+ blocksSrc: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/blocks.min.js",
1975
+ generatorSrc: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/javascript.min.js",
1976
+ mediaPath: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/media/"
1977
+ },
1978
+ {
1979
+ name: "unpkg",
1980
+ mainSrc: "https://unpkg.com/blockly@9.0.0/blockly.min.js",
1981
+ blocksSrc: "https://unpkg.com/blockly@9.0.0/blocks.min.js",
1982
+ generatorSrc: "https://unpkg.com/blockly@9.0.0/javascript.min.js",
1983
+ mediaPath: "https://unpkg.com/blockly@9.0.0/media/"
1984
+ },
1985
+ {
1986
+ name: "jsdelivr",
1987
+ mainSrc: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/blockly.min.js",
1988
+ blocksSrc: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/blocks.min.js",
1989
+ generatorSrc: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/javascript.min.js",
1990
+ mediaPath: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/media/"
1991
+ }
1992
+ ];
1993
+ const tryLoadFromCDN = async (cdnIndex) => {
1994
+ var _a2, _b2, _c;
1995
+ if (cdnIndex >= cdnOptions.length) {
1996
+ throw new Error("All Blockly CDN loading options failed");
1997
+ }
1998
+ const cdn = cdnOptions[cdnIndex];
1999
+ try {
2000
+ await loadScript(cdn.mainSrc);
2001
+ const BlocklyGlobal = window.Blockly;
2002
+ if (typeof BlocklyGlobal === "undefined") throw new Error(`Blockly global not found from ${cdn.name}.`);
2003
+ if ((_b2 = (_a2 = BlocklyGlobal.utils) == null ? void 0 : _a2.global) == null ? void 0 : _b2.setPaths) {
2004
+ BlocklyGlobal.utils.global.setPaths(cdn.mediaPath);
2005
+ } else if ((_c = BlocklyGlobal.utils) == null ? void 0 : _c.global) {
2006
+ BlocklyGlobal.utils.global.blocklyPath = cdn.mediaPath;
2007
+ BlocklyGlobal.MEDIA = cdn.mediaPath;
2008
+ } else {
2009
+ BlocklyGlobal.MEDIA = cdn.mediaPath;
2010
+ }
2011
+ await Promise.all([loadScript(cdn.blocksSrc), loadScript(cdn.generatorSrc)]);
2012
+ if (typeof BlocklyGlobal.Blocks === "undefined") throw new Error(`Blockly.Blocks not found from ${cdn.name}.`);
2013
+ if (typeof BlocklyGlobal.JavaScript === "undefined") throw new Error(`Blockly.JavaScript not found from ${cdn.name}.`);
2014
+ resolve();
2015
+ } catch (error) {
2016
+ await tryLoadFromCDN(cdnIndex + 1);
2017
+ }
2018
+ };
2019
+ tryLoadFromCDN(0).catch(reject);
2020
+ });
2021
+ };
2022
+ var useBlocklyLoader = () => {
2023
+ const [isLoading, setIsLoading] = React25.useState(true);
2024
+ const [loadError, setLoadError] = React25.useState(null);
2025
+ const [isReady, setIsReady] = React25.useState(false);
2026
+ const attemptLoad = React25.useCallback(() => {
2027
+ setLoadError(null);
2028
+ setIsReady(false);
2029
+ loadBlocklyScript().then(() => {
2030
+ setIsReady(true);
2031
+ setIsLoading(false);
2032
+ setLoadError(null);
2033
+ }).catch((error) => {
2034
+ setLoadError(error.message || "Unknown error loading Blockly.");
2035
+ setIsLoading(false);
2036
+ setIsReady(false);
2037
+ });
2038
+ }, []);
2039
+ React25.useEffect(() => {
2040
+ if (isLoading && !isReady && !loadError) attemptLoad();
2041
+ }, [isLoading, isReady, loadError, attemptLoad]);
2042
+ const retry = React25.useCallback(() => {
2043
+ setLoadError(null);
2044
+ setIsReady(false);
2045
+ setIsLoading(true);
2046
+ }, []);
2047
+ return { isLoading, loadError, isReady, retry };
2048
+ };
2049
+ var BlocklyProgrammingQuestionUI = React25__namespace.default.forwardRef(({
2050
+ question,
2051
+ userAnswer,
2052
+ showCorrectAnswer = false
2053
+ }, ref) => {
2054
+ const blocklyDivRef = React25.useRef(null);
2055
+ const workspaceRef = React25.useRef(null);
2056
+ const [isInitializingComponent, setIsInitializingComponent] = React25.useState(false);
2057
+ const [componentError, setComponentError] = React25.useState(null);
2058
+ const { isLoading: blocklyLoading, loadError: blocklyLoadError, isReady: blocklyReady, retry } = useBlocklyLoader();
2059
+ React25.useImperativeHandle(ref, () => ({
2060
+ getWorkspaceXml: () => {
2061
+ var _a, _b;
2062
+ if (workspaceRef.current && blocklyReady) {
2063
+ const LocalBlockly = window.Blockly;
2064
+ if (!((_a = LocalBlockly == null ? void 0 : LocalBlockly.Xml) == null ? void 0 : _a.workspaceToDom) || !((_b = LocalBlockly == null ? void 0 : LocalBlockly.Xml) == null ? void 0 : _b.domToText)) {
2065
+ console.warn("Blockly.Xml methods not available for XML serialization in getWorkspaceXml.");
2066
+ return null;
2067
+ }
2068
+ try {
2069
+ const xml = LocalBlockly.Xml.workspaceToDom(workspaceRef.current);
2070
+ return LocalBlockly.Xml.domToText(xml);
2071
+ } catch (e) {
2072
+ console.error("Error serializing Blockly workspace to XML in getWorkspaceXml:", e);
2073
+ return null;
2074
+ }
2075
+ }
2076
+ return null;
2077
+ }
2078
+ }));
2079
+ const initializeBlocklyWorkspace = React25.useCallback(() => {
2080
+ var _a;
2081
+ if (!blocklyReady || !blocklyDivRef.current) return;
2082
+ const LocalBlockly = window.Blockly;
2083
+ if (!(LocalBlockly == null ? void 0 : LocalBlockly.inject) || !(LocalBlockly == null ? void 0 : LocalBlockly.Xml) || !(LocalBlockly == null ? void 0 : LocalBlockly.Events) || !(LocalBlockly == null ? void 0 : LocalBlockly.Themes)) {
2084
+ setComponentError("Blockly library not fully loaded.");
2085
+ setIsInitializingComponent(false);
2086
+ return;
2087
+ }
2088
+ setComponentError(null);
2089
+ let newXmlToLoad = null;
2090
+ if (showCorrectAnswer && question.solutionWorkspaceXML) {
2091
+ newXmlToLoad = question.solutionWorkspaceXML;
2092
+ } else if (typeof userAnswer === "string" && userAnswer.trim().startsWith("<xml")) {
2093
+ newXmlToLoad = userAnswer;
2094
+ } else if (question.initialWorkspace) {
2095
+ newXmlToLoad = question.initialWorkspace;
2096
+ }
2097
+ if (workspaceRef.current) {
2098
+ let currentWorkspaceXML = "";
2099
+ try {
2100
+ currentWorkspaceXML = LocalBlockly.Xml.domToText(LocalBlockly.Xml.workspaceToDom(workspaceRef.current));
2101
+ } catch (e) {
2102
+ }
2103
+ const readOnlyStateMatches = workspaceRef.current.options.readOnly === showCorrectAnswer;
2104
+ if (currentWorkspaceXML === newXmlToLoad && readOnlyStateMatches) {
2105
+ setIsInitializingComponent(false);
2106
+ return;
2107
+ }
2108
+ if (currentWorkspaceXML === newXmlToLoad && !readOnlyStateMatches) {
2109
+ workspaceRef.current.updateOptions({ readOnly: showCorrectAnswer, trashcan: !showCorrectAnswer });
2110
+ setIsInitializingComponent(false);
2111
+ return;
2112
+ }
2113
+ }
2114
+ setIsInitializingComponent(true);
2115
+ if ((_a = workspaceRef.current) == null ? void 0 : _a.dispose) {
2116
+ try {
2117
+ workspaceRef.current.dispose();
2118
+ } catch (e) {
2119
+ console.error("Error disposing previous workspace:", e);
2120
+ }
2121
+ workspaceRef.current = null;
2122
+ }
2123
+ try {
2124
+ const toolbox = question.toolboxDefinition || `<xml><category name="Logic" colour="210"><block type="controls_if"></block></category></xml>`;
2125
+ const workspace = LocalBlockly.inject(blocklyDivRef.current, {
2126
+ toolbox,
2127
+ scrollbars: true,
2128
+ trashcan: !showCorrectAnswer,
2129
+ readOnly: showCorrectAnswer,
2130
+ zoom: {
2131
+ controls: true,
2132
+ wheel: true,
2133
+ startScale: 0.9,
2134
+ maxScale: 3,
2135
+ minScale: 0.3,
2136
+ scaleSpeed: 1.2
2137
+ },
2138
+ grid: {
2139
+ spacing: 20,
2140
+ length: 3,
2141
+ colour: "#374151",
2142
+ snap: true
2143
+ },
2144
+ theme: LocalBlockly.Themes.Classic || void 0,
2145
+ move: {
2146
+ scrollbars: {
2147
+ horizontal: true,
2148
+ vertical: true
2149
+ },
2150
+ drag: true,
2151
+ wheel: true
2152
+ },
2153
+ renderer: "geras"
2154
+ });
2155
+ workspaceRef.current = workspace;
2156
+ if (newXmlToLoad) {
2157
+ try {
2158
+ const dom = LocalBlockly.Xml.textToDom(newXmlToLoad);
2159
+ LocalBlockly.Xml.domToWorkspace(dom, workspace);
2160
+ } catch (e) {
2161
+ console.error("Error loading XML to workspace:", e, "XML:", newXmlToLoad);
2162
+ setComponentError("Error loading blocks.");
2163
+ }
2164
+ }
2165
+ if (workspace.scrollCenter) workspace.scrollCenter();
2166
+ if (LocalBlockly.svgResize) LocalBlockly.svgResize(workspace);
2167
+ setTimeout(() => {
2168
+ if (workspace && LocalBlockly.svgResize) {
2169
+ LocalBlockly.svgResize(workspace);
2170
+ }
2171
+ }, 100);
2172
+ } catch (e) {
2173
+ console.error("Error initializing Blockly workspace:", e);
2174
+ setComponentError(`Init failed: ${e instanceof Error ? e.message : String(e)}`);
2175
+ } finally {
2176
+ setIsInitializingComponent(false);
2177
+ }
2178
+ }, [
2179
+ blocklyReady,
2180
+ question.id,
2181
+ question.toolboxDefinition,
2182
+ question.initialWorkspace,
2183
+ question.solutionWorkspaceXML,
2184
+ showCorrectAnswer,
2185
+ userAnswer
2186
+ ]);
2187
+ React25.useEffect(() => {
2188
+ if (blocklyReady && blocklyDivRef.current) {
2189
+ initializeBlocklyWorkspace();
2190
+ }
2191
+ return () => {
2192
+ var _a;
2193
+ if ((_a = workspaceRef.current) == null ? void 0 : _a.dispose) {
2194
+ try {
2195
+ workspaceRef.current.dispose();
2196
+ } catch (disposeError) {
2197
+ console.error("Error during Blockly workspace disposal on unmount:", disposeError);
2198
+ }
2199
+ workspaceRef.current = null;
2200
+ }
2201
+ };
2202
+ }, [blocklyReady, question.id, initializeBlocklyWorkspace]);
2203
+ React25.useEffect(() => {
2204
+ let resizeTimeout;
2205
+ const handleResize = () => {
2206
+ clearTimeout(resizeTimeout);
2207
+ resizeTimeout = setTimeout(() => {
2208
+ if (workspaceRef.current && blocklyReady) {
2209
+ const LocalBlockly = window.Blockly;
2210
+ if (LocalBlockly == null ? void 0 : LocalBlockly.svgResize) {
2211
+ LocalBlockly.svgResize(workspaceRef.current);
2212
+ }
2213
+ }
2214
+ }, 150);
2215
+ };
2216
+ window.addEventListener("resize", handleResize);
2217
+ return () => {
2218
+ window.removeEventListener("resize", handleResize);
2219
+ clearTimeout(resizeTimeout);
2220
+ };
2221
+ }, [blocklyReady]);
2222
+ const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
2223
+ const workspaceContainerId = `blockly-workspace-container-${question.id}`;
2224
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: question.prompt })), question.points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, blocklyLoading && /* @__PURE__ */ React25__namespace.default.createElement(
2225
+ "div",
2226
+ {
2227
+ style: {
2228
+ height: workspaceHeight,
2229
+ width: "100%",
2230
+ borderRadius: "0.375rem",
2231
+ border: "1px solid hsl(var(--border))",
2232
+ backgroundColor: "hsl(var(--background))"
2233
+ },
2234
+ className: "flex items-center justify-center"
2235
+ },
2236
+ /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "text-center" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-muted-foreground animate-pulse mb-2" }, "Loading Blockly Environment..."), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "w-8 h-8 border-4 border-muted border-t-primary rounded-full animate-spin mx-auto" }))
2237
+ ), blocklyLoadError && !blocklyLoading && /* @__PURE__ */ React25__namespace.default.createElement(
2238
+ "div",
2239
+ {
2240
+ style: {
2241
+ height: workspaceHeight,
2242
+ width: "100%",
2243
+ borderRadius: "0.375rem",
2244
+ border: "1px solid hsl(var(--destructive))",
2245
+ backgroundColor: "hsl(var(--card))"
2246
+ },
2247
+ className: "flex items-center justify-center p-4"
2248
+ },
2249
+ /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "text-destructive text-center" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "font-semibold text-lg" }, "Failed to load Blockly"), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm mt-2 mb-3" }, blocklyLoadError), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "space-x-2" }, /* @__PURE__ */ React25__namespace.default.createElement(
2250
+ "button",
2251
+ {
2252
+ onClick: retry,
2253
+ className: "px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors text-sm"
2254
+ },
2255
+ "Try Again"
2256
+ ), /* @__PURE__ */ React25__namespace.default.createElement(
2257
+ "button",
2258
+ {
2259
+ onClick: () => window.location.reload(),
2260
+ className: "px-4 py-2 bg-destructive text-destructive-foreground rounded hover:bg-destructive/90 transition-colors text-sm"
2261
+ },
2262
+ "Refresh Page"
2263
+ )))
2264
+ ), !blocklyLoading && !blocklyLoadError && /* @__PURE__ */ React25__namespace.default.createElement(
2265
+ "div",
2266
+ {
2267
+ id: workspaceContainerId,
2268
+ ref: blocklyDivRef,
2269
+ style: {
2270
+ height: workspaceHeight,
2271
+ width: "100%",
2272
+ borderRadius: "0.375rem",
2273
+ border: `1px solid ${componentError ? "hsl(var(--destructive))" : "hsl(var(--border))"}`,
2274
+ backgroundColor: "hsl(var(--card))",
2275
+ position: "relative",
2276
+ userSelect: "none",
2277
+ overflow: "hidden"
2278
+ },
2279
+ "aria-label": `Blockly programming workspace for question: ${question.prompt}`
2280
+ }
2281
+ ), showCorrectAnswer && question.explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement(MarkdownRenderer, { content: question.explanation, className: "text-sm text-accent-foreground/80" }))));
2282
+ });
2283
+ BlocklyProgrammingQuestionUI.displayName = "BlocklyProgrammingQuestionUI";
2284
+ var SCRATCH_JS_ENGINE_LAYOUT_PATH = "/static/scratch-blocks/js/blockly_compressed_vertical.js";
2285
+ var SCRATCH_JS_BLOCK_DEFINITIONS_PATH = "/static/scratch-blocks/js/blocks_compressed_vertical.js";
2286
+ var SCRATCH_JS_MSG_EN_PATH = "/static/scratch-blocks/msg/js/en.js";
2287
+ var SCRATCH_MEDIA_PATH = "/static/scratch-blocks/media/";
2288
+ var loadedScriptPromises = /* @__PURE__ */ new Map();
2289
+ var loadScript2 = (src) => {
2290
+ const fullSrc = src.startsWith("/") ? src : `/${src}`;
2291
+ if (loadedScriptPromises.has(fullSrc)) {
2292
+ const promise2 = loadedScriptPromises.get(fullSrc);
2293
+ return promise2;
2294
+ }
2295
+ const promise = new Promise((resolve, reject) => {
2296
+ const existingScript = document.querySelector(`script[src="${fullSrc}"]`);
2297
+ if (existingScript) {
2298
+ const status = existingScript.getAttribute("data-load-status");
2299
+ if (status === "loaded") {
2300
+ resolve();
2301
+ return;
2302
+ } else if (status === "loading") {
2303
+ const onLoad2 = () => {
2304
+ existingScript.setAttribute("data-load-status", "loaded");
2305
+ existingScript.removeEventListener("load", onLoad2);
2306
+ existingScript.removeEventListener("error", onError2);
2307
+ resolve();
2308
+ };
2309
+ const onError2 = (ev) => {
2310
+ existingScript.setAttribute("data-load-status", "error");
2311
+ existingScript.removeEventListener("load", onLoad2);
2312
+ existingScript.removeEventListener("error", onError2);
2313
+ loadedScriptPromises.delete(fullSrc);
2314
+ const errorMsg = `Error event for existing script tag ${src}. Full URL: ${existingScript.src}. Event: ${ev.type}.`;
2315
+ console.error("ScratchUI: loadScript error (existing) -", errorMsg);
2316
+ reject(new Error(errorMsg));
2317
+ };
2318
+ existingScript.addEventListener("load", onLoad2);
2319
+ existingScript.addEventListener("error", onError2);
2320
+ return;
2321
+ } else if (status === "error") {
2322
+ existingScript.remove();
2323
+ }
2324
+ }
2325
+ const script = document.createElement("script");
2326
+ script.src = fullSrc;
2327
+ script.async = false;
2328
+ script.setAttribute("data-load-status", "loading");
2329
+ const onLoad = () => {
2330
+ script.setAttribute("data-load-status", "loaded");
2331
+ script.removeEventListener("load", onLoad);
2332
+ script.removeEventListener("error", onError);
2333
+ resolve();
2334
+ };
2335
+ const onError = (ev) => {
2336
+ const errorMsg = `Failed to load new script: ${script.src}. Event: ${typeof ev === "string" ? ev : ev.type}. Check browser network tab for 404 or other errors. Ensure file exists in public folder and path is correct.`;
2337
+ console.error("ScratchUI: loadScript error (new) -", errorMsg, "Full URL attempted:", script.src);
2338
+ script.setAttribute("data-load-status", "error");
2339
+ script.removeEventListener("load", onLoad);
2340
+ script.removeEventListener("error", onError);
2341
+ loadedScriptPromises.delete(fullSrc);
2342
+ reject(new Error(errorMsg));
2343
+ };
2344
+ script.addEventListener("load", onLoad);
2345
+ script.addEventListener("error", onError);
2346
+ document.body.appendChild(script);
2347
+ });
2348
+ loadedScriptPromises.set(fullSrc, promise);
2349
+ return promise;
2350
+ };
2351
+ var ScratchProgrammingQuestionUI = React25.forwardRef(({
2352
+ question,
2353
+ userAnswer,
2354
+ showCorrectAnswer = false
2355
+ }, ref) => {
2356
+ const blocklyDivRef = React25.useRef(null);
2357
+ const workspaceRef = React25.useRef(null);
2358
+ const [isBlocklyReady, setIsBlocklyReady] = React25.useState(false);
2359
+ const [componentError, setComponentError] = React25.useState(null);
2360
+ const [isLoadingScripts, setIsLoadingScripts] = React25.useState(true);
2361
+ const attemptLoadScripts = React25.useCallback(async () => {
2362
+ var _a, _b, _c, _d;
2363
+ setIsLoadingScripts(true);
2364
+ setComponentError(null);
2365
+ console.log("ScratchUI: Starting script loading sequence...");
2366
+ try {
2367
+ console.log("ScratchUI: Attempting to load Scratch Engine/Layout:", SCRATCH_JS_ENGINE_LAYOUT_PATH);
2368
+ await loadScript2(SCRATCH_JS_ENGINE_LAYOUT_PATH);
2369
+ const BlocklyGlobalEngine = window.Blockly;
2370
+ if (typeof BlocklyGlobalEngine === "undefined") {
2371
+ throw new Error(`Blockly global object (window.Blockly) not found after loading engine script: ${SCRATCH_JS_ENGINE_LAYOUT_PATH}.`);
2372
+ }
2373
+ console.log(`ScratchUI: After engine/layout load: Blockly defined: ${!!BlocklyGlobalEngine}, Blockly.Blocks defined: ${!!(BlocklyGlobalEngine == null ? void 0 : BlocklyGlobalEngine.Blocks)}, Blockly.Msg defined: ${!!(BlocklyGlobalEngine == null ? void 0 : BlocklyGlobalEngine.Msg)}, Blockly.ScratchMsgs defined: ${!!(BlocklyGlobalEngine == null ? void 0 : BlocklyGlobalEngine.ScratchMsgs)}`);
2374
+ console.log("ScratchUI: Attempting to load Messages:", SCRATCH_JS_MSG_EN_PATH);
2375
+ await loadScript2(SCRATCH_JS_MSG_EN_PATH);
2376
+ let BlocklyGlobalAfterMsg = window.Blockly;
2377
+ if (!BlocklyGlobalAfterMsg) throw new Error("Blockly global disappeared after loading message script.");
2378
+ console.log(`ScratchUI: After en.js load: Blockly defined: ${!!BlocklyGlobalAfterMsg}, Blockly.Msg defined: ${!!(BlocklyGlobalAfterMsg == null ? void 0 : BlocklyGlobalAfterMsg.Msg)}, Keys in Blockly.Msg: ${(BlocklyGlobalAfterMsg == null ? void 0 : BlocklyGlobalAfterMsg.Msg) ? Object.keys(BlocklyGlobalAfterMsg.Msg).length : "N/A"}. Sample Blockly.Msg.LOGIC_HUE: ${(_a = BlocklyGlobalAfterMsg == null ? void 0 : BlocklyGlobalAfterMsg.Msg) == null ? void 0 : _a.LOGIC_HUE}`);
2379
+ if (!BlocklyGlobalAfterMsg.Msg || Object.keys(BlocklyGlobalAfterMsg.Msg).length === 0) {
2380
+ console.warn("ScratchUI: Blockly.Msg appears unpopulated or empty. Checking if Blockly.ScratchMsgs.addLocaleData can be used...");
2381
+ if (BlocklyGlobalAfterMsg.ScratchMsgs && typeof BlocklyGlobalAfterMsg.ScratchMsgs.addLocaleData === "function") {
2382
+ console.log("ScratchUI: Blockly.ScratchMsgs.addLocaleData is available. Attempting to call Blockly.ScratchMsgs.addLocaleData('en').");
2383
+ BlocklyGlobalAfterMsg.ScratchMsgs.addLocaleData("en");
2384
+ BlocklyGlobalAfterMsg = window.Blockly;
2385
+ console.log(`ScratchUI: After attempting addLocaleData('en'): Keys in Blockly.Msg: ${(BlocklyGlobalAfterMsg == null ? void 0 : BlocklyGlobalAfterMsg.Msg) ? Object.keys(BlocklyGlobalAfterMsg.Msg).length : "N/A"}. Sample Blockly.Msg.LOGIC_HUE: ${(_b = BlocklyGlobalAfterMsg == null ? void 0 : BlocklyGlobalAfterMsg.Msg) == null ? void 0 : _b.LOGIC_HUE}`);
2386
+ if (!BlocklyGlobalAfterMsg.Msg || Object.keys(BlocklyGlobalAfterMsg.Msg).length === 0) {
2387
+ throw new Error("Blockly.Msg still empty after calling addLocaleData. Message loading failed critically.");
2388
+ }
2389
+ } else {
2390
+ throw new Error("Blockly.Msg is empty, and Blockly.ScratchMsgs.addLocaleData is not available. Message loading failed.");
2391
+ }
2392
+ }
2393
+ console.log("ScratchUI: Attempting to load Scratch Block Definitions:", SCRATCH_JS_BLOCK_DEFINITIONS_PATH);
2394
+ await loadScript2(SCRATCH_JS_BLOCK_DEFINITIONS_PATH);
2395
+ const BlocklyGlobalBlocks = window.Blockly;
2396
+ if (!BlocklyGlobalBlocks || !BlocklyGlobalBlocks.Blocks) {
2397
+ throw new Error(`Blockly.Blocks not defined after loading ${SCRATCH_JS_BLOCK_DEFINITIONS_PATH}.`);
2398
+ }
2399
+ console.log(`ScratchUI: After block definitions load: Blockly.Blocks defined: ${!!BlocklyGlobalBlocks.Blocks}. Keys: ${BlocklyGlobalBlocks.Blocks ? Object.keys(BlocklyGlobalBlocks.Blocks).slice(0, 10).join(", ") + "..." : "N/A"}. Essential block motion_movesteps defined: ${!!((_c = BlocklyGlobalBlocks.Blocks) == null ? void 0 : _c.motion_movesteps)}`);
2400
+ if (typeof ((_d = BlocklyGlobalBlocks.Blocks) == null ? void 0 : _d.motion_movesteps) === "undefined") {
2401
+ throw new Error(`Essential Scratch blocks (e.g., motion_movesteps) not found after loading block definitions. Available blocks: ${Object.keys(BlocklyGlobalBlocks.Blocks || {}).join(", ")}`);
2402
+ }
2403
+ setIsBlocklyReady(true);
2404
+ console.log("ScratchUI: All Scratch scripts loaded and essential checks passed. Blockly is ready for injection.");
2405
+ } catch (error) {
2406
+ console.error("ScratchUI: Error during Scratch/Blockly script loading sequence:", error);
2407
+ setComponentError(error.message || "Failed to load critical Scratch/Blockly scripts.");
2408
+ setIsBlocklyReady(false);
2409
+ } finally {
2410
+ setIsLoadingScripts(false);
2411
+ }
2412
+ }, []);
2413
+ React25.useEffect(() => {
2414
+ attemptLoadScripts();
2415
+ }, [attemptLoadScripts]);
2416
+ React25.useImperativeHandle(ref, () => ({
2417
+ getWorkspaceXml: () => {
2418
+ const LocalBlockly = window.Blockly;
2419
+ if (workspaceRef.current && (LocalBlockly == null ? void 0 : LocalBlockly.Xml)) {
2420
+ try {
2421
+ const xml = LocalBlockly.Xml.workspaceToDom(workspaceRef.current);
2422
+ return LocalBlockly.Xml.domToText(xml);
2423
+ } catch (e) {
2424
+ console.error("ScratchUI: Error serializing Scratch workspace to XML:", e);
2425
+ return null;
2426
+ }
2427
+ }
2428
+ return null;
2429
+ }
2430
+ }));
2431
+ const initializeWorkspace = React25.useCallback(() => {
2432
+ var _a, _b;
2433
+ const LocalBlockly = window.Blockly;
2434
+ if (!isBlocklyReady || !blocklyDivRef.current || !LocalBlockly) {
2435
+ console.warn("ScratchUI: Conditions not met for workspace initialization. isBlocklyReady:", isBlocklyReady, "blocklyDivRef.current:", !!blocklyDivRef.current, "LocalBlockly:", !!LocalBlockly);
2436
+ return;
2437
+ }
2438
+ if (!LocalBlockly.inject || !LocalBlockly.Xml || !LocalBlockly.Blocks || !LocalBlockly.Msg) {
2439
+ setComponentError("ScratchUI: Essential Blockly library parts (inject, Xml, Blocks, Msg) are not available for injection.");
2440
+ return;
2441
+ }
2442
+ if (Object.keys(LocalBlockly.Msg).length === 0 || !LocalBlockly.Msg.CATEGORY_MOTION) {
2443
+ setComponentError("ScratchUI: Blockly.Msg is empty or essential messages (like CATEGORY_MOTION) are missing. Messages did not load correctly. Injection might fail.");
2444
+ console.error("ScratchUI: Blockly.Msg is empty or missing common keys before injection. Current Msg keys count:", Object.keys(LocalBlockly.Msg).length, "CATEGORY_MOTION:", LocalBlockly.Msg.CATEGORY_MOTION);
2445
+ return;
2446
+ }
2447
+ if (typeof ((_a = LocalBlockly.Blocks) == null ? void 0 : _a.motion_movesteps) === "undefined" || typeof ((_b = LocalBlockly.Blocks) == null ? void 0 : _b.event_whenflagclicked) === "undefined") {
2448
+ const availableBlocks = Object.keys(LocalBlockly.Blocks || {}).join(", ");
2449
+ setComponentError(`ScratchUI: Essential Scratch block definitions (e.g., motion_movesteps, event_whenflagclicked) are missing before injection. Available blocks: ${availableBlocks}`);
2450
+ return;
2451
+ }
2452
+ setComponentError(null);
2453
+ if (workspaceRef.current && typeof workspaceRef.current.dispose === "function") {
2454
+ try {
2455
+ workspaceRef.current.dispose();
2456
+ } catch (e) {
2457
+ console.warn("ScratchUI: Minor error disposing previous workspace instance:", e);
2458
+ }
2459
+ workspaceRef.current = null;
2460
+ }
2461
+ try {
2462
+ console.log("ScratchUI: Attempting to inject Blockly workspace...");
2463
+ const simplifiedToolbox = `
2464
+ <xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-simple-debug" style="display: none">
2465
+ <category name="Events" colour="#FFD500" secondaryColour="#CCAA00">
2466
+
2467
+ <block type="event_whenflagclicked"></block>
2468
+ </category>
2469
+ <category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
2470
+
2471
+ <block type="motion_movesteps">
2472
+ <value name="STEPS"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
2473
+ </block>
2474
+ <block type="motion_turnright">
2475
+ <value name="DEGREES"><shadow type="math_number"><field name="NUM">15</field></shadow></value>
2476
+ </block>
2477
+ </category>
2478
+
2479
+ <category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
2480
+ <block type="looks_sayforsecs">
2481
+ <value name="MESSAGE"><shadow type="text"><field name="TEXT">Hello!</field></shadow></value>
2482
+ <value name="SECS"><shadow type="math_number"><field name="NUM">2</field></shadow></value>
2483
+ </block>
2484
+ </category>
2485
+ </xml>`;
2486
+ const actualToolbox = question.toolboxDefinition || simplifiedToolbox;
2487
+ console.log("ScratchUI: Using Toolbox Definition:", actualToolbox);
2488
+ console.log("ScratchUI: Media path:", SCRATCH_MEDIA_PATH);
2489
+ console.log("ScratchUI: Blockly.Msg.CATEGORY_MOTION at inject time:", LocalBlockly.Msg.CATEGORY_MOTION);
2490
+ console.log("ScratchUI: Blockly.Colours available:", !!LocalBlockly.Colours);
2491
+ const workspace = LocalBlockly.inject(blocklyDivRef.current, {
2492
+ toolbox: actualToolbox,
2493
+ // Use the simplified one for debugging first
2494
+ media: SCRATCH_MEDIA_PATH,
2495
+ scrollbars: true,
2496
+ trashcan: !showCorrectAnswer,
2497
+ readOnly: showCorrectAnswer,
2498
+ renderer: "zelos",
2499
+ zoom: { controls: true, wheel: true, startScale: 0.75, maxScale: 3, minScale: 0.3, scaleSpeed: 1.2 },
2500
+ colours: LocalBlockly.Colours
2501
+ });
2502
+ workspaceRef.current = workspace;
2503
+ console.log("ScratchUI: Workspace injected successfully.");
2504
+ let xmlToLoad = null;
2505
+ if (showCorrectAnswer && question.solutionWorkspaceXML) {
2506
+ xmlToLoad = question.solutionWorkspaceXML;
2507
+ } else if (typeof userAnswer === "string" && userAnswer.trim().startsWith("<xml")) {
2508
+ xmlToLoad = userAnswer;
2509
+ } else if (question.initialWorkspace) {
2510
+ xmlToLoad = question.initialWorkspace;
2511
+ }
2512
+ if (xmlToLoad) {
2513
+ try {
2514
+ console.log("ScratchUI: Attempting to load XML to workspace:", xmlToLoad.substring(0, 100) + "...");
2515
+ const dom = LocalBlockly.Xml.textToDom(xmlToLoad);
2516
+ LocalBlockly.Xml.domToWorkspace(dom, workspace);
2517
+ console.log("ScratchUI: XML loaded to workspace.");
2518
+ } catch (xmlError) {
2519
+ console.error("ScratchUI: Error loading XML to workspace:", xmlError);
2520
+ setComponentError(`ScratchUI: Error loading blocks from XML: ${xmlError.message || String(xmlError)}`);
2521
+ }
2522
+ }
2523
+ if (workspace && LocalBlockly.svgResize) {
2524
+ LocalBlockly.svgResize(workspace);
2525
+ setTimeout(() => {
2526
+ if (workspaceRef.current && LocalBlockly.svgResize) LocalBlockly.svgResize(workspaceRef.current);
2527
+ }, 100);
2528
+ }
2529
+ } catch (e) {
2530
+ console.error("ScratchUI: Error during Blockly.inject or subsequent workspace setup:", e);
2531
+ console.error("ScratchUI: Error Details - Name:", e.name, "Message:", e.message, "Stack:", e.stack);
2532
+ setComponentError(`ScratchUI: Workspace initialization failed: ${e.message || String(e)}. Check console for details. Toolbox used: ${question.toolboxDefinition ? "Custom" : "Default Simplified"}.`);
2533
+ }
2534
+ }, [question, showCorrectAnswer, userAnswer, isBlocklyReady]);
2535
+ React25.useEffect(() => {
2536
+ if (isBlocklyReady) {
2537
+ initializeWorkspace();
2538
+ }
2539
+ const handleResize = () => {
2540
+ const LocalBlocklyResize = window.Blockly;
2541
+ if (workspaceRef.current && (LocalBlocklyResize == null ? void 0 : LocalBlocklyResize.svgResize)) {
2542
+ LocalBlocklyResize.svgResize(workspaceRef.current);
2543
+ }
2544
+ };
2545
+ window.addEventListener("resize", handleResize);
2546
+ return () => {
2547
+ window.removeEventListener("resize", handleResize);
2548
+ if (workspaceRef.current && typeof workspaceRef.current.dispose === "function") {
2549
+ if (window.Blockly) {
2550
+ try {
2551
+ workspaceRef.current.dispose();
2552
+ } catch (e) {
2553
+ console.warn("ScratchUI: Error disposing workspace on unmount:", e);
2554
+ }
2555
+ }
2556
+ workspaceRef.current = null;
2557
+ }
2558
+ };
2559
+ }, [isBlocklyReady, initializeWorkspace]);
2560
+ const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
2561
+ if (isLoadingScripts) {
2562
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { style: { height: workspaceHeight, width: "100%", display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid hsl(var(--border))", borderRadius: "0.375rem", backgroundColor: "hsl(var(--background))" } }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "text-center" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "w-8 h-8 border-4 border-muted border-t-primary rounded-full animate-spin mx-auto mb-2" }), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-muted-foreground" }, "Loading Scratch Assets...")));
2563
+ }
2564
+ if (componentError) {
2565
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { style: { height: workspaceHeight, width: "100%", color: "hsl(var(--destructive))", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", border: "1px solid hsl(var(--destructive))", borderRadius: "0.375rem", padding: "1rem", backgroundColor: "hsl(var(--card))" } }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "font-semibold text-lg mb-2" }, "Failed to load Scratch Workspace."), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-muted-foreground mb-3 text-center" }, componentError), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs text-muted-foreground mb-4 text-center" }, "Please ensure all Scratch/Blockly JavaScript files are correctly copied to your", /* @__PURE__ */ React25__namespace.default.createElement("code", null, "public/static/scratch-blocks/js"), " directory. Check browser console for more details.", /* @__PURE__ */ React25__namespace.default.createElement("br", null), /* @__PURE__ */ React25__namespace.default.createElement("strong", null, "CRITICAL: Ensure you have copied the CSS files from ", /* @__PURE__ */ React25__namespace.default.createElement("code", null, "node_modules/scratch-blocks/css/"), " (e.g., ", /* @__PURE__ */ React25__namespace.default.createElement("code", null, "vertical.css"), ") to ", /* @__PURE__ */ React25__namespace.default.createElement("code", null, "public/static/scratch-blocks/css/"), " and linked it in your main layout. Without CSS, blocks will not render correctly.")), /* @__PURE__ */ React25__namespace.default.createElement(Button, { onClick: attemptLoadScripts, variant: "outline" }, "Try Reloading Scripts"));
2566
+ }
2567
+ if (!isBlocklyReady && !isLoadingScripts) {
2568
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { style: { height: workspaceHeight, width: "100%", display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid hsl(var(--border))", borderRadius: "0.375rem", backgroundColor: "hsl(var(--background))" } }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-muted-foreground" }, "Scratch environment did not initialize (Blockly not ready). Check console for script loading errors."));
2569
+ }
2570
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full border-none shadow-none" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, { className: "p-0 pb-4" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl mb-1 font-body" }, question.prompt), question.points && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-sm text-muted-foreground" }, "Points: ", question.points)), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "p-0" }, /* @__PURE__ */ React25__namespace.default.createElement(
2571
+ "div",
2572
+ {
2573
+ ref: blocklyDivRef,
2574
+ style: {
2575
+ height: workspaceHeight,
2576
+ width: "100%",
2577
+ borderRadius: "0.375rem",
2578
+ backgroundColor: "hsl(var(--card))",
2579
+ position: "relative",
2580
+ userSelect: "none",
2581
+ overflow: "hidden",
2582
+ display: isLoadingScripts || componentError || !isBlocklyReady ? "none" : "block"
2583
+ },
2584
+ "aria-label": `Scratch programming workspace for question: ${question.prompt}`
2585
+ }
2586
+ ), showCorrectAnswer && question.explanation && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm font-semibold text-accent-foreground" }, "Explanation:"), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-accent-foreground/80" }, question.explanation))));
2587
+ });
2588
+ ScratchProgrammingQuestionUI.displayName = "ScratchProgrammingQuestionUI";
2589
+
2590
+ // src/react-ui/components/ui/QuestionRenderer.tsx
2591
+ var QuestionRenderer = React25__namespace.default.forwardRef(({
2592
+ question,
2593
+ onAnswerChange,
2594
+ userAnswer,
2595
+ showCorrectAnswer = false
2596
+ }, ref) => {
2597
+ const commonProps = {
2598
+ question,
2599
+ onAnswerChange,
2600
+ userAnswer,
2601
+ showCorrectAnswer
2602
+ };
2603
+ switch (question.questionType) {
2604
+ case "multiple_choice":
2605
+ return /* @__PURE__ */ React25__namespace.default.createElement(MultipleChoiceQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2606
+ case "true_false":
2607
+ return /* @__PURE__ */ React25__namespace.default.createElement(TrueFalseQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2608
+ case "multiple_response":
2609
+ return /* @__PURE__ */ React25__namespace.default.createElement(MultipleResponseQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2610
+ case "short_answer":
2611
+ return /* @__PURE__ */ React25__namespace.default.createElement(ShortAnswerQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2612
+ case "numeric":
2613
+ return /* @__PURE__ */ React25__namespace.default.createElement(NumericQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2614
+ case "fill_in_the_blanks":
2615
+ return /* @__PURE__ */ React25__namespace.default.createElement(FillInTheBlanksQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2616
+ case "sequence":
2617
+ return /* @__PURE__ */ React25__namespace.default.createElement(SequenceQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2618
+ case "matching":
2619
+ return /* @__PURE__ */ React25__namespace.default.createElement(MatchingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2620
+ case "drag_and_drop":
2621
+ return /* @__PURE__ */ React25__namespace.default.createElement(DragAndDropQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2622
+ case "hotspot":
2623
+ return /* @__PURE__ */ React25__namespace.default.createElement(HotspotQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question }));
2624
+ case "blockly_programming":
2625
+ return /* @__PURE__ */ React25__namespace.default.createElement(BlocklyProgrammingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question, ref }));
2626
+ case "scratch_programming":
2627
+ return /* @__PURE__ */ React25__namespace.default.createElement(ScratchProgrammingQuestionUI, __spreadProps(__spreadValues({}, commonProps), { question, ref }));
2628
+ default:
2629
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "p-4 border border-destructive bg-destructive/10 rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "font-semibold text-destructive" }, "Unsupported Question Type"), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, "The question type is not currently supported by the renderer."), /* @__PURE__ */ React25__namespace.default.createElement("pre", { className: "mt-2 p-2 bg-muted rounded text-xs font-code overflow-x-auto" }, JSON.stringify(question, null, 2)));
2630
+ }
2631
+ });
2632
+ QuestionRenderer.displayName = "QuestionRenderer";
2633
+ var Progress = React25__namespace.forwardRef((_a, ref) => {
2634
+ var _b = _a, { className, value } = _b, props = __objRest(_b, ["className", "value"]);
2635
+ return /* @__PURE__ */ React25__namespace.createElement(
2636
+ ProgressPrimitive__namespace.Root,
2637
+ __spreadValues({
2638
+ ref,
2639
+ className: cn(
2640
+ "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
2641
+ className
2642
+ )
2643
+ }, props),
2644
+ /* @__PURE__ */ React25__namespace.createElement(
2645
+ ProgressPrimitive__namespace.Indicator,
2646
+ {
2647
+ className: "h-full w-full flex-1 bg-primary transition-all",
2648
+ style: { transform: `translateX(-${100 - (value || 0)}%)` }
2649
+ }
2650
+ )
2651
+ );
2652
+ });
2653
+ Progress.displayName = ProgressPrimitive__namespace.Root.displayName;
2654
+ var Accordion = AccordionPrimitive__namespace.Root;
2655
+ var AccordionItem = React25__namespace.forwardRef((_a, ref) => {
2656
+ var _b = _a, { className } = _b, props = __objRest(_b, ["className"]);
2657
+ return /* @__PURE__ */ React25__namespace.createElement(
2658
+ AccordionPrimitive__namespace.Item,
2659
+ __spreadValues({
2660
+ ref,
2661
+ className: cn("border-b", className)
2662
+ }, props)
2663
+ );
2664
+ });
2665
+ AccordionItem.displayName = "AccordionItem";
2666
+ var AccordionTrigger = React25__namespace.forwardRef((_a, ref) => {
2667
+ var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
2668
+ return /* @__PURE__ */ React25__namespace.createElement(AccordionPrimitive__namespace.Header, { className: "flex" }, /* @__PURE__ */ React25__namespace.createElement(
2669
+ AccordionPrimitive__namespace.Trigger,
2670
+ __spreadValues({
2671
+ ref,
2672
+ className: cn(
2673
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
2674
+ className
2675
+ )
2676
+ }, props),
2677
+ children,
2678
+ /* @__PURE__ */ React25__namespace.createElement(lucideReact.ChevronDown, { className: "h-4 w-4 shrink-0 transition-transform duration-200" })
2679
+ ));
2680
+ });
2681
+ AccordionTrigger.displayName = AccordionPrimitive__namespace.Trigger.displayName;
2682
+ var AccordionContent = React25__namespace.forwardRef((_a, ref) => {
2683
+ var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
2684
+ return /* @__PURE__ */ React25__namespace.createElement(
2685
+ AccordionPrimitive__namespace.Content,
2686
+ __spreadValues({
2687
+ ref,
2688
+ className: "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
2689
+ }, props),
2690
+ /* @__PURE__ */ React25__namespace.createElement("div", { className: cn("pb-4 pt-0", className) }, children)
2691
+ );
2692
+ });
2693
+ AccordionContent.displayName = AccordionPrimitive__namespace.Content.displayName;
2694
+ var ScrollArea = React25__namespace.forwardRef((_a, ref) => {
2695
+ var _b = _a, { className, children } = _b, props = __objRest(_b, ["className", "children"]);
2696
+ return /* @__PURE__ */ React25__namespace.createElement(
2697
+ ScrollAreaPrimitive__namespace.Root,
2698
+ __spreadValues({
2699
+ ref,
2700
+ className: cn("relative overflow-hidden", className)
2701
+ }, props),
2702
+ /* @__PURE__ */ React25__namespace.createElement(ScrollAreaPrimitive__namespace.Viewport, { className: "h-full w-full rounded-[inherit]" }, children),
2703
+ /* @__PURE__ */ React25__namespace.createElement(ScrollBar, null),
2704
+ /* @__PURE__ */ React25__namespace.createElement(ScrollAreaPrimitive__namespace.Corner, null)
2705
+ );
2706
+ });
2707
+ ScrollArea.displayName = ScrollAreaPrimitive__namespace.Root.displayName;
2708
+ var ScrollBar = React25__namespace.forwardRef((_a, ref) => {
2709
+ var _b = _a, { className, orientation = "vertical" } = _b, props = __objRest(_b, ["className", "orientation"]);
2710
+ return /* @__PURE__ */ React25__namespace.createElement(
2711
+ ScrollAreaPrimitive__namespace.ScrollAreaScrollbar,
2712
+ __spreadValues({
2713
+ ref,
2714
+ orientation,
2715
+ className: cn(
2716
+ "flex touch-none select-none transition-colors",
2717
+ orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
2718
+ orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
2719
+ className
2720
+ )
2721
+ }, props),
2722
+ /* @__PURE__ */ React25__namespace.createElement(ScrollAreaPrimitive__namespace.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border" })
2723
+ );
2724
+ });
2725
+ ScrollBar.displayName = ScrollAreaPrimitive__namespace.ScrollAreaScrollbar.displayName;
2726
+
2727
+ // src/react-ui/components/ui/QuizResult.tsx
2728
+ var QuizResult = ({ result, quizTitle, onExitQuiz }) => {
2729
+ var _a, _b, _c, _d;
2730
+ const getAnswerDisplay = (answer) => {
2731
+ if (answer === null || answer === void 0) return "Not Answered";
2732
+ if (typeof answer === "boolean") return answer ? "True" : "False";
2733
+ if (Array.isArray(answer)) return answer.join(", ");
2734
+ if (typeof answer === "object") return JSON.stringify(answer);
2735
+ return String(answer);
2736
+ };
2737
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full max-w-3xl mx-auto shadow-xl" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-3xl font-headline text-center" }, "Quiz Results: ", quizTitle), /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, { className: "text-center text-lg" }, "Here's how you performed!")), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "space-y-6" }, /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "bg-secondary/50" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-xl flex items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.BarChart2, { className: "mr-2 h-5 w-5 text-primary" }), "Overall Score")), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "grid grid-cols-1 md:grid-cols-3 gap-4 text-center" }, /* @__PURE__ */ React25__namespace.default.createElement("div", null, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-3xl font-bold text-primary" }, result.score, " / ", result.maxScore), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, "Points")), /* @__PURE__ */ React25__namespace.default.createElement("div", null, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-3xl font-bold text-primary" }, result.percentage.toFixed(2), "%"), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-muted-foreground" }, "Percentage")), /* @__PURE__ */ React25__namespace.default.createElement("div", null, result.passed !== void 0 && (result.passed ? /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex flex-col items-center text-green-600" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.CheckCircle, { className: "h-10 w-10" }), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xl font-semibold mt-1" }, "Passed")) : /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex flex-col items-center text-destructive" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.XCircle, { className: "h-10 w-10" }), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xl font-semibold mt-1" }, "Failed")))))), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.Clock, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ React25__namespace.default.createElement("span", null, "Total time spent:"), /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-semibold" }, (_b = (_a = result.totalTimeSpentSeconds) == null ? void 0 : _a.toFixed(0)) != null ? _b : "N/A", " seconds")), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.Percent, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ React25__namespace.default.createElement("span", null, "Average time per question:"), /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-semibold" }, (_d = (_c = result.averageTimePerQuestionSeconds) == null ? void 0 : _c.toFixed(1)) != null ? _d : "N/A", " seconds"))), result.scormStatus && result.scormStatus !== "idle" && result.scormStatus !== "no_api" && /* @__PURE__ */ React25__namespace.default.createElement(Card, null, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-lg" }, "SCORM Sync Status")), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, null, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: `flex items-center ${result.scormStatus === "error" ? "text-destructive" : "text-muted-foreground"}` }, result.scormStatus === "error" && /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.AlertTriangle, { className: "mr-2 h-4 w-4" }), "Status: ", /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-semibold ml-1" }, result.scormStatus)), result.scormError && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs text-destructive mt-1" }, "Details: ", result.scormError))), result.webhookStatus && result.webhookStatus !== "idle" && /* @__PURE__ */ React25__namespace.default.createElement(Card, null, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-lg" }, "Webhook Sync Status")), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, null, /* @__PURE__ */ React25__namespace.default.createElement("p", { className: `flex items-center ${result.webhookStatus === "error" ? "text-destructive" : "text-muted-foreground"}` }, result.webhookStatus === "error" && /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.AlertTriangle, { className: "mr-2 h-4 w-4" }), "Status: ", /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-semibold ml-1" }, result.webhookStatus)), result.webhookError && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs text-destructive mt-1" }, "Details: ", result.webhookError))), /* @__PURE__ */ React25__namespace.default.createElement(Accordion, { type: "single", collapsible: true, className: "w-full" }, /* @__PURE__ */ React25__namespace.default.createElement(AccordionItem, { value: "question-breakdown" }, /* @__PURE__ */ React25__namespace.default.createElement(AccordionTrigger, { className: "text-lg font-semibold" }, "Detailed Question Breakdown"), /* @__PURE__ */ React25__namespace.default.createElement(AccordionContent, null, /* @__PURE__ */ React25__namespace.default.createElement(ScrollArea, { className: "h-[300px] pr-4" }, /* @__PURE__ */ React25__namespace.default.createElement("ul", { className: "space-y-4" }, result.questionResults.map((qResult, index) => {
2738
+ var _a2, _b2;
2739
+ return /* @__PURE__ */ React25__namespace.default.createElement("li", { key: qResult.questionId, className: "p-4 border rounded-md bg-background" }, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex justify-between items-center mb-2" }, /* @__PURE__ */ React25__namespace.default.createElement("h4", { className: "font-semibold" }, "Question ", index + 1), qResult.isCorrect ? /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "text-green-600 font-medium flex items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.CheckCircle, { className: "mr-1 h-4 w-4" }), " Correct") : /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "text-destructive font-medium flex items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.XCircle, { className: "mr-1 h-4 w-4" }), " Incorrect")), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-medium" }, "Your Answer:"), " ", getAnswerDisplay(qResult.userAnswer)), !qResult.isCorrect && /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm" }, /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-medium" }, "Correct Answer:"), " ", getAnswerDisplay(qResult.correctAnswer)), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs text-muted-foreground" }, /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-medium" }, "Points Earned:"), " ", qResult.pointsEarned), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-xs text-muted-foreground" }, /* @__PURE__ */ React25__namespace.default.createElement("span", { className: "font-medium" }, "Time Spent:"), " ", (_b2 = (_a2 = qResult.timeSpentSeconds) == null ? void 0 : _a2.toFixed(0)) != null ? _b2 : "N/A", "s"));
2740
+ }))))))), /* @__PURE__ */ React25__namespace.default.createElement(CardFooter, null, onExitQuiz && /* @__PURE__ */ React25__namespace.default.createElement(Button, { variant: "outline", onClick: onExitQuiz, className: "w-full" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.LogOut, { className: "mr-2 h-4 w-4" }), "Exit Results")));
2741
+ };
2742
+
2743
+ // src/react-ui/components/ui/QuizPlayer.tsx
2744
+ var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
2745
+ var _a;
2746
+ const [engine, setEngine] = React25.useState(null);
2747
+ const [currentQuestion, setCurrentQuestion] = React25.useState(null);
2748
+ const [currentQuestionNumber, setCurrentQuestionNumber] = React25.useState(0);
2749
+ const [totalQuestions, setTotalQuestions] = React25.useState(0);
2750
+ const [userAnswer, setUserAnswer] = React25.useState(null);
2751
+ const [quizFinished, setQuizFinished] = React25.useState(false);
2752
+ const [finalResult, setFinalResult] = React25.useState(null);
2753
+ const [timeLeft, setTimeLeft] = React25.useState(null);
2754
+ const [isLoading, setIsLoading] = React25.useState(true);
2755
+ const [error, setError] = React25.useState(null);
2756
+ const programmingQuestionRef = React25.useRef(null);
2757
+ const handleAnswerChange = React25.useCallback((answer) => {
2758
+ if ((currentQuestion == null ? void 0 : currentQuestion.questionType) !== "blockly_programming" && (currentQuestion == null ? void 0 : currentQuestion.questionType) !== "scratch_programming") {
2759
+ setUserAnswer(answer);
2760
+ }
2761
+ }, [currentQuestion]);
2762
+ React25.useEffect(() => {
2763
+ setIsLoading(true);
2764
+ setError(null);
2765
+ setQuizFinished(false);
2766
+ setFinalResult(null);
2767
+ setUserAnswer(null);
2768
+ let localQuizEngine = null;
2769
+ try {
2770
+ const callbacks = {
2771
+ onQuizStart: (initialData) => {
2772
+ setCurrentQuestion(initialData.initialQuestion);
2773
+ setCurrentQuestionNumber(initialData.currentQuestionNumber);
2774
+ setTotalQuestions(initialData.totalQuestions);
2775
+ setTimeLeft(initialData.timeLimitInSeconds);
2776
+ setIsLoading(false);
2777
+ },
2778
+ onQuestionChange: (question, qNum, total) => {
2779
+ setCurrentQuestion(question);
2780
+ setCurrentQuestionNumber(qNum);
2781
+ setTotalQuestions(total);
2782
+ const existingAnswer = engine == null ? void 0 : engine.getUserAnswer((question == null ? void 0 : question.id) || "");
2783
+ setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
2784
+ },
2785
+ onQuizFinish: (results) => {
2786
+ setFinalResult(results);
2787
+ setQuizFinished(true);
2788
+ onQuizComplete(results);
2789
+ setIsLoading(false);
2790
+ },
2791
+ onTimeTick: (timeLeftInSeconds) => {
2792
+ setTimeLeft(timeLeftInSeconds);
2793
+ },
2794
+ onQuizTimeUp: () => {
2795
+ setError("Time's up! Your quiz has been submitted automatically.");
2796
+ }
2797
+ };
2798
+ localQuizEngine = new QuizEngine({ config: quizConfig, callbacks });
2799
+ setEngine(localQuizEngine);
2800
+ const initialQ = localQuizEngine.getCurrentQuestion();
2801
+ setCurrentQuestion(initialQ);
2802
+ setCurrentQuestionNumber(localQuizEngine.getCurrentQuestionNumber());
2803
+ setTotalQuestions(localQuizEngine.getTotalQuestions());
2804
+ setTimeLeft(localQuizEngine.getTimeLeftInSeconds());
2805
+ if (initialQ) {
2806
+ const existingAnswer = localQuizEngine.getUserAnswer(initialQ.id);
2807
+ setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
2808
+ }
2809
+ setIsLoading(false);
2810
+ return () => {
2811
+ if (localQuizEngine) {
2812
+ localQuizEngine.destroy();
2813
+ }
2814
+ };
2815
+ } catch (e) {
2816
+ console.error("Error initializing QuizEngine:", e);
2817
+ setError(e instanceof Error ? e.message : "Failed to load quiz.");
2818
+ setIsLoading(false);
2819
+ if (localQuizEngine) {
2820
+ localQuizEngine.destroy();
2821
+ }
2822
+ }
2823
+ }, [quizConfig, onQuizComplete]);
2824
+ const handleSubmitAnswer = () => {
2825
+ var _a2;
2826
+ if (!engine || !currentQuestion) return;
2827
+ let answerToSubmit = null;
2828
+ if (currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming") {
2829
+ if (programmingQuestionRef.current && typeof programmingQuestionRef.current.getWorkspaceXml === "function") {
2830
+ answerToSubmit = programmingQuestionRef.current.getWorkspaceXml();
2831
+ } else {
2832
+ console.warn(`${currentQuestion.questionType} ref not available for submission. Submitting last known answer from engine if any.`);
2833
+ answerToSubmit = (_a2 = engine.getUserAnswer(currentQuestion.id)) != null ? _a2 : null;
2834
+ }
2835
+ } else {
2836
+ answerToSubmit = userAnswer;
2837
+ }
2838
+ if (answerToSubmit !== void 0) {
2839
+ engine.submitAnswer(currentQuestion.id, answerToSubmit);
2840
+ }
2841
+ };
2842
+ const handleNext = () => {
2843
+ if (engine) {
2844
+ handleSubmitAnswer();
2845
+ if (engine.getCurrentQuestionNumber() < engine.getTotalQuestions()) {
2846
+ engine.nextQuestion();
2847
+ } else {
2848
+ handleFinishQuiz();
2849
+ }
2850
+ }
2851
+ };
2852
+ const handlePrevious = () => {
2853
+ if (engine && engine.getCurrentQuestionNumber() > 1) {
2854
+ handleSubmitAnswer();
2855
+ engine.previousQuestion();
2856
+ }
2857
+ };
2858
+ const handleFinishQuiz = async () => {
2859
+ if (engine) {
2860
+ setIsLoading(true);
2861
+ handleSubmitAnswer();
2862
+ await engine.calculateResults();
2863
+ }
2864
+ };
2865
+ const progressPercent = React25.useMemo(() => {
2866
+ if (totalQuestions === 0) return 0;
2867
+ return quizFinished ? 100 : Math.max(0, Math.min(100, (currentQuestionNumber - 1) / totalQuestions * 100));
2868
+ }, [currentQuestionNumber, totalQuestions, quizFinished]);
2869
+ if (isLoading) {
2870
+ return /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex flex-col items-center justify-center h-64" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.Loader2, { className: "h-12 w-12 animate-spin text-primary" }), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "mt-4 text-muted-foreground" }, "Loading Quiz..."));
2871
+ }
2872
+ if (error) {
2873
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full max-w-2xl mx-auto shadow-xl" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-destructive flex items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.AlertCircle, { className: "mr-2 h-6 w-6" }), "Quiz Error")), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, null, /* @__PURE__ */ React25__namespace.default.createElement("p", null, error)), /* @__PURE__ */ React25__namespace.default.createElement(CardFooter, null, onExitQuiz && /* @__PURE__ */ React25__namespace.default.createElement(Button, { variant: "outline", onClick: onExitQuiz }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.LogOut, { className: "mr-2 h-4 w-4" }), " Exit Quiz")));
2874
+ }
2875
+ if (quizFinished && finalResult) {
2876
+ return /* @__PURE__ */ React25__namespace.default.createElement(QuizResult, { result: finalResult, onExitQuiz, quizTitle: quizConfig.title });
2877
+ }
2878
+ if (!currentQuestion) {
2879
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full max-w-2xl mx-auto shadow-xl" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, null, "Quiz Ended"), /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, null, "No more questions, or quiz not loaded correctly.")), /* @__PURE__ */ React25__namespace.default.createElement(CardFooter, null, onExitQuiz && /* @__PURE__ */ React25__namespace.default.createElement(Button, { variant: "outline", onClick: onExitQuiz }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.LogOut, { className: "mr-2 h-4 w-4" }), " Exit Quiz")));
2880
+ }
2881
+ const formatTime = (seconds) => {
2882
+ if (seconds === null) return "-:--";
2883
+ const mins = Math.floor(seconds / 60);
2884
+ const secs = seconds % 60;
2885
+ return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
2886
+ };
2887
+ return /* @__PURE__ */ React25__namespace.default.createElement(Card, { className: "w-full max-w-3xl mx-auto shadow-xl" }, /* @__PURE__ */ React25__namespace.default.createElement(CardHeader, null, /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(CardTitle, { className: "text-2xl font-headline" }, quizConfig.title), timeLeft !== null && /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "flex items-center text-sm text-muted-foreground" }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.Clock, { className: "mr-1 h-4 w-4" }), "Time Left: ", formatTime(timeLeft))), quizConfig.description && /* @__PURE__ */ React25__namespace.default.createElement(CardDescription, null, quizConfig.description), /* @__PURE__ */ React25__namespace.default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React25__namespace.default.createElement(Progress, { value: progressPercent, "aria-label": `Quiz progress: ${currentQuestionNumber} of ${totalQuestions} questions`, className: "w-full" }), /* @__PURE__ */ React25__namespace.default.createElement("p", { className: "text-sm text-muted-foreground mt-1 text-right" }, "Question ", currentQuestionNumber, " of ", totalQuestions))), /* @__PURE__ */ React25__namespace.default.createElement(CardContent, { className: "min-h-[200px]" }, /* @__PURE__ */ React25__namespace.default.createElement(
2888
+ QuestionRenderer,
2889
+ {
2890
+ question: currentQuestion,
2891
+ onAnswerChange: handleAnswerChange,
2892
+ userAnswer,
2893
+ showCorrectAnswer: ((_a = quizConfig.settings) == null ? void 0 : _a.showCorrectAnswers) === "immediately",
2894
+ key: currentQuestion.id,
2895
+ ref: currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming" ? programmingQuestionRef : null
2896
+ }
2897
+ )), /* @__PURE__ */ React25__namespace.default.createElement(CardFooter, { className: "flex justify-between items-center" }, /* @__PURE__ */ React25__namespace.default.createElement(Button, { variant: "outline", onClick: handlePrevious, disabled: currentQuestionNumber <= 1 }, /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.ChevronLeft, { className: "mr-2 h-4 w-4" }), " Previous"), onExitQuiz && /* @__PURE__ */ React25__namespace.default.createElement(Button, { variant: "ghost", onClick: onExitQuiz, className: "text-muted-foreground hover:text-destructive" }, "Exit Quiz"), /* @__PURE__ */ React25__namespace.default.createElement(Button, { onClick: handleNext }, currentQuestionNumber === totalQuestions ? "Finish Quiz" : "Next", currentQuestionNumber !== totalQuestions && /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.ChevronRight, { className: "ml-2 h-4 w-4" }), currentQuestionNumber === totalQuestions && /* @__PURE__ */ React25__namespace.default.createElement(lucideReact.CheckCircle, { className: "ml-2 h-4 w-4" }))));
2898
+ };
2899
+
2900
+ // src/player.ts
2901
+ function mountQuizPlayer(targetElementId, quizConfig) {
2902
+ const targetElement = document.getElementById(targetElementId);
2903
+ if (!targetElement) {
2904
+ console.error(`Quiz Player Mount Error: Element with ID "${targetElementId}" not found.`);
2905
+ document.body.innerHTML = `<p style="color: red; text-align: center; padding: 20px;">Critical Error: Target render element #${targetElementId} not found.</p>`;
2906
+ return;
2907
+ }
2908
+ const AppContainer = () => {
2909
+ const [quizResult, setQuizResult] = React25.useState(null);
2910
+ const handleQuizComplete = (result) => {
2911
+ console.log("Quiz Complete (captured inside React AppContainer):", result);
2912
+ setQuizResult(result);
2913
+ };
2914
+ const handleExit = () => {
2915
+ console.log("Quiz Exited");
2916
+ const rootEl = document.getElementById(targetElementId);
2917
+ if (rootEl) {
2918
+ const root2 = rootEl._reactRootContainer;
2919
+ if (root2) {
2920
+ root2.unmount();
2921
+ }
2922
+ rootEl.innerHTML = '<p style="text-align: center; padding: 20px; color: #4b5563;">Quiz exited. You may now close this window.</p>';
2923
+ }
2924
+ };
2925
+ if (quizResult) {
2926
+ return React25__namespace.default.createElement(QuizResult, {
2927
+ result: quizResult,
2928
+ quizTitle: quizConfig.title,
2929
+ onExitQuiz: handleExit
2930
+ });
2931
+ }
2932
+ return React25__namespace.default.createElement(QuizPlayer, {
2933
+ quizConfig,
2934
+ onQuizComplete: handleQuizComplete,
2935
+ onExitQuiz: handleExit
2936
+ });
2937
+ };
2938
+ const root = ReactDOM__default.default.createRoot(targetElement);
2939
+ root.render(React25__namespace.default.createElement(React25__namespace.default.StrictMode, null, React25__namespace.default.createElement(AppContainer)));
2940
+ }
2941
+
2942
+ exports.mountQuizPlayer = mountQuizPlayer;