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

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