@thanh01.pmt/interactive-quiz-kit 1.0.25 → 1.0.28

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 DELETED
@@ -1,4295 +0,0 @@
1
- import * as React9 from 'react';
2
- import React9__default, { useRef, useState, useImperativeHandle, useCallback, useEffect, forwardRef, useMemo } from 'react';
3
- import ReactDOM from 'react-dom/client';
4
- import { useTranslation } from 'react-i18next';
5
- import { z } from 'zod';
6
- import { genkit } from 'genkit';
7
- import { gemini20Flash, googleAI } from '@genkit-ai/googleai';
8
- import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
9
- import { Circle, Check, ChevronDown, ChevronUp, Loader2, Play, CheckCircle, XCircle, RotateCcw, BarChart2, Clock, Percent, AlertTriangle, LogOut, Wand2, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
10
- import { clsx } from 'clsx';
11
- import { twMerge } from 'tailwind-merge';
12
- import { jsx, jsxs } from 'react/jsx-runtime';
13
- import * as LabelPrimitive from '@radix-ui/react-label';
14
- import { cva } from 'class-variance-authority';
15
- import ReactMarkdown from 'react-markdown';
16
- import remarkGfm from 'remark-gfm';
17
- import rehypeHighlight from 'rehype-highlight';
18
- import remarkMath from 'remark-math';
19
- import rehypeKatex from 'rehype-katex';
20
- import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
21
- import { Slot } from '@radix-ui/react-slot';
22
- import * as SelectPrimitive from '@radix-ui/react-select';
23
- import CodeMirror from '@uiw/react-codemirror';
24
- import { cpp } from '@codemirror/lang-cpp';
25
- import { javascript } from '@codemirror/lang-javascript';
26
- import { python } from '@codemirror/lang-python';
27
- import * as TabsPrimitive from '@radix-ui/react-tabs';
28
- import * as ProgressPrimitive from '@radix-ui/react-progress';
29
- import * as AccordionPrimitive from '@radix-ui/react-accordion';
30
- import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
31
-
32
- // src/player.ts
33
-
34
- // src/services/SCORMService.ts
35
- var SCORM_TRUE = "true";
36
- var SCORM_NO_ERROR = "0";
37
- var CMI_CORE_LESSON_STATUS_PASSED = "passed";
38
- var CMI_CORE_LESSON_STATUS_FAILED = "failed";
39
- var CMI_CORE_LESSON_STATUS_COMPLETED = "completed";
40
- var CMI_CORE_LESSON_STATUS_INCOMPLETE = "incomplete";
41
- var CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED = "not attempted";
42
- var CMI_COMPLETION_STATUS_COMPLETED = "completed";
43
- var CMI_COMPLETION_STATUS_INCOMPLETE = "incomplete";
44
- var CMI_SUCCESS_STATUS_PASSED = "passed";
45
- var CMI_SUCCESS_STATUS_FAILED = "failed";
46
- var SCORMService = class {
47
- constructor(settings) {
48
- this.scormAPI = null;
49
- this.scormVersionFound = null;
50
- this.isInitialized = false;
51
- this.isTerminated = false;
52
- this.studentName = null;
53
- this.settings = {
54
- setCompletionOnFinish: true,
55
- setSuccessOnPass: true,
56
- autoCommit: true,
57
- ...settings
58
- };
59
- if (typeof window !== "undefined") {
60
- this._findAPI();
61
- }
62
- }
63
- _findAPIRecursive(win) {
64
- if (win === null) return null;
65
- if (win.API_1484_11) {
66
- this.scormVersionFound = "2004";
67
- return win.API_1484_11;
68
- }
69
- if (win.API) {
70
- this.scormVersionFound = "1.2";
71
- return win.API;
72
- }
73
- if (win.parent && win.parent !== win) {
74
- return this._findAPIRecursive(win.parent);
75
- }
76
- if (win.opener && typeof win.opener !== "undefined" && win.opener !== win && win.opener !== win.parent) {
77
- try {
78
- if (win.opener.document) {
79
- return this._findAPIRecursive(win.opener);
80
- }
81
- } catch (e) {
82
- console.warn("SCORMService: Could not access win.opener for API search due to cross-origin restrictions.");
83
- }
84
- }
85
- return null;
86
- }
87
- _findAPI() {
88
- try {
89
- this.scormAPI = this._findAPIRecursive(window);
90
- if (this.scormAPI) {
91
- if (!this.scormVersionFound) this.scormVersionFound = this.settings.version;
92
- console.log(`SCORMService: API Found. Version determined: ${this.scormVersionFound}`);
93
- } else {
94
- console.warn("SCORMService: SCORM API not found in window hierarchy.");
95
- }
96
- } catch (e) {
97
- console.error("SCORMService: Error finding SCORM API", e);
98
- this.scormAPI = null;
99
- }
100
- }
101
- hasAPI() {
102
- return this.scormAPI !== null;
103
- }
104
- getSCORMVersion() {
105
- return this.scormVersionFound;
106
- }
107
- initialize() {
108
- if (!this.hasAPI()) return { success: false, error: "SCORM API not found." };
109
- if (this.isInitialized) return { success: true, studentName: this.studentName || void 0 };
110
- const result = this.scormVersionFound === "2004" ? this.scormAPI.Initialize("") : this.scormAPI.LMSInitialize("");
111
- if (result.toString() === SCORM_TRUE || result === true) {
112
- this.isInitialized = true;
113
- this.isTerminated = false;
114
- const studentNameVar = this.settings.studentNameVar || (this.scormVersionFound === "2004" ? "cmi.learner_name" : "cmi.core.student_name");
115
- this.studentName = this.getValue(studentNameVar);
116
- if (this.scormVersionFound === "2004") {
117
- const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
118
- if (this.getValue(completionStatusVar) === "not attempted") {
119
- this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
120
- }
121
- } else {
122
- const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
123
- if (this.getValue(lessonStatusVar) === CMI_CORE_LESSON_STATUS_NOT_ATTEMPTED) {
124
- this.setValue(lessonStatusVar, CMI_CORE_LESSON_STATUS_INCOMPLETE);
125
- }
126
- }
127
- if (this.settings.autoCommit) this.commit();
128
- return { success: true, studentName: this.studentName || void 0 };
129
- } else {
130
- const error = this.getLastError();
131
- return { success: false, error: `Initialization failed: ${error.message}` };
132
- }
133
- }
134
- terminate() {
135
- if (!this.hasAPI() || !this.isInitialized || this.isTerminated) {
136
- const reason = !this.hasAPI() ? "API not found" : !this.isInitialized ? "Not initialized" : "Already terminated";
137
- return { success: !this.hasAPI() || this.isTerminated, error: this.isTerminated ? void 0 : reason };
138
- }
139
- const result = this.scormVersionFound === "2004" ? this.scormAPI.Terminate("") : this.scormAPI.LMSFinish("");
140
- if (result.toString() === SCORM_TRUE || result === true) {
141
- this.isTerminated = true;
142
- this.isInitialized = false;
143
- return { success: true };
144
- } else {
145
- const error = this.getLastError();
146
- return { success: false, error: `Termination failed: ${error.message}` };
147
- }
148
- }
149
- setValue(element, value) {
150
- if (!this.hasAPI() || !this.isInitialized) {
151
- return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
152
- }
153
- const valStr = value.toString();
154
- const result = this.scormVersionFound === "2004" ? this.scormAPI.SetValue(element, valStr) : this.scormAPI.LMSSetValue(element, valStr);
155
- if (result.toString() === SCORM_TRUE || result === true) {
156
- if (this.settings.autoCommit) this.commit();
157
- return { success: true };
158
- } else {
159
- const error = this.getLastError();
160
- return { success: false, error: `SetValue failed for ${element}: ${error.message}` };
161
- }
162
- }
163
- getValue(element) {
164
- if (!this.hasAPI() || !this.isInitialized) return null;
165
- const value = this.scormVersionFound === "2004" ? this.scormAPI.GetValue(element) : this.scormAPI.LMSGetValue(element);
166
- const error = this.getLastError();
167
- if (error.code !== SCORM_NO_ERROR && error.code !== "403" && error.code !== "0") {
168
- console.warn(`SCORMService: GetValue for ${element} produced an error ${error.code}: ${error.message}. Returning raw value:`, value);
169
- }
170
- return value?.toString() ?? null;
171
- }
172
- commit() {
173
- if (!this.hasAPI() || !this.isInitialized) {
174
- return { success: false, error: !this.hasAPI() ? "SCORM API not found." : "SCORM not initialized." };
175
- }
176
- const result = this.scormVersionFound === "2004" ? this.scormAPI.Commit("") : this.scormAPI.LMSCommit("");
177
- if (result.toString() === SCORM_TRUE || result === true) {
178
- return { success: true };
179
- } else {
180
- const error = this.getLastError();
181
- return { success: false, error: `Commit failed: ${error.message}` };
182
- }
183
- }
184
- setScore(rawScore, maxScore, minScore = 0) {
185
- if (!this.hasAPI() || !this.isInitialized) return;
186
- if (this.scormVersionFound === "2004") {
187
- const scoreRawVar = this.settings.scoreRawVar_2004 || this.settings.scoreRawVar || "cmi.score.raw";
188
- const scoreMaxVar = this.settings.scoreMaxVar_2004 || this.settings.scoreMaxVar || "cmi.score.max";
189
- const scoreMinVar = this.settings.scoreMinVar_2004 || this.settings.scoreMinVar || "cmi.score.min";
190
- const scoreScaledVar = this.settings.scoreScaledVar_2004 || "cmi.score.scaled";
191
- this.setValue(scoreMinVar, minScore);
192
- this.setValue(scoreMaxVar, maxScore);
193
- this.setValue(scoreRawVar, rawScore);
194
- if (maxScore > minScore) {
195
- const scaledScore = (rawScore - minScore) / (maxScore - minScore);
196
- this.setValue(scoreScaledVar, parseFloat(scaledScore.toFixed(4)));
197
- } else if (maxScore === minScore && maxScore !== 0) {
198
- this.setValue(scoreScaledVar, rawScore >= maxScore ? 1 : 0);
199
- } else {
200
- this.setValue(scoreScaledVar, 0);
201
- }
202
- } else {
203
- const scoreRawVar = this.settings.scoreRawVar_1_2 || this.settings.scoreRawVar || "cmi.core.score.raw";
204
- const scoreMaxVar = this.settings.scoreMaxVar_1_2 || this.settings.scoreMaxVar || "cmi.core.score.max";
205
- const scoreMinVar = this.settings.scoreMinVar_1_2 || this.settings.scoreMinVar || "cmi.core.score.min";
206
- this.setValue(scoreMinVar, minScore);
207
- this.setValue(scoreMaxVar, maxScore);
208
- this.setValue(scoreRawVar, rawScore);
209
- }
210
- }
211
- setLessonStatus(status, passed) {
212
- if (!this.hasAPI() || !this.isInitialized) return;
213
- if (this.scormVersionFound === "2004") {
214
- const completionStatusVar = this.settings.completionStatusVar_2004 || this.settings.lessonStatusVar || "cmi.completion_status";
215
- const successStatusVar = this.settings.successStatusVar_2004 || "cmi.success_status";
216
- if (this.settings.setCompletionOnFinish && (status === "completed" || status === "passed" || status === "failed")) {
217
- this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_COMPLETED);
218
- } else if (status === "incomplete" || status === "browsed") {
219
- this.setValue(completionStatusVar, CMI_COMPLETION_STATUS_INCOMPLETE);
220
- }
221
- if (this.settings.setSuccessOnPass && passed !== void 0) {
222
- this.setValue(successStatusVar, passed ? CMI_SUCCESS_STATUS_PASSED : CMI_SUCCESS_STATUS_FAILED);
223
- }
224
- } else {
225
- const lessonStatusVar = this.settings.lessonStatusVar_1_2 || this.settings.lessonStatusVar || "cmi.core.lesson_status";
226
- let finalStatus = status;
227
- if (this.settings.setCompletionOnFinish) {
228
- if (this.settings.setSuccessOnPass && passed !== void 0) {
229
- finalStatus = passed ? CMI_CORE_LESSON_STATUS_PASSED : CMI_CORE_LESSON_STATUS_FAILED;
230
- } else {
231
- finalStatus = CMI_CORE_LESSON_STATUS_COMPLETED;
232
- }
233
- } else {
234
- if (status === CMI_CORE_LESSON_STATUS_PASSED || status === CMI_CORE_LESSON_STATUS_FAILED) ; else {
235
- finalStatus = CMI_CORE_LESSON_STATUS_INCOMPLETE;
236
- }
237
- }
238
- this.setValue(lessonStatusVar, finalStatus);
239
- }
240
- }
241
- getLastError() {
242
- if (!this.hasAPI()) return { code: "-1", message: "SCORM API not found." };
243
- const errorCode = this.scormVersionFound === "2004" ? this.scormAPI.GetLastError() : this.scormAPI.LMSGetLastError();
244
- if (errorCode === SCORM_NO_ERROR || errorCode === 0 || errorCode === "0") {
245
- return { code: SCORM_NO_ERROR, message: "No error." };
246
- }
247
- const errorMessage = this.scormVersionFound === "2004" ? this.scormAPI.GetErrorString(errorCode.toString()) : this.scormAPI.LMSGetErrorString(errorCode.toString());
248
- const diagnostic = this.scormVersionFound === "2004" ? this.scormAPI.GetDiagnostic(errorCode.toString()) : this.scormAPI.LMSGetDiagnostic(errorCode.toString());
249
- return {
250
- code: errorCode.toString(),
251
- message: errorMessage?.toString() ?? "Unknown error.",
252
- diagnostic: diagnostic?.toString() ?? void 0
253
- };
254
- }
255
- formatCMITime(totalSeconds) {
256
- const pad = (num, size = 2) => num.toString().padStart(size, "0");
257
- if (this.scormVersionFound === "2004") {
258
- const hours = Math.floor(totalSeconds / 3600);
259
- const minutes = Math.floor(totalSeconds % 3600 / 60);
260
- const seconds = parseFloat((totalSeconds % 60).toFixed(2));
261
- let timeString = "PT";
262
- if (hours > 0) timeString += `${hours}H`;
263
- if (minutes > 0 || hours > 0 && seconds > 0) {
264
- timeString += `${minutes}M`;
265
- }
266
- if (seconds > 0 || timeString === "PT") {
267
- timeString += `${seconds}S`;
268
- }
269
- return timeString === "PT" ? "PT0S" : timeString;
270
- } else {
271
- const hours = Math.floor(totalSeconds / 3600);
272
- const minutes = Math.floor(totalSeconds % 3600 / 60);
273
- const secondsOnly = Math.floor(totalSeconds % 60);
274
- const centiseconds = Math.floor((totalSeconds - Math.floor(totalSeconds)) * 100);
275
- return `${pad(hours, 4)}:${pad(minutes)}:${pad(secondsOnly)}.${pad(centiseconds)}`;
276
- }
277
- }
278
- };
279
-
280
- // src/services/evaluators/multiple-choice-evaluator.ts
281
- var MultipleChoiceEvaluator = class {
282
- async evaluate(question, answer) {
283
- const points = question.points ?? 0;
284
- const correctAnswerId = question.correctAnswerId;
285
- const isCorrect = answer === correctAnswerId;
286
- const correctOption = question.options.find((opt) => opt.id === correctAnswerId);
287
- const correctAnswerDetail = {
288
- id: correctAnswerId,
289
- value: correctOption?.text || ""
290
- };
291
- return Promise.resolve({
292
- isCorrect,
293
- correctAnswer: correctAnswerDetail,
294
- pointsEarned: isCorrect ? points : 0
295
- });
296
- }
297
- };
298
-
299
- // src/services/evaluators/multiple-response-evaluator.ts
300
- var MultipleResponseEvaluator = class {
301
- async evaluate(question, answer) {
302
- const points = question.points ?? 0;
303
- const correctAnswerIds = question.correctAnswerIds;
304
- let isCorrect = false;
305
- if (Array.isArray(answer)) {
306
- const userAnswerSet = new Set(answer);
307
- const correctAnswerSet = new Set(correctAnswerIds);
308
- isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
309
- }
310
- const correctValues = correctAnswerIds.map(
311
- (id) => question.options.find((opt) => opt.id === id)?.text || ""
312
- );
313
- const correctAnswerDetail = {
314
- id: correctAnswerIds,
315
- value: correctValues
316
- };
317
- return Promise.resolve({
318
- isCorrect,
319
- correctAnswer: correctAnswerDetail,
320
- pointsEarned: isCorrect ? points : 0
321
- });
322
- }
323
- };
324
-
325
- // src/services/evaluators/true-false-evaluator.ts
326
- var TrueFalseEvaluator = class {
327
- async evaluate(question, answer) {
328
- const points = question.points ?? 0;
329
- const correctAnswer = question.correctAnswer;
330
- let userAnswer = answer;
331
- if (typeof answer === "string") {
332
- userAnswer = answer.toLowerCase() === "true";
333
- }
334
- const isCorrect = typeof userAnswer === "boolean" && userAnswer === correctAnswer;
335
- const correctAnswerDetail = {
336
- id: null,
337
- value: correctAnswer
338
- };
339
- return Promise.resolve({
340
- isCorrect,
341
- correctAnswer: correctAnswerDetail,
342
- pointsEarned: isCorrect ? points : 0
343
- });
344
- }
345
- };
346
-
347
- // src/services/evaluators/short-answer-evaluator.ts
348
- var ShortAnswerEvaluator = class {
349
- async evaluate(question, answer) {
350
- const points = question.points ?? 0;
351
- let isCorrect = false;
352
- if (typeof answer === "string") {
353
- const userAnswerTrimmed = answer.trim();
354
- const caseSensitive = question.isCaseSensitive ?? false;
355
- isCorrect = question.acceptedAnswers.some(
356
- (accAns) => caseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
357
- );
358
- }
359
- const correctAnswerDetail = {
360
- id: null,
361
- value: question.acceptedAnswers
362
- };
363
- return Promise.resolve({
364
- isCorrect,
365
- correctAnswer: correctAnswerDetail,
366
- pointsEarned: isCorrect ? points : 0
367
- });
368
- }
369
- };
370
-
371
- // src/services/evaluators/numeric-evaluator.ts
372
- var NumericEvaluator = class {
373
- async evaluate(question, answer) {
374
- const points = question.points ?? 0;
375
- let isCorrect = false;
376
- if (typeof answer === "string" || typeof answer === "number") {
377
- const userAnswerNum = parseFloat(String(answer));
378
- if (!isNaN(userAnswerNum)) {
379
- isCorrect = question.tolerance != null ? Math.abs(userAnswerNum - question.answer) <= question.tolerance : userAnswerNum === question.answer;
380
- }
381
- }
382
- const correctAnswerDetail = {
383
- id: null,
384
- value: question.answer
385
- };
386
- return Promise.resolve({
387
- isCorrect,
388
- correctAnswer: correctAnswerDetail,
389
- pointsEarned: isCorrect ? points : 0
390
- });
391
- }
392
- };
393
-
394
- // src/services/evaluators/sequence-evaluator.ts
395
- var SequenceEvaluator = class {
396
- async evaluate(question, answer) {
397
- const points = question.points ?? 0;
398
- let isCorrect = false;
399
- if (Array.isArray(answer) && answer.length === question.correctOrder.length) {
400
- isCorrect = answer.every((itemId, index) => itemId === question.correctOrder[index]);
401
- }
402
- const correctValues = question.correctOrder.map(
403
- (id) => question.items.find((item) => item.id === id)?.content || ""
404
- );
405
- const correctAnswerDetail = {
406
- id: question.correctOrder,
407
- value: correctValues
408
- };
409
- return Promise.resolve({
410
- isCorrect,
411
- correctAnswer: correctAnswerDetail,
412
- pointsEarned: isCorrect ? points : 0
413
- });
414
- }
415
- };
416
-
417
- // src/services/evaluators/matching-evaluator.ts
418
- var MatchingEvaluator = class {
419
- async evaluate(question, answer) {
420
- const points = question.points ?? 0;
421
- let isCorrect = false;
422
- if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
423
- const userAnswerMap = answer;
424
- isCorrect = question.correctAnswerMap.length === Object.keys(userAnswerMap).length && question.correctAnswerMap.every((map) => userAnswerMap[map.promptId] === map.optionId);
425
- }
426
- const correctMap = question.correctAnswerMap.reduce((acc, curr) => {
427
- const promptText = question.prompts.find((p) => p.id === curr.promptId)?.content || "";
428
- const optionText = question.options.find((o) => o.id === curr.optionId)?.content || "";
429
- acc[promptText] = optionText;
430
- return acc;
431
- }, {});
432
- const correctAnswerDetail = {
433
- id: null,
434
- value: correctMap
435
- };
436
- return Promise.resolve({
437
- isCorrect,
438
- correctAnswer: correctAnswerDetail,
439
- pointsEarned: isCorrect ? points : 0
440
- });
441
- }
442
- };
443
-
444
- // src/services/evaluators/fill-in-the-blanks-evaluator.ts
445
- var FillInTheBlanksEvaluator = class {
446
- async evaluate(question, answer) {
447
- const points = question.points ?? 0;
448
- let isCorrect = false;
449
- if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
450
- const userAnswerMap = answer;
451
- isCorrect = question.answers.length > 0 && question.answers.every((correctAnsDef) => {
452
- const userValForBlank = userAnswerMap[correctAnsDef.blankId]?.trim();
453
- if (userValForBlank === void 0) return false;
454
- const caseSensitive = question.isCaseSensitive ?? false;
455
- return correctAnsDef.acceptedValues.some(
456
- (accVal) => caseSensitive ? accVal.trim() === userValForBlank : accVal.trim().toLowerCase() === userValForBlank.toLowerCase()
457
- );
458
- });
459
- }
460
- const correctMap = question.answers.reduce((acc, curr) => {
461
- acc[curr.blankId] = curr.acceptedValues.join(" | ");
462
- return acc;
463
- }, {});
464
- const correctAnswerDetail = {
465
- id: null,
466
- value: correctMap
467
- };
468
- return Promise.resolve({
469
- isCorrect,
470
- correctAnswer: correctAnswerDetail,
471
- pointsEarned: isCorrect ? points : 0
472
- });
473
- }
474
- };
475
-
476
- // src/services/evaluators/drag-and-drop-evaluator.ts
477
- var DragAndDropEvaluator = class {
478
- async evaluate(question, answer) {
479
- const points = question.points ?? 0;
480
- let isCorrect = false;
481
- if (typeof answer === "object" && answer !== null && !Array.isArray(answer)) {
482
- const userAnswerMap = answer;
483
- isCorrect = question.answerMap.length === Object.keys(userAnswerMap).length && question.answerMap.every((map) => userAnswerMap[map.draggableId] === map.dropZoneId);
484
- }
485
- const correctMap = question.answerMap.reduce((acc, curr) => {
486
- const draggableText = question.draggableItems.find((d) => d.id === curr.draggableId)?.content || "";
487
- const dropZoneText = question.dropZones.find((z3) => z3.id === curr.dropZoneId)?.label || "";
488
- acc[draggableText] = dropZoneText;
489
- return acc;
490
- }, {});
491
- const correctAnswerDetail = {
492
- id: null,
493
- value: correctMap
494
- };
495
- return Promise.resolve({
496
- isCorrect,
497
- correctAnswer: correctAnswerDetail,
498
- pointsEarned: isCorrect ? points : 0
499
- });
500
- }
501
- };
502
-
503
- // src/services/evaluators/hotspot-evaluator.ts
504
- var HotspotEvaluator = class {
505
- async evaluate(question, answer) {
506
- const points = question.points ?? 0;
507
- let isCorrect = false;
508
- if (Array.isArray(answer)) {
509
- const userAnswerSet = new Set(answer);
510
- const correctAnswerSet = new Set(question.correctHotspotIds);
511
- isCorrect = userAnswerSet.size === correctAnswerSet.size && [...userAnswerSet].every((id) => correctAnswerSet.has(id));
512
- }
513
- const correctValues = question.correctHotspotIds.map(
514
- (id) => question.hotspots.find((h) => h.id === id)?.description || id
515
- );
516
- const correctAnswerDetail = {
517
- id: question.correctHotspotIds,
518
- value: correctValues
519
- };
520
- return Promise.resolve({
521
- isCorrect,
522
- correctAnswer: correctAnswerDetail,
523
- pointsEarned: isCorrect ? points : 0
524
- });
525
- }
526
- };
527
-
528
- // src/services/evaluators/programming-evaluator.ts
529
- var ProgrammingEvaluator = class {
530
- async evaluate(question, answer) {
531
- const points = question.points ?? 0;
532
- let isCorrect = false;
533
- if (typeof answer === "string" && typeof question.solutionGeneratedCode === "string") {
534
- if (typeof window !== "undefined" && window.Blockly?.JavaScript) {
535
- const LocalBlockly = window.Blockly;
536
- let generatedUserCode = "";
537
- try {
538
- const tempWorkspace = new LocalBlockly.Workspace();
539
- const dom = LocalBlockly.Xml.textToDom(answer);
540
- LocalBlockly.Xml.domToWorkspace(dom, tempWorkspace);
541
- generatedUserCode = LocalBlockly.JavaScript.workspaceToCode(tempWorkspace) || "";
542
- const normalize = (code) => code.replace(/\s+/g, " ").trim();
543
- isCorrect = normalize(generatedUserCode) === normalize(question.solutionGeneratedCode);
544
- tempWorkspace.dispose();
545
- } catch (e) {
546
- console.error(`Error generating code from user's ${question.questionType} XML for evaluation:`, e);
547
- isCorrect = false;
548
- }
549
- } else {
550
- console.warn(`Blockly library not available for ${question.questionType} evaluation. Skipping code comparison.`);
551
- isCorrect = false;
552
- }
553
- }
554
- const correctAnswerDetail = {
555
- id: null,
556
- value: question.solutionGeneratedCode || ""
557
- };
558
- return Promise.resolve({
559
- isCorrect,
560
- correctAnswer: correctAnswerDetail,
561
- pointsEarned: isCorrect ? points : 0
562
- });
563
- }
564
- };
565
-
566
- // src/utils/jsonUtils.ts
567
- var JsonRepairEngine = class {
568
- /**
569
- * Attempts to repair unterminated strings in JSON.
570
- * NOTE: This is a heuristic approach and may not be perfect for all cases.
571
- */
572
- static repairUnterminatedStrings(jsonStr) {
573
- let repaired = jsonStr;
574
- let inString = false;
575
- let escaped = false;
576
- let lastQuoteIndex = -1;
577
- for (let i = 0; i < repaired.length; i++) {
578
- const char = repaired[i];
579
- if (escaped) {
580
- escaped = false;
581
- continue;
582
- }
583
- if (char === "\\") {
584
- escaped = true;
585
- continue;
586
- }
587
- if (char === '"') {
588
- inString = !inString;
589
- if (inString) {
590
- lastQuoteIndex = i;
591
- }
592
- }
593
- }
594
- if (inString && lastQuoteIndex !== -1) {
595
- const beforeUnterminated = repaired.substring(0, lastQuoteIndex + 1);
596
- const afterUnterminated = repaired.substring(lastQuoteIndex + 1);
597
- const breakPoints = [",", "}", "]", "\n"];
598
- let breakIndex = -1;
599
- for (let i = 0; i < afterUnterminated.length; i++) {
600
- if (breakPoints.includes(afterUnterminated[i])) {
601
- breakIndex = i;
602
- break;
603
- }
604
- }
605
- if (breakIndex !== -1) {
606
- const stringContent = afterUnterminated.substring(0, breakIndex);
607
- const remainder = afterUnterminated.substring(breakIndex);
608
- const escapedContent = stringContent.replace(/(?<!\\)"/g, '\\"');
609
- repaired = beforeUnterminated + escapedContent + '"' + remainder;
610
- } else {
611
- const escapedContent = afterUnterminated.replace(/(?<!\\)"/g, '\\"');
612
- repaired = beforeUnterminated + escapedContent + '"';
613
- }
614
- }
615
- return repaired;
616
- }
617
- // FIX: Replaced unsafe single quote replacement with a stateful parser.
618
- /**
619
- * Safely replaces single quotes with double quotes only for keys and string values,
620
- * ignoring apostrophes inside already double-quoted strings.
621
- */
622
- static safelyFixQuotes(jsonStr) {
623
- let result = "";
624
- let inDoubleQuoteString = false;
625
- let escaped = false;
626
- for (let i = 0; i < jsonStr.length; i++) {
627
- const char = jsonStr[i];
628
- if (escaped) {
629
- result += char;
630
- escaped = false;
631
- continue;
632
- }
633
- if (char === "\\") {
634
- escaped = true;
635
- result += char;
636
- continue;
637
- }
638
- if (char === '"') {
639
- inDoubleQuoteString = !inDoubleQuoteString;
640
- }
641
- if (char === "'" && !inDoubleQuoteString) {
642
- result += '"';
643
- } else {
644
- result += char;
645
- }
646
- }
647
- return result;
648
- }
649
- /**
650
- * Fixes common JSON formatting issues using more robust methods.
651
- */
652
- static applyCommonFixes(jsonStr) {
653
- let fixed = jsonStr;
654
- fixed = this.safelyFixQuotes(fixed);
655
- fixed = fixed.replace(/,\s*([}\]])/g, "$1");
656
- fixed = fixed.replace(/("|}|\d|]|true|false|null)\s*\n\s*(")/g, "$1,\n$2");
657
- fixed = fixed.replace(/"[\s\S]*?"/g, (match) => {
658
- const content = match.substring(1, match.length - 1);
659
- const fixedContent = content.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
660
- return `"${fixedContent}"`;
661
- });
662
- fixed = fixed.replace(/"(true|false|null)"/g, "$1");
663
- return fixed;
664
- }
665
- /**
666
- * Validates JSON by attempting to parse and providing detailed error info.
667
- */
668
- static validateAndGetError(jsonStr) {
669
- try {
670
- JSON.parse(jsonStr);
671
- return { isValid: true };
672
- } catch (error) {
673
- const errorMessage = error.message || "";
674
- const positionMatch = errorMessage.match(/position (\d+)/);
675
- const position = positionMatch ? parseInt(positionMatch[1], 10) : void 0;
676
- return {
677
- isValid: false,
678
- error: errorMessage,
679
- position
680
- };
681
- }
682
- }
683
- /**
684
- * Main repair function that attempts multiple strategies.
685
- */
686
- static repairJson(jsonStr) {
687
- let current = jsonStr.trim();
688
- const maxAttempts = 5;
689
- let lastError = "";
690
- let lastPosition = -1;
691
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
692
- const validation = this.validateAndGetError(current);
693
- if (validation.isValid) {
694
- return current;
695
- }
696
- console.warn(`JSON repair attempt ${attempt + 1}: ${validation.error}`);
697
- if (validation.error === lastError && validation.position === lastPosition) {
698
- console.error("Repair attempt stuck on the same error, aborting this strategy.");
699
- if (validation.position) {
700
- const truncated = current.substring(0, validation.position);
701
- const openBraces = (truncated.match(/{/g) || []).length;
702
- const closeBraces = (truncated.match(/}/g) || []).length;
703
- const openBrackets = (truncated.match(/\[/g) || []).length;
704
- const closeBrackets = (truncated.match(/\]/g) || []).length;
705
- let repaired = truncated.replace(/,\s*$/, "");
706
- for (let i = 0; i < openBrackets - closeBrackets; i++) repaired += "]";
707
- for (let i = 0; i < openBraces - closeBraces; i++) repaired += "}";
708
- current = repaired;
709
- const finalValidation = this.validateAndGetError(current);
710
- if (finalValidation.isValid) return current;
711
- }
712
- break;
713
- }
714
- lastError = validation.error || "";
715
- lastPosition = validation.position;
716
- if (validation.error?.includes("Unterminated string")) {
717
- current = this.repairUnterminatedStrings(current);
718
- } else {
719
- current = this.applyCommonFixes(current);
720
- }
721
- }
722
- try {
723
- let finalAttempt = this.applyCommonFixes(jsonStr.trim());
724
- finalAttempt = this.repairUnterminatedStrings(finalAttempt);
725
- JSON.parse(finalAttempt);
726
- return finalAttempt;
727
- } catch (e) {
728
- throw new Error(`Unable to repair JSON after ${maxAttempts} attempts. Last known error: ${lastError}`);
729
- }
730
- }
731
- };
732
- function extractJsonFromMarkdown(text) {
733
- if (!text) {
734
- throw new Error("Input text is empty or null.");
735
- }
736
- const trimmedText = text.trim();
737
- try {
738
- JSON.parse(trimmedText);
739
- return trimmedText;
740
- } catch (e) {
741
- }
742
- const markdownPatterns = [
743
- /```(?:json|JSON)\s*([\s\S]*?)\s*```/,
744
- // ```json ... ```
745
- /```\s*({[\s\S]*?}|\[[\s\S]*?\])\s*```/
746
- // ``` { ... } ``` or ``` [ ... ] ```
747
- ];
748
- for (const pattern of markdownPatterns) {
749
- const match = trimmedText.match(pattern);
750
- if (match && match[1]) {
751
- const content = match[1].trim();
752
- try {
753
- JSON.parse(content);
754
- return content;
755
- } catch (e) {
756
- console.warn("JSON inside markdown block is invalid, attempting repair...");
757
- try {
758
- return JsonRepairEngine.repairJson(content);
759
- } catch (repairError) {
760
- console.warn(`Markdown block repair failed: ${repairError.message}. Trying other strategies...`);
761
- }
762
- }
763
- }
764
- }
765
- const firstBrace = trimmedText.indexOf("{");
766
- const firstBracket = trimmedText.indexOf("[");
767
- let startIndex = -1;
768
- if (firstBrace === -1 && firstBracket === -1) ; else if (firstBrace === -1) {
769
- startIndex = firstBracket;
770
- } else if (firstBracket === -1) {
771
- startIndex = firstBrace;
772
- } else {
773
- startIndex = Math.min(firstBrace, firstBracket);
774
- }
775
- if (startIndex !== -1) {
776
- const textToProcess = trimmedText.substring(startIndex);
777
- let balance = 0;
778
- let inString = false;
779
- let escaped = false;
780
- const startChar = textToProcess[0];
781
- const endChar = startChar === "{" ? "}" : "]";
782
- for (let i = 0; i < textToProcess.length; i++) {
783
- const char = textToProcess[i];
784
- if (escaped) {
785
- escaped = false;
786
- continue;
787
- }
788
- if (char === "\\") {
789
- escaped = true;
790
- continue;
791
- }
792
- if (char === '"') {
793
- inString = !inString;
794
- }
795
- if (!inString) {
796
- if (char === startChar) balance++;
797
- if (char === endChar) balance--;
798
- }
799
- if (balance === 0 && i > 0) {
800
- const potentialJson = textToProcess.substring(0, i + 1);
801
- try {
802
- JSON.parse(potentialJson);
803
- return potentialJson;
804
- } catch (e) {
805
- console.warn(`Balanced JSON segment is invalid, attempting repair...`);
806
- try {
807
- return JsonRepairEngine.repairJson(potentialJson);
808
- } catch (repairError) {
809
- console.warn(`Repair failed for balanced segment: ${repairError.message}`);
810
- }
811
- }
812
- break;
813
- }
814
- }
815
- }
816
- console.warn("All extraction strategies failed, attempting to repair the entire input text as a last resort.");
817
- try {
818
- return JsonRepairEngine.repairJson(trimmedText);
819
- } catch (finalError) {
820
- throw new Error(`Unable to extract or repair valid JSON from AI response. Preview: "${trimmedText.substring(0, 100)}...". Final error: ${finalError.message}`);
821
- }
822
- }
823
- z.object({
824
- language: z.custom(),
825
- problemPrompt: z.string(),
826
- userCode: z.string(),
827
- testCase: z.custom()
828
- });
829
- var AIEvaluationOutputSchema = z.object({
830
- passed: z.boolean().describe("Did the user's code produce the expected output for the given input?"),
831
- actualOutput: z.any().describe("The actual output produced by the user's code."),
832
- reasoning: z.string().describe("A brief explanation of why the code passed or failed, or if there was a syntax error.")
833
- });
834
- var EvaluateUserCodeOutputSchema = AIEvaluationOutputSchema;
835
-
836
- // src/ai/flows/evaluate-user-code.ts
837
- async function evaluateUserCode(clientInput, apiKey) {
838
- try {
839
- const ai = genkit({
840
- plugins: [googleAI({ apiKey })],
841
- model: gemini20Flash
842
- });
843
- const { language, problemPrompt, userCode, testCase } = clientInput;
844
- const promptText = `
845
- You are an expert Code Judge and Teaching Assistant for a ${language} programming course.
846
- Your task is to evaluate a student's code submission for a specific problem against a single test case.
847
-
848
- ## Problem Description
849
- ${problemPrompt}
850
-
851
- ## Student's Code Submission
852
- \`\`\`${language}
853
- ${userCode}
854
- \`\`\`
855
-
856
- ## Test Case to Evaluate
857
- - Input(s): ${JSON.stringify(testCase.input)}
858
- - Expected Output: ${JSON.stringify(testCase.expectedOutput)}
859
-
860
- ## Your Task
861
- 1. **Analyze Execution:** Mentally execute the student's code with the provided input(s).
862
- 2. **Determine Output:** Figure out what the actual output of the code would be.
863
- 3. **Compare:** Compare the actual output with the expected output.
864
- 4. **Handle Errors:** If the code has a syntax error or would crash, treat it as a failure.
865
- 5. **Provide Reasoning:** Briefly explain your conclusion. If it failed, explain why (e.g., "incorrect result", "infinite loop", "syntax error on line 5").
866
-
867
- **CRITICAL JSON OUTPUT FORMAT:**
868
- Return ONLY the JSON object with this EXACT structure.
869
-
870
- \`\`\`json
871
- {
872
- "passed": false,
873
- "actualOutput": 5,
874
- "reasoning": "The function correctly summed the numbers but did not filter for only even numbers."
875
- }
876
- \`\`\`
877
-
878
- Return only the JSON response.`;
879
- const response = await ai.generate(promptText);
880
- const rawText = response.text;
881
- const jsonText = extractJsonFromMarkdown(rawText);
882
- const aiGeneratedContent = JSON.parse(jsonText);
883
- return EvaluateUserCodeOutputSchema.parse(aiGeneratedContent);
884
- } catch (error) {
885
- console.error("Error evaluating user code:", error);
886
- if (error instanceof z.ZodError) {
887
- throw new Error(`AI evaluation output validation failed: ${error.message}`);
888
- }
889
- return {
890
- passed: false,
891
- actualOutput: "Evaluation Error",
892
- reasoning: `The AI judge failed to process the code. Error: ${error.message}`
893
- };
894
- }
895
- }
896
-
897
- // src/services/APIKeyService.ts
898
- var GEMINI_API_KEY_SERVICE_NAME = "gemini";
899
- var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
900
- function _encode(data) {
901
- if (typeof window !== "undefined" && typeof window.btoa === "function") {
902
- try {
903
- return window.btoa(data);
904
- } catch (e) {
905
- console.error("Base64 encoding (btoa) failed:", e);
906
- return data;
907
- }
908
- }
909
- return data;
910
- }
911
- function _decode(data) {
912
- if (typeof window !== "undefined" && typeof window.atob === "function") {
913
- try {
914
- return window.atob(data);
915
- } catch (e) {
916
- console.error("Base64 decoding (atob) failed:", e);
917
- return data;
918
- }
919
- }
920
- return data;
921
- }
922
- var APIKeyService = class {
923
- static getStorageKey(serviceName) {
924
- return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
925
- }
926
- /**
927
- * Saves an API key to localStorage. The key is mildly obfuscated using Base64.
928
- * @param serviceName - The name of the service (e.g., 'gemini').
929
- * @param apiKey - The API key to save.
930
- */
931
- static saveAPIKey(serviceName, apiKey) {
932
- if (typeof window !== "undefined" && window.localStorage) {
933
- try {
934
- const encodedKey = _encode(apiKey);
935
- localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
936
- } catch (e) {
937
- console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
938
- }
939
- } else {
940
- console.warn("localStorage is not available. APIKeyService cannot save keys.");
941
- }
942
- }
943
- /**
944
- * Retrieves an API key from localStorage.
945
- * @param serviceName - The name of the service.
946
- * @returns The decoded API key, or null if not found or if localStorage is unavailable.
947
- */
948
- static getAPIKey(serviceName) {
949
- if (typeof window !== "undefined" && window.localStorage) {
950
- try {
951
- const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
952
- if (storedKey) {
953
- return _decode(storedKey);
954
- }
955
- } catch (e) {
956
- console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
957
- }
958
- }
959
- return null;
960
- }
961
- /**
962
- * Removes an API key from localStorage.
963
- * @param serviceName - The name of the service.
964
- */
965
- static removeAPIKey(serviceName) {
966
- if (typeof window !== "undefined" && window.localStorage) {
967
- try {
968
- localStorage.removeItem(this.getStorageKey(serviceName));
969
- } catch (e) {
970
- console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
971
- }
972
- }
973
- }
974
- /**
975
- * Checks if an API key exists in localStorage for the given service.
976
- * @param serviceName - The name of the service.
977
- * @returns True if a key exists, false otherwise.
978
- */
979
- static hasAPIKey(serviceName) {
980
- return this.getAPIKey(serviceName) !== null;
981
- }
982
- };
983
-
984
- // src/services/CodeEvaluationService.ts
985
- var CodeEvaluationService = class {
986
- constructor() {
987
- this.apiKey = APIKeyService.getAPIKey(GEMINI_API_KEY_SERVICE_NAME);
988
- }
989
- /**
990
- * Evaluates a user's code against a single test case using an AI judge.
991
- * @param question The full CodingQuestion object.
992
- * @param userCode The user's submitted code string.
993
- * @param testCase The specific TestCase to evaluate against.
994
- * @returns A promise that resolves to an EvaluationResult object.
995
- */
996
- async evaluateSingleTestCase(question, userCode, testCase) {
997
- if (!this.apiKey) {
998
- return {
999
- testCaseId: testCase.id,
1000
- passed: false,
1001
- actualOutput: "Configuration Error",
1002
- reasoning: "API Key is not configured."
1003
- };
1004
- }
1005
- const aiResult = await evaluateUserCode({
1006
- language: question.language,
1007
- problemPrompt: question.prompt,
1008
- userCode,
1009
- testCase
1010
- }, this.apiKey);
1011
- return {
1012
- testCaseId: testCase.id,
1013
- ...aiResult
1014
- };
1015
- }
1016
- /**
1017
- * Evaluates user's code against all test cases for a given question.
1018
- * @param question The full CodingQuestion object.
1019
- * @param userCode The user's submitted code string.
1020
- * @returns A promise that resolves to an array of EvaluationResult objects.
1021
- */
1022
- async evaluateAllTestCases(question, userCode) {
1023
- const results = [];
1024
- for (const testCase of question.testCases) {
1025
- const result = await this.evaluateSingleTestCase(question, userCode, testCase);
1026
- results.push(result);
1027
- }
1028
- return results;
1029
- }
1030
- /**
1031
- * Evaluates user's code against only the public test cases for a given question.
1032
- * Useful for a "Run Tests" button before final submission.
1033
- * @param question The full CodingQuestion object.
1034
- * @param userCode The user's submitted code string.
1035
- * @returns A promise that resolves to an array of EvaluationResult objects.
1036
- */
1037
- async evaluatePublicTestCases(question, userCode) {
1038
- const publicTestCases = question.testCases.filter((tc) => tc.isPublic);
1039
- const results = [];
1040
- for (const testCase of publicTestCases) {
1041
- const result = await this.evaluateSingleTestCase(question, userCode, testCase);
1042
- results.push(result);
1043
- }
1044
- return results;
1045
- }
1046
- };
1047
-
1048
- // src/services/evaluators/coding-evaluator.ts
1049
- var CodingEvaluator = class {
1050
- async evaluate(question, answer) {
1051
- const points = question.points ?? 0;
1052
- if (typeof answer !== "string" || !answer.trim()) {
1053
- return {
1054
- isCorrect: false,
1055
- correctAnswer: { id: null, value: question.solutionCode },
1056
- pointsEarned: 0,
1057
- evaluationDetails: question.testCases.map((tc) => ({
1058
- testCaseId: tc.id,
1059
- passed: false,
1060
- actualOutput: "No submission",
1061
- reasoning: "User did not submit any code."
1062
- }))
1063
- };
1064
- }
1065
- try {
1066
- const evaluationService = new CodeEvaluationService();
1067
- const testCaseResults = await evaluationService.evaluateAllTestCases(question, answer);
1068
- const isCorrect = testCaseResults.every((result) => result.passed);
1069
- const correctAnswerDetail = {
1070
- id: null,
1071
- value: question.solutionCode
1072
- };
1073
- return {
1074
- isCorrect,
1075
- correctAnswer: correctAnswerDetail,
1076
- pointsEarned: isCorrect ? points : 0,
1077
- evaluationDetails: testCaseResults
1078
- // Pass through the detailed results
1079
- };
1080
- } catch (error) {
1081
- console.error("A critical error occurred during code evaluation:", error);
1082
- return {
1083
- isCorrect: false,
1084
- correctAnswer: { id: null, value: question.solutionCode },
1085
- pointsEarned: 0,
1086
- evaluationDetails: question.testCases.map((tc) => ({
1087
- testCaseId: tc.id,
1088
- passed: false,
1089
- actualOutput: "Evaluation Error",
1090
- reasoning: error instanceof Error ? error.message : "An unknown error occurred."
1091
- }))
1092
- };
1093
- }
1094
- }
1095
- };
1096
-
1097
- // src/services/QuizEngine.ts
1098
- var QuizEngine = class {
1099
- constructor(options) {
1100
- this.userAnswers = /* @__PURE__ */ new Map();
1101
- this.currentQuestionIndex = 0;
1102
- this.timerId = null;
1103
- this.timeLeftInSeconds = null;
1104
- this.scormService = null;
1105
- this.quizResultState = { scormStatus: "idle" };
1106
- this.questionStartTime = null;
1107
- this.questionTimings = /* @__PURE__ */ new Map();
1108
- this.config = options.config;
1109
- this.callbacks = options.callbacks || {};
1110
- this.questions = this.config.settings?.shuffleQuestions ? [...this.config.questions].sort(() => Math.random() - 0.5) : this.config.questions;
1111
- this.overallStartTime = Date.now();
1112
- this.evaluators = /* @__PURE__ */ new Map();
1113
- this.registerEvaluators();
1114
- if (this.config.settings?.timeLimitMinutes && this.config.settings.timeLimitMinutes > 0) {
1115
- this.timeLeftInSeconds = this.config.settings.timeLimitMinutes * 60;
1116
- }
1117
- if (this.config.settings?.scorm) {
1118
- this.quizResultState.scormStatus = "initializing";
1119
- this.scormService = new SCORMService(this.config.settings.scorm);
1120
- if (this.scormService.hasAPI()) {
1121
- const initResult = this.scormService.initialize();
1122
- if (initResult.success) {
1123
- this.quizResultState.scormStatus = "initialized";
1124
- this.quizResultState.studentName = initResult.studentName;
1125
- } else {
1126
- this.quizResultState.scormStatus = "error";
1127
- this.quizResultState.scormError = initResult.error || "SCORM initialization failed.";
1128
- }
1129
- } else {
1130
- this.quizResultState.scormStatus = "no_api";
1131
- }
1132
- }
1133
- const initialQ = this.getCurrentQuestion();
1134
- if (initialQ) {
1135
- this.questionStartTime = Date.now();
1136
- }
1137
- if (this.callbacks.onQuizStart) {
1138
- this.callbacks.onQuizStart({
1139
- initialQuestion: initialQ,
1140
- currentQuestionNumber: this.getCurrentQuestionNumber(),
1141
- totalQuestions: this.getTotalQuestions(),
1142
- timeLimitInSeconds: this.timeLeftInSeconds,
1143
- scormStatus: this.quizResultState.scormStatus,
1144
- studentName: this.quizResultState.studentName
1145
- });
1146
- }
1147
- if (this.timeLeftInSeconds !== null) {
1148
- this.startTimer();
1149
- }
1150
- this.callbacks.onQuestionChange?.(initialQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1151
- }
1152
- registerEvaluators() {
1153
- this.evaluators.set("multiple_choice", new MultipleChoiceEvaluator());
1154
- this.evaluators.set("multiple_response", new MultipleResponseEvaluator());
1155
- this.evaluators.set("true_false", new TrueFalseEvaluator());
1156
- this.evaluators.set("short_answer", new ShortAnswerEvaluator());
1157
- this.evaluators.set("numeric", new NumericEvaluator());
1158
- this.evaluators.set("sequence", new SequenceEvaluator());
1159
- this.evaluators.set("matching", new MatchingEvaluator());
1160
- this.evaluators.set("fill_in_the_blanks", new FillInTheBlanksEvaluator());
1161
- this.evaluators.set("drag_and_drop", new DragAndDropEvaluator());
1162
- this.evaluators.set("hotspot", new HotspotEvaluator());
1163
- const programmingEvaluator = new ProgrammingEvaluator();
1164
- this.evaluators.set("blockly_programming", programmingEvaluator);
1165
- this.evaluators.set("scratch_programming", programmingEvaluator);
1166
- this.evaluators.set("coding", new CodingEvaluator());
1167
- }
1168
- _recordCurrentQuestionTime() {
1169
- if (this.questionStartTime && this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.questions.length) {
1170
- const currentQId = this.questions[this.currentQuestionIndex].id;
1171
- const elapsedMs = Date.now() - this.questionStartTime;
1172
- const currentTotalTime = this.questionTimings.get(currentQId) || 0;
1173
- this.questionTimings.set(currentQId, currentTotalTime + elapsedMs / 1e3);
1174
- }
1175
- this.questionStartTime = null;
1176
- }
1177
- startTimer() {
1178
- if (this.timerId !== null) clearInterval(this.timerId);
1179
- this.timerId = setInterval(() => this.handleTick(), 1e3);
1180
- }
1181
- stopTimer() {
1182
- if (this.timerId !== null) {
1183
- clearInterval(this.timerId);
1184
- this.timerId = null;
1185
- }
1186
- }
1187
- handleTick() {
1188
- if (this.timeLeftInSeconds === null) return;
1189
- if (this.timeLeftInSeconds > 0) {
1190
- this.timeLeftInSeconds--;
1191
- this.callbacks.onTimeTick?.(this.timeLeftInSeconds);
1192
- }
1193
- if (this.timeLeftInSeconds <= 0) {
1194
- this.stopTimer();
1195
- this.callbacks.onQuizTimeUp?.();
1196
- this.calculateResults();
1197
- }
1198
- }
1199
- getTimeLeftInSeconds() {
1200
- return this.timeLeftInSeconds;
1201
- }
1202
- getCurrentQuestion() {
1203
- return this.questions[this.currentQuestionIndex] || null;
1204
- }
1205
- getCurrentQuestionNumber() {
1206
- return this.currentQuestionIndex + 1;
1207
- }
1208
- getTotalQuestions() {
1209
- return this.questions.length;
1210
- }
1211
- getUserAnswer(questionId) {
1212
- return this.userAnswers.get(questionId);
1213
- }
1214
- isQuizFinished() {
1215
- return this.quizResultState.score !== void 0;
1216
- }
1217
- submitAnswer(questionId, answer) {
1218
- this.userAnswers.set(questionId, answer);
1219
- const question = this.questions.find((q) => q.id === questionId);
1220
- if (question) this.callbacks.onAnswerSubmit?.(question, answer);
1221
- }
1222
- nextQuestion() {
1223
- this._recordCurrentQuestionTime();
1224
- if (this.currentQuestionIndex < this.questions.length - 1) {
1225
- this.currentQuestionIndex++;
1226
- const currentQ = this.getCurrentQuestion();
1227
- this.questionStartTime = Date.now();
1228
- this.callbacks.onQuestionChange?.(currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1229
- return currentQ;
1230
- }
1231
- return null;
1232
- }
1233
- previousQuestion() {
1234
- this._recordCurrentQuestionTime();
1235
- if (this.currentQuestionIndex > 0) {
1236
- this.currentQuestionIndex--;
1237
- const currentQ = this.getCurrentQuestion();
1238
- this.questionStartTime = Date.now();
1239
- this.callbacks.onQuestionChange?.(currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1240
- return currentQ;
1241
- }
1242
- return null;
1243
- }
1244
- goToQuestion(index) {
1245
- if (index >= 0 && index < this.questions.length && index !== this.currentQuestionIndex) {
1246
- this._recordCurrentQuestionTime();
1247
- this.currentQuestionIndex = index;
1248
- const currentQ = this.getCurrentQuestion();
1249
- this.questionStartTime = Date.now();
1250
- this.callbacks.onQuestionChange?.(currentQ, this.getCurrentQuestionNumber(), this.getTotalQuestions());
1251
- return currentQ;
1252
- }
1253
- return this.getCurrentQuestion();
1254
- }
1255
- getElapsedTime() {
1256
- return Date.now() - this.overallStartTime;
1257
- }
1258
- destroy() {
1259
- this.stopTimer();
1260
- this._recordCurrentQuestionTime();
1261
- if (this.scormService && this.scormService.hasAPI()) {
1262
- if (["initialized", "committed", "sending_data"].includes(this.quizResultState.scormStatus || "")) {
1263
- const termResult = this.scormService.terminate();
1264
- if (termResult.success) {
1265
- this.quizResultState.scormStatus = "terminated";
1266
- } else {
1267
- this.quizResultState.scormStatus = "error";
1268
- this.quizResultState.scormError = termResult.error || "SCORM termination failed on destroy.";
1269
- }
1270
- }
1271
- }
1272
- this.scormService = null;
1273
- }
1274
- // (Tiếp theo từ Phần 1)
1275
- async calculateResults() {
1276
- this.stopTimer();
1277
- this._recordCurrentQuestionTime();
1278
- let totalScore = 0;
1279
- let maxScore = 0;
1280
- const questionResultsArray = [];
1281
- let accumulatedTotalTimeSpent = 0;
1282
- for (const question of this.questions) {
1283
- const userAnswerRaw = this.userAnswers.get(question.id) || null;
1284
- maxScore += question.points ?? 0;
1285
- const evaluator = this.evaluators.get(question.questionType);
1286
- if (!evaluator) {
1287
- console.warn(`No evaluator found for question type: ${question.questionType}`);
1288
- questionResultsArray.push({
1289
- questionId: question.id,
1290
- questionType: question.questionType,
1291
- prompt: question.prompt,
1292
- isCorrect: false,
1293
- pointsEarned: 0,
1294
- userAnswer: { id: null, value: userAnswerRaw },
1295
- correctAnswer: { id: null, value: "Evaluation not implemented." },
1296
- timeSpentSeconds: parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2))
1297
- });
1298
- continue;
1299
- }
1300
- const {
1301
- isCorrect,
1302
- correctAnswer: correctAnswerDetail,
1303
- pointsEarned,
1304
- evaluationDetails
1305
- } = await evaluator.evaluate(question, userAnswerRaw);
1306
- totalScore += pointsEarned;
1307
- const timeSpentOnThisQuestion = parseFloat((this.questionTimings.get(question.id) || 0).toFixed(2));
1308
- accumulatedTotalTimeSpent += timeSpentOnThisQuestion;
1309
- const userAnswerDetail = this.formatUserAnswerDetail(question, userAnswerRaw);
1310
- questionResultsArray.push({
1311
- questionId: question.id,
1312
- questionType: question.questionType,
1313
- prompt: question.prompt,
1314
- isCorrect,
1315
- pointsEarned,
1316
- userAnswer: userAnswerDetail,
1317
- correctAnswer: correctAnswerDetail,
1318
- timeSpentSeconds: timeSpentOnThisQuestion,
1319
- evaluationDetails
1320
- });
1321
- }
1322
- const percentage = maxScore > 0 ? parseFloat((totalScore / maxScore * 100).toFixed(2)) : 0;
1323
- let passed = void 0;
1324
- if (this.config.settings?.passingScorePercent != null) {
1325
- passed = percentage >= this.config.settings.passingScorePercent;
1326
- }
1327
- const totalQuizTimeSpentSeconds = parseFloat(accumulatedTotalTimeSpent.toFixed(2));
1328
- const averageTimePerQuestionSeconds = this.questions.length > 0 ? parseFloat((totalQuizTimeSpentSeconds / this.questions.length).toFixed(2)) : 0;
1329
- const metadataPerformance = await this._calculateMetadataPerformance();
1330
- const finalResults = {
1331
- score: totalScore,
1332
- maxScore,
1333
- percentage,
1334
- answers: this.userAnswers,
1335
- questionResults: questionResultsArray,
1336
- passed,
1337
- webhookStatus: "idle",
1338
- scormStatus: this.quizResultState.scormStatus || "idle",
1339
- scormError: this.quizResultState.scormError,
1340
- studentName: this.quizResultState.studentName,
1341
- totalTimeSpentSeconds: totalQuizTimeSpentSeconds,
1342
- averageTimePerQuestionSeconds,
1343
- ...metadataPerformance
1344
- };
1345
- this.quizResultState = { ...this.quizResultState, ...finalResults };
1346
- if (this.config.settings?.scorm) this._sendResultsToSCORM(finalResults);
1347
- await this._sendResultsToWebhook(finalResults);
1348
- this.callbacks.onQuizFinish?.(finalResults);
1349
- return finalResults;
1350
- }
1351
- formatUserAnswerDetail(question, userAnswerRaw) {
1352
- if (userAnswerRaw === null) return null;
1353
- switch (question.questionType) {
1354
- case "multiple_choice": {
1355
- const q = question;
1356
- const id = userAnswerRaw;
1357
- return { id, value: q.options.find((opt) => opt.id === id)?.text || "" };
1358
- }
1359
- case "multiple_response": {
1360
- const q = question;
1361
- const ids = userAnswerRaw;
1362
- const values = ids.map((id) => q.options.find((opt) => opt.id === id)?.text || "");
1363
- return { id: ids, value: values };
1364
- }
1365
- case "sequence": {
1366
- const q = question;
1367
- const ids = userAnswerRaw;
1368
- const values = ids.map((id) => q.items.find((item) => item.id === id)?.content || "");
1369
- return { id: ids, value: values };
1370
- }
1371
- case "matching": {
1372
- const q = question;
1373
- const userAnswerMap = userAnswerRaw;
1374
- const valueMap = {};
1375
- for (const promptId in userAnswerMap) {
1376
- const optionId = userAnswerMap[promptId];
1377
- const promptText = q.prompts.find((p) => p.id === promptId)?.content || "";
1378
- const optionText = q.options.find((o) => o.id === optionId)?.content || "";
1379
- valueMap[promptText] = optionText;
1380
- }
1381
- return { id: null, value: valueMap };
1382
- }
1383
- case "drag_and_drop": {
1384
- const q = question;
1385
- if (typeof userAnswerRaw === "object" && userAnswerRaw !== null && !Array.isArray(userAnswerRaw)) {
1386
- const userAnswerMapByIds = userAnswerRaw;
1387
- const enrichedUserAnswerMap = {};
1388
- for (const draggableId in userAnswerMapByIds) {
1389
- const dropZoneId = userAnswerMapByIds[draggableId];
1390
- const draggableText = q.draggableItems.find((d) => d.id === draggableId)?.content || `(ID: ${draggableId})`;
1391
- const dropZoneText = q.dropZones.find((z3) => z3.id === dropZoneId)?.label || `(ID: ${dropZoneId})`;
1392
- enrichedUserAnswerMap[draggableText] = dropZoneText;
1393
- }
1394
- return { id: null, value: enrichedUserAnswerMap };
1395
- }
1396
- return { id: null, value: userAnswerRaw };
1397
- }
1398
- default:
1399
- return { id: null, value: userAnswerRaw };
1400
- }
1401
- }
1402
- async _calculateMetadataPerformance() {
1403
- const loPerformanceMap = /* @__PURE__ */ new Map();
1404
- const categoryPerformanceMap = /* @__PURE__ */ new Map();
1405
- const topicPerformanceMap = /* @__PURE__ */ new Map();
1406
- const difficultyPerformanceMap = /* @__PURE__ */ new Map();
1407
- const bloomLevelPerformanceMap = /* @__PURE__ */ new Map();
1408
- const updateMap = (map, key, points, isCorrect) => {
1409
- if (!key) return;
1410
- const current = map.get(key) || { totalQuestions: 0, correctQuestions: 0, pointsEarned: 0, maxPoints: 0 };
1411
- current.totalQuestions++;
1412
- current.maxPoints += points;
1413
- if (isCorrect) {
1414
- current.correctQuestions++;
1415
- current.pointsEarned += points;
1416
- }
1417
- map.set(key, current);
1418
- };
1419
- for (const q of this.questions) {
1420
- const userAnswer = this.userAnswers.get(q.id) || null;
1421
- const evaluator = this.evaluators.get(q.questionType);
1422
- if (evaluator) {
1423
- const { isCorrect } = await evaluator.evaluate(q, userAnswer);
1424
- const pointsForThisQuestion = q.points ?? 0;
1425
- updateMap(loPerformanceMap, q.learningObjective, pointsForThisQuestion, isCorrect);
1426
- updateMap(categoryPerformanceMap, q.category, pointsForThisQuestion, isCorrect);
1427
- updateMap(topicPerformanceMap, q.topic, pointsForThisQuestion, isCorrect);
1428
- updateMap(difficultyPerformanceMap, q.difficulty, pointsForThisQuestion, isCorrect);
1429
- updateMap(bloomLevelPerformanceMap, q.bloomLevel, pointsForThisQuestion, isCorrect);
1430
- }
1431
- }
1432
- const formatPerformanceArray = (map, keyName) => {
1433
- return Array.from(map.entries()).map(([key, data]) => ({
1434
- [keyName]: key,
1435
- totalQuestions: data.totalQuestions,
1436
- correctQuestions: data.correctQuestions,
1437
- pointsEarned: data.pointsEarned,
1438
- maxPoints: data.maxPoints,
1439
- percentage: data.maxPoints > 0 ? parseFloat((data.pointsEarned / data.maxPoints * 100).toFixed(2)) : 0
1440
- }));
1441
- };
1442
- return {
1443
- performanceByLearningObjective: formatPerformanceArray(loPerformanceMap, "learningObjective"),
1444
- performanceByCategory: formatPerformanceArray(categoryPerformanceMap, "category"),
1445
- performanceByTopic: formatPerformanceArray(topicPerformanceMap, "topic"),
1446
- performanceByDifficulty: formatPerformanceArray(difficultyPerformanceMap, "difficulty"),
1447
- performanceByBloomLevel: formatPerformanceArray(bloomLevelPerformanceMap, "bloomLevel")
1448
- };
1449
- }
1450
- async _sendResultsToWebhook(results) {
1451
- if (!this.config.settings?.webhookUrl) {
1452
- results.webhookStatus = "idle";
1453
- return;
1454
- }
1455
- results.webhookStatus = "sending";
1456
- try {
1457
- const response = await fetch(this.config.settings.webhookUrl, {
1458
- method: "POST",
1459
- headers: { "Content-Type": "application/json" },
1460
- body: JSON.stringify(results)
1461
- });
1462
- if (response.ok) {
1463
- results.webhookStatus = "success";
1464
- } else {
1465
- results.webhookStatus = "error";
1466
- results.webhookError = `Webhook returned status: ${response.status} ${response.statusText}`;
1467
- try {
1468
- const errorBody = await response.text();
1469
- results.webhookError += ` - Body: ${errorBody.substring(0, 200)}`;
1470
- } catch (e) {
1471
- }
1472
- }
1473
- } catch (error) {
1474
- results.webhookStatus = "error";
1475
- results.webhookError = error instanceof Error ? `Fetch error: ${error.message}` : "Unknown webhook error.";
1476
- }
1477
- }
1478
- _sendResultsToSCORM(results) {
1479
- if (!this.scormService || !this.scormService.hasAPI() || this.quizResultState.scormStatus === "no_api") {
1480
- results.scormStatus = this.quizResultState.scormStatus || "idle";
1481
- return;
1482
- }
1483
- if (this.quizResultState.scormStatus === "error" && this.quizResultState.scormError?.includes("initialization failed")) {
1484
- results.scormStatus = "error";
1485
- results.scormError = this.quizResultState.scormError;
1486
- return;
1487
- }
1488
- results.scormStatus = "sending_data";
1489
- try {
1490
- this.scormService.setScore(results.score, results.maxScore, 0);
1491
- let lessonStatusSetting = "completed";
1492
- if (this.config.settings?.passingScorePercent !== void 0 && this.config.settings?.passingScorePercent !== null) {
1493
- lessonStatusSetting = results.passed ? "passed" : "failed";
1494
- } else if (this.config.settings?.scorm?.setCompletionOnFinish) {
1495
- lessonStatusSetting = "completed";
1496
- }
1497
- this.scormService.setLessonStatus(lessonStatusSetting, results.passed);
1498
- if (results.totalTimeSpentSeconds !== void 0 && this.scormService.formatCMITime) {
1499
- const cmiTime = this.scormService.formatCMITime(results.totalTimeSpentSeconds);
1500
- const sessionTimeVar = this.config.settings?.scorm?.sessionTimeVar || (this.scormService.getSCORMVersion() === "2004" ? "cmi.session_time" : "cmi.core.session_time");
1501
- if (sessionTimeVar) this.scormService.setValue(sessionTimeVar, cmiTime);
1502
- }
1503
- const commitResult = this.scormService.commit();
1504
- if (commitResult.success) {
1505
- results.scormStatus = "committed";
1506
- } else {
1507
- results.scormStatus = "error";
1508
- results.scormError = commitResult.error || "SCORM commit failed.";
1509
- }
1510
- } catch (e) {
1511
- results.scormStatus = "error";
1512
- results.scormError = e instanceof Error ? e.message : "Unknown SCORM data sending error.";
1513
- }
1514
- }
1515
- };
1516
- function cn(...inputs) {
1517
- return twMerge(clsx(inputs));
1518
- }
1519
- var RadioGroup = React9.forwardRef(({ className, ...props }, ref) => {
1520
- return /* @__PURE__ */ jsx(
1521
- RadioGroupPrimitive.Root,
1522
- {
1523
- className: cn("grid gap-2", className),
1524
- ...props,
1525
- ref
1526
- }
1527
- );
1528
- });
1529
- RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
1530
- var RadioGroupItem = React9.forwardRef(({ className, ...props }, ref) => {
1531
- return /* @__PURE__ */ jsx(
1532
- RadioGroupPrimitive.Item,
1533
- {
1534
- ref,
1535
- className: cn(
1536
- "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",
1537
- className
1538
- ),
1539
- ...props,
1540
- children: /* @__PURE__ */ jsx(RadioGroupPrimitive.Indicator, { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(Circle, { className: "h-2.5 w-2.5 fill-current text-current" }) })
1541
- }
1542
- );
1543
- });
1544
- RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
1545
- var labelVariants = cva(
1546
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1547
- );
1548
- var Label = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1549
- LabelPrimitive.Root,
1550
- {
1551
- ref,
1552
- className: cn(labelVariants(), className),
1553
- ...props
1554
- }
1555
- ));
1556
- Label.displayName = LabelPrimitive.Root.displayName;
1557
- var Card = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1558
- "div",
1559
- {
1560
- ref,
1561
- className: cn(
1562
- "rounded-lg border bg-card text-card-foreground shadow-sm",
1563
- className
1564
- ),
1565
- ...props
1566
- }
1567
- ));
1568
- Card.displayName = "Card";
1569
- var CardHeader = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1570
- "div",
1571
- {
1572
- ref,
1573
- className: cn("flex flex-col space-y-1.5 p-6", className),
1574
- ...props
1575
- }
1576
- ));
1577
- CardHeader.displayName = "CardHeader";
1578
- var CardTitle = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1579
- "div",
1580
- {
1581
- ref,
1582
- className: cn(
1583
- "text-2xl font-semibold leading-none tracking-tight",
1584
- className
1585
- ),
1586
- ...props
1587
- }
1588
- ));
1589
- CardTitle.displayName = "CardTitle";
1590
- var CardDescription = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1591
- "div",
1592
- {
1593
- ref,
1594
- className: cn("text-sm text-muted-foreground", className),
1595
- ...props
1596
- }
1597
- ));
1598
- CardDescription.displayName = "CardDescription";
1599
- var CardContent = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("p-6 pt-0", className), ...props }));
1600
- CardContent.displayName = "CardContent";
1601
- var CardFooter = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1602
- "div",
1603
- {
1604
- ref,
1605
- className: cn("flex items-center p-6 pt-0", className),
1606
- ...props
1607
- }
1608
- ));
1609
- CardFooter.displayName = "CardFooter";
1610
- var MarkdownRenderer = ({
1611
- content,
1612
- className
1613
- }) => {
1614
- if (!content) {
1615
- return null;
1616
- }
1617
- const getVideoId = (url) => {
1618
- try {
1619
- const urlObj = new URL(url);
1620
- if (urlObj.hostname.includes("youtube.com") || urlObj.hostname.includes("youtu.be")) {
1621
- const videoId = urlObj.hostname.includes("youtu.be") ? urlObj.pathname.split("/").pop() : urlObj.searchParams.get("v");
1622
- return { platform: "youtube", id: videoId ? videoId : null };
1623
- }
1624
- if (urlObj.hostname.includes("vimeo.com")) {
1625
- const videoId = urlObj.pathname.split("/").pop();
1626
- return { platform: "vimeo", id: videoId ? videoId : null };
1627
- }
1628
- } catch (e) {
1629
- }
1630
- return { platform: null, id: null };
1631
- };
1632
- const processContentForVideos = (text) => {
1633
- const videoUrlRegex = /(^|\s)((https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/|vimeo\.com\/)[^\s]+)($|\s)/g;
1634
- return text.replace(
1635
- videoUrlRegex,
1636
- (match, preWhitespace, url, _p3, _p4, postWhitespace) => {
1637
- const replacement = `
1638
-
1639
- ![Embedded Video](${url.trim()})
1640
-
1641
- `;
1642
- return `${preWhitespace.replace(
1643
- /\n/g,
1644
- ""
1645
- )}${replacement}${postWhitespace.replace(/\n/g, "")}`;
1646
- }
1647
- );
1648
- };
1649
- const processedContent = processContentForVideos(content);
1650
- return (
1651
- // Using Tailwind Typography for beautiful default styling of markdown content
1652
- /* @__PURE__ */ jsx("div", { className: `prose dark:prose-invert max-w-none ${className}`, children: /* @__PURE__ */ jsx(
1653
- ReactMarkdown,
1654
- {
1655
- remarkPlugins: [remarkGfm, remarkMath],
1656
- rehypePlugins: [rehypeHighlight, rehypeKatex],
1657
- components: {
1658
- // Override the default image component to handle videos and responsive images
1659
- img: ({ node, ...props }) => {
1660
- const src = props.src || "";
1661
- const { platform, id } = getVideoId(src);
1662
- if (platform && id) {
1663
- const videoSrc = platform === "youtube" ? `https://www.youtube.com/embed/${id}` : `https://player.vimeo.com/video/${id}`;
1664
- return /* @__PURE__ */ jsx("div", { className: "aspect-w-16 aspect-h-9 my-4", children: /* @__PURE__ */ jsx(
1665
- "iframe",
1666
- {
1667
- src: videoSrc,
1668
- title: props.alt || "Embedded video",
1669
- allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
1670
- allowFullScreen: true,
1671
- className: "w-full h-full rounded-md"
1672
- }
1673
- ) });
1674
- }
1675
- return (
1676
- // eslint-disable-next-line @next/next/no-img-element
1677
- /* @__PURE__ */ jsx(
1678
- "img",
1679
- {
1680
- ...props,
1681
- style: {
1682
- maxWidth: "100%",
1683
- height: "auto",
1684
- borderRadius: "0.5rem",
1685
- margin: "1rem 0"
1686
- },
1687
- alt: props.alt || ""
1688
- }
1689
- )
1690
- );
1691
- },
1692
- // Override the default table to add responsive wrapper
1693
- table: ({ node, ...props }) => /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsx("table", { ...props, className: "my-4 w-full text-sm" }) }),
1694
- // Override default blockquote for better styling
1695
- blockquote: ({ node, ...props }) => /* @__PURE__ */ jsx(
1696
- "blockquote",
1697
- {
1698
- ...props,
1699
- className: "border-l-4 border-primary bg-muted/50 p-4 my-4 italic"
1700
- }
1701
- )
1702
- },
1703
- children: processedContent
1704
- }
1705
- ) })
1706
- );
1707
- };
1708
- var MultipleChoiceQuestionUI = ({
1709
- question,
1710
- onAnswerChange,
1711
- userAnswer,
1712
- showCorrectAnswer = false
1713
- }) => {
1714
- const { prompt, options, points, explanation, correctAnswerId, id: questionId } = question;
1715
- const handleSelection = (value) => {
1716
- onAnswerChange(value);
1717
- };
1718
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
1719
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
1720
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
1721
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
1722
- "Points: ",
1723
- points
1724
- ] })
1725
- ] }),
1726
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
1727
- /* @__PURE__ */ jsx(
1728
- RadioGroup,
1729
- {
1730
- value: userAnswer || void 0,
1731
- onValueChange: handleSelection,
1732
- className: "space-y-3",
1733
- "aria-labelledby": `question-prompt-${questionId}`,
1734
- children: options.map((option) => {
1735
- const isSelected = userAnswer === option.id;
1736
- const isCorrect = option.id === correctAnswerId;
1737
- let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary";
1738
- if (showCorrectAnswer) {
1739
- if (isCorrect) {
1740
- itemClassName += " border-green-500 bg-green-500/10";
1741
- } else if (isSelected && !isCorrect) {
1742
- itemClassName += " border-destructive bg-destructive/10";
1743
- } else {
1744
- itemClassName += " border-muted";
1745
- }
1746
- } else {
1747
- itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
1748
- }
1749
- return /* @__PURE__ */ jsx(
1750
- Label,
1751
- {
1752
- htmlFor: option.id,
1753
- className: itemClassName,
1754
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
1755
- /* @__PURE__ */ jsx(RadioGroupItem, { value: option.id, id: option.id, className: "mr-3" }),
1756
- /* @__PURE__ */ jsx("div", { className: "text-base flex-1", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: option.text }) })
1757
- ] })
1758
- },
1759
- option.id
1760
- );
1761
- })
1762
- }
1763
- ),
1764
- showCorrectAnswer && explanation && /* @__PURE__ */ jsxs("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md", children: [
1765
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Explanation:" }),
1766
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })
1767
- ] })
1768
- ] })
1769
- ] });
1770
- };
1771
- var TrueFalseQuestionUI = ({
1772
- question,
1773
- onAnswerChange,
1774
- userAnswer,
1775
- showCorrectAnswer = false
1776
- }) => {
1777
- const { prompt, points, explanation, correctAnswer, id: questionId } = question;
1778
- const options = [
1779
- { id: `true-${questionId}`, label: "True", value: "true" },
1780
- { id: `false-${questionId}`, label: "False", value: "false" }
1781
- ];
1782
- const handleSelection = (value) => {
1783
- onAnswerChange(value);
1784
- };
1785
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
1786
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
1787
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
1788
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
1789
- "Points: ",
1790
- points
1791
- ] })
1792
- ] }),
1793
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
1794
- /* @__PURE__ */ jsx(
1795
- RadioGroup,
1796
- {
1797
- value: userAnswer || void 0,
1798
- onValueChange: handleSelection,
1799
- className: "space-y-3",
1800
- "aria-labelledby": `question-prompt-${questionId}`,
1801
- children: options.map((option) => {
1802
- const isSelected = userAnswer === option.value;
1803
- const isOptionCorrect = option.value === "true" && correctAnswer === true || option.value === "false" && correctAnswer === false;
1804
- let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary";
1805
- if (showCorrectAnswer) {
1806
- if (isOptionCorrect) {
1807
- itemClassName += " border-green-500 bg-green-500/10";
1808
- } else if (isSelected && !isOptionCorrect) {
1809
- itemClassName += " border-destructive bg-destructive/10";
1810
- } else {
1811
- itemClassName += " border-muted";
1812
- }
1813
- } else {
1814
- itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
1815
- }
1816
- return /* @__PURE__ */ jsx(
1817
- Label,
1818
- {
1819
- htmlFor: option.id,
1820
- className: itemClassName,
1821
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
1822
- /* @__PURE__ */ jsx(RadioGroupItem, { value: option.value, id: option.id, className: "mr-3" }),
1823
- /* @__PURE__ */ jsx("span", { className: "text-base", children: option.label })
1824
- ] })
1825
- },
1826
- option.id
1827
- );
1828
- })
1829
- }
1830
- ),
1831
- showCorrectAnswer && explanation && /* @__PURE__ */ jsxs("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md", children: [
1832
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Explanation:" }),
1833
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })
1834
- ] })
1835
- ] })
1836
- ] });
1837
- };
1838
- var Checkbox = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1839
- CheckboxPrimitive.Root,
1840
- {
1841
- ref,
1842
- className: cn(
1843
- "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",
1844
- className
1845
- ),
1846
- ...props,
1847
- children: /* @__PURE__ */ jsx(
1848
- CheckboxPrimitive.Indicator,
1849
- {
1850
- className: cn("flex items-center justify-center text-current"),
1851
- children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
1852
- }
1853
- )
1854
- }
1855
- ));
1856
- Checkbox.displayName = CheckboxPrimitive.Root.displayName;
1857
- var MultipleResponseQuestionUI = ({
1858
- question,
1859
- onAnswerChange,
1860
- userAnswer,
1861
- showCorrectAnswer = false
1862
- }) => {
1863
- const { prompt, options, points, explanation, correctAnswerIds, id: questionId } = question;
1864
- const handleSelectionChange = (optionId, checked) => {
1865
- const currentAnswers = Array.isArray(userAnswer) ? [...userAnswer] : [];
1866
- let newAnswers;
1867
- if (checked) {
1868
- if (!currentAnswers.includes(optionId)) {
1869
- newAnswers = [...currentAnswers, optionId];
1870
- } else {
1871
- newAnswers = currentAnswers;
1872
- }
1873
- } else {
1874
- newAnswers = currentAnswers.filter((id) => id !== optionId);
1875
- }
1876
- onAnswerChange(newAnswers.length > 0 ? newAnswers : null);
1877
- };
1878
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
1879
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
1880
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
1881
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
1882
- "Points: ",
1883
- points
1884
- ] })
1885
- ] }),
1886
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
1887
- /* @__PURE__ */ jsx("div", { className: "space-y-3", role: "group", "aria-labelledby": `question-prompt-${questionId}`, children: options.map((option) => {
1888
- const isSelected = Array.isArray(userAnswer) && userAnswer.includes(option.id);
1889
- const isCorrectOption = correctAnswerIds.includes(option.id);
1890
- let itemClassName = "p-4 rounded-lg border-2 transition-all cursor-pointer hover:border-primary flex items-center";
1891
- if (showCorrectAnswer) {
1892
- if (isCorrectOption) {
1893
- itemClassName += isSelected ? " border-green-500 bg-green-500/10" : " border-green-500";
1894
- } else {
1895
- itemClassName += isSelected ? " border-destructive bg-destructive/10" : " border-muted";
1896
- }
1897
- } else {
1898
- itemClassName += isSelected ? " border-primary bg-primary/10" : " border-muted";
1899
- }
1900
- return /* @__PURE__ */ jsxs(
1901
- Label,
1902
- {
1903
- htmlFor: option.id,
1904
- className: itemClassName,
1905
- children: [
1906
- /* @__PURE__ */ jsx(
1907
- Checkbox,
1908
- {
1909
- id: option.id,
1910
- checked: isSelected,
1911
- onCheckedChange: (checked) => handleSelectionChange(option.id, !!checked),
1912
- className: "mr-3",
1913
- "aria-label": option.text.replace(/<[^>]*>?/gm, "")
1914
- }
1915
- ),
1916
- /* @__PURE__ */ jsx("div", { className: "text-base flex-1", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: option.text }) })
1917
- ]
1918
- },
1919
- option.id
1920
- );
1921
- }) }),
1922
- showCorrectAnswer && explanation && /* @__PURE__ */ jsxs("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md", children: [
1923
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Explanation:" }),
1924
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })
1925
- ] })
1926
- ] })
1927
- ] });
1928
- };
1929
- var Input = React9.forwardRef(
1930
- ({ className, type, ...props }, ref) => {
1931
- return /* @__PURE__ */ jsx(
1932
- "input",
1933
- {
1934
- type,
1935
- className: cn(
1936
- "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",
1937
- className
1938
- ),
1939
- ref,
1940
- ...props
1941
- }
1942
- );
1943
- }
1944
- );
1945
- Input.displayName = "Input";
1946
- var ShortAnswerQuestionUI = ({
1947
- question,
1948
- onAnswerChange,
1949
- userAnswer,
1950
- showCorrectAnswer = false
1951
- }) => {
1952
- const { prompt, points, explanation, acceptedAnswers, id: questionId, isCaseSensitive } = question;
1953
- const handleInputChange = (event) => {
1954
- onAnswerChange(event.target.value || null);
1955
- };
1956
- const displayUserAnswer = userAnswer || "";
1957
- let isActuallyCorrect = false;
1958
- if (showCorrectAnswer && userAnswer) {
1959
- const userAnswerTrimmed = userAnswer.trim();
1960
- isActuallyCorrect = acceptedAnswers.some(
1961
- (accAns) => isCaseSensitive ? accAns.trim() === userAnswerTrimmed : accAns.trim().toLowerCase() === userAnswerTrimmed.toLowerCase()
1962
- );
1963
- }
1964
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
1965
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
1966
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
1967
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
1968
- "Points: ",
1969
- points
1970
- ] })
1971
- ] }),
1972
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
1973
- /* @__PURE__ */ jsx(Label, { htmlFor: `short-answer-input-${questionId}`, className: "sr-only", children: "Your Answer" }),
1974
- /* @__PURE__ */ jsx(
1975
- Input,
1976
- {
1977
- id: `short-answer-input-${questionId}`,
1978
- type: "text",
1979
- value: displayUserAnswer,
1980
- onChange: handleInputChange,
1981
- placeholder: "Type your answer here...",
1982
- "aria-describedby": explanation ? `explanation-${questionId}` : void 0,
1983
- className: `
1984
- ${showCorrectAnswer && userAnswer ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
1985
- `
1986
- }
1987
- ),
1988
- showCorrectAnswer && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
1989
- userAnswer && !isActuallyCorrect && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: "Your answer was marked incorrect." }),
1990
- /* @__PURE__ */ jsxs("div", { className: "p-3 bg-accent/20 border border-accent rounded-md", children: [
1991
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Accepted Answers:" }),
1992
- /* @__PURE__ */ jsx("ul", { className: "list-disc list-inside text-sm text-accent-foreground/80", children: acceptedAnswers.map((ans, idx) => /* @__PURE__ */ jsx("li", { children: ans }, idx)) }),
1993
- isCaseSensitive && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "(Case-sensitive)" })
1994
- ] }),
1995
- explanation && /* @__PURE__ */ jsxs("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md", children: [
1996
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: "Explanation:" }),
1997
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })
1998
- ] })
1999
- ] })
2000
- ] })
2001
- ] });
2002
- };
2003
- var NumericQuestionUI = ({
2004
- question,
2005
- onAnswerChange,
2006
- userAnswer,
2007
- showCorrectAnswer = false
2008
- }) => {
2009
- const { prompt, points, explanation, answer: correctAnswerValue, tolerance, id: questionId } = question;
2010
- const handleInputChange = (event) => {
2011
- const value = event.target.value;
2012
- if (value === "" || /^-?\d*\.?\d*$/.test(value)) {
2013
- onAnswerChange(value === "" ? null : value);
2014
- }
2015
- };
2016
- const displayUserAnswer = userAnswer || "";
2017
- let isActuallyCorrect = false;
2018
- if (showCorrectAnswer && userAnswer !== null && userAnswer !== "") {
2019
- const userAnswerNum = parseFloat(String(userAnswer));
2020
- if (!isNaN(userAnswerNum)) {
2021
- isActuallyCorrect = tolerance !== void 0 && tolerance !== null ? Math.abs(userAnswerNum - correctAnswerValue) <= tolerance : userAnswerNum === correctAnswerValue;
2022
- }
2023
- }
2024
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
2025
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
2026
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
2027
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
2028
- "Points: ",
2029
- points
2030
- ] })
2031
- ] }),
2032
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
2033
- /* @__PURE__ */ jsx(Label, { htmlFor: `numeric-input-${questionId}`, className: "sr-only", children: "Your Answer" }),
2034
- /* @__PURE__ */ jsx(
2035
- Input,
2036
- {
2037
- id: `numeric-input-${questionId}`,
2038
- type: "text",
2039
- inputMode: "numeric",
2040
- pattern: "[0-9]*\\.?[0-9]*",
2041
- value: displayUserAnswer,
2042
- onChange: handleInputChange,
2043
- placeholder: "Enter a number",
2044
- "aria-describedby": explanation ? `explanation-${questionId}` : void 0,
2045
- className: `
2046
- ${showCorrectAnswer && userAnswer !== null && userAnswer !== "" ? isActuallyCorrect ? "border-green-500 focus-visible:ring-green-500" : "border-destructive focus-visible:ring-destructive" : "border-input"}
2047
- `
2048
- }
2049
- ),
2050
- showCorrectAnswer && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
2051
- userAnswer !== null && userAnswer !== "" && !isActuallyCorrect && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: "Your answer was marked incorrect." }),
2052
- /* @__PURE__ */ jsxs("div", { className: "p-3 bg-accent/20 border border-accent rounded-md", children: [
2053
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Correct Answer:" }),
2054
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-accent-foreground/80", children: [
2055
- correctAnswerValue,
2056
- tolerance !== void 0 && tolerance !== null && tolerance > 0 && ` (Tolerance: \xB1${tolerance}, Accepted range: ${correctAnswerValue - tolerance} to ${correctAnswerValue + tolerance})`
2057
- ] })
2058
- ] }),
2059
- explanation && /* @__PURE__ */ jsxs("div", { id: `explanation-${questionId}`, className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md", children: [
2060
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: "Explanation:" }),
2061
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })
2062
- ] })
2063
- ] })
2064
- ] })
2065
- ] });
2066
- };
2067
- var FillInTheBlanksQuestionUI = ({
2068
- question,
2069
- onAnswerChange,
2070
- userAnswer,
2071
- showCorrectAnswer = false
2072
- }) => {
2073
- const { prompt, segments, answers: correctAnswersMap, points, explanation, id: questionId, isCaseSensitive } = question;
2074
- const [userInputs, setUserInputs] = useState({});
2075
- useEffect(() => {
2076
- if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
2077
- setUserInputs(userAnswer);
2078
- } else {
2079
- const initialInputs = {};
2080
- segments.forEach((segment) => {
2081
- if (segment.type === "blank" && segment.id) {
2082
- initialInputs[segment.id] = "";
2083
- }
2084
- });
2085
- setUserInputs(initialInputs);
2086
- }
2087
- }, [segments, userAnswer]);
2088
- const handleInputChange = (blankId, value) => {
2089
- const newInputs = { ...userInputs, [blankId]: value };
2090
- setUserInputs(newInputs);
2091
- const hasValue = Object.values(newInputs).some((val) => val.trim() !== "");
2092
- onAnswerChange(hasValue ? newInputs : null);
2093
- };
2094
- const getCorrectnessForBlank = (blankId) => {
2095
- if (!showCorrectAnswer || !userInputs[blankId]) return null;
2096
- const userAnswerForBlank = userInputs[blankId]?.trim();
2097
- const correctAnswerDef = correctAnswersMap.find((a) => a.blankId === blankId);
2098
- if (!correctAnswerDef || !userAnswerForBlank) return false;
2099
- const caseSensitive = isCaseSensitive === void 0 ? false : isCaseSensitive;
2100
- return correctAnswerDef.acceptedValues.some(
2101
- (accVal) => caseSensitive ? accVal.trim() === userAnswerForBlank : accVal.trim().toLowerCase() === userAnswerForBlank.toLowerCase()
2102
- );
2103
- };
2104
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
2105
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
2106
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
2107
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
2108
- "Points: ",
2109
- points
2110
- ] })
2111
- ] }),
2112
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
2113
- /* @__PURE__ */ jsx("div", { className: "text-base leading-relaxed flex flex-wrap items-center gap-x-1", "aria-labelledby": `question-prompt-${questionId}`, children: segments.map((segment, index) => {
2114
- if (segment.type === "text") {
2115
- return /* @__PURE__ */ jsx(
2116
- MarkdownRenderer,
2117
- {
2118
- content: segment.content || "",
2119
- className: "inline"
2120
- },
2121
- `text-${index}`
2122
- );
2123
- }
2124
- if (segment.type === "blank" && segment.id) {
2125
- const blankId = segment.id;
2126
- const isCorrect = getCorrectnessForBlank(blankId);
2127
- let inputClassName = "inline-block w-auto min-w-[100px] max-w-[200px] h-8 mx-1 align-baseline text-base";
2128
- if (showCorrectAnswer && userInputs[blankId]?.trim()) {
2129
- inputClassName += isCorrect ? " border-green-500 focus-visible:ring-green-500" : " border-destructive focus-visible:ring-destructive";
2130
- } else {
2131
- inputClassName += " border-input";
2132
- }
2133
- return /* @__PURE__ */ jsx(
2134
- Input,
2135
- {
2136
- id: blankId,
2137
- type: "text",
2138
- value: userInputs[blankId] || "",
2139
- onChange: (e) => handleInputChange(blankId, e.target.value),
2140
- placeholder: "\u0110i\u1EC1n...",
2141
- className: inputClassName,
2142
- "aria-label": `Blank ${index + 1}`,
2143
- disabled: showCorrectAnswer
2144
- },
2145
- blankId
2146
- );
2147
- }
2148
- return null;
2149
- }) }),
2150
- showCorrectAnswer && explanation && /* @__PURE__ */ jsxs("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md", children: [
2151
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Gi\u1EA3i th\xEDch chung:" }),
2152
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })
2153
- ] }),
2154
- showCorrectAnswer && /* @__PURE__ */ jsx("div", { className: "mt-4 space-y-3", children: correctAnswersMap.map((ansDef) => {
2155
- const isBlankCorrect = getCorrectnessForBlank(ansDef.blankId);
2156
- const userAnswerDisplay = userInputs[ansDef.blankId] || "Ch\u01B0a tr\u1EA3 l\u1EDDi";
2157
- return /* @__PURE__ */ jsxs("div", { className: `p-2 border rounded-md ${isBlankCorrect ? "border-green-500/50 bg-green-500/10" : "border-destructive/50 bg-destructive/10"}`, children: [
2158
- /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
2159
- /* @__PURE__ */ jsxs("span", { className: "font-semibold", children: [
2160
- "\xD4 tr\u1ED1ng '",
2161
- segments.find((s) => s.id === ansDef.blankId && s.type === "blank")?.id || ansDef.blankId,
2162
- "':"
2163
- ] }),
2164
- ' B\u1EA1n \u0111\xE3 \u0111i\u1EC1n: "',
2165
- userAnswerDisplay,
2166
- '".'
2167
- ] }),
2168
- /* @__PURE__ */ jsxs("p", { className: "text-xs", children: [
2169
- "\u0110\xE1p \xE1n ch\u1EA5p nh\u1EADn: ",
2170
- ansDef.acceptedValues.join(", ")
2171
- ] })
2172
- ] }, `feedback-${ansDef.blankId}`);
2173
- }) })
2174
- ] })
2175
- ] });
2176
- };
2177
- var buttonVariants = cva(
2178
- "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",
2179
- {
2180
- variants: {
2181
- variant: {
2182
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
2183
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
2184
- outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
2185
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
2186
- ghost: "hover:bg-accent hover:text-accent-foreground",
2187
- link: "text-primary underline-offset-4 hover:underline"
2188
- },
2189
- size: {
2190
- default: "h-10 px-4 py-2",
2191
- sm: "h-9 rounded-md px-3",
2192
- lg: "h-11 rounded-md px-8",
2193
- icon: "h-10 w-10"
2194
- }
2195
- },
2196
- defaultVariants: {
2197
- variant: "default",
2198
- size: "default"
2199
- }
2200
- }
2201
- );
2202
- var Button = React9.forwardRef(
2203
- ({ className, variant, size, asChild = false, ...props }, ref) => {
2204
- const Comp = asChild ? Slot : "button";
2205
- return /* @__PURE__ */ jsx(
2206
- Comp,
2207
- {
2208
- className: cn(buttonVariants({ variant, size, className })),
2209
- ref,
2210
- ...props
2211
- }
2212
- );
2213
- }
2214
- );
2215
- Button.displayName = "Button";
2216
- var SequenceQuestionUI = ({
2217
- question,
2218
- onAnswerChange,
2219
- userAnswer,
2220
- showCorrectAnswer = false
2221
- }) => {
2222
- const { prompt, items, points, explanation, id: questionId, correctOrder } = question;
2223
- const [selectedSequence, setSelectedSequence] = useState([]);
2224
- const [availableItems, setAvailableItems] = useState([]);
2225
- useEffect(() => {
2226
- const initialUserOrder = Array.isArray(userAnswer) ? userAnswer : [];
2227
- const initialSelected = [];
2228
- const initialAvailable = [...items];
2229
- initialUserOrder.forEach((itemId) => {
2230
- const item = items.find((i) => i.id === itemId);
2231
- if (item) {
2232
- initialSelected.push(item);
2233
- const itemIndexInAvailable = initialAvailable.findIndex((i) => i.id === itemId);
2234
- if (itemIndexInAvailable > -1) {
2235
- initialAvailable.splice(itemIndexInAvailable, 1);
2236
- }
2237
- }
2238
- });
2239
- setSelectedSequence(initialSelected);
2240
- setAvailableItems(initialAvailable);
2241
- }, [items, userAnswer]);
2242
- const handleSelectItem = (item) => {
2243
- if (showCorrectAnswer) return;
2244
- const newSelectedSequence = [...selectedSequence, item];
2245
- setSelectedSequence(newSelectedSequence);
2246
- setAvailableItems(availableItems.filter((i) => i.id !== item.id));
2247
- onAnswerChange(newSelectedSequence.map((i) => i.id));
2248
- };
2249
- const handleRemoveFromSequence = (itemToRemove, index) => {
2250
- if (showCorrectAnswer) return;
2251
- const newSelectedSequence = selectedSequence.filter((_, i) => i !== index);
2252
- setSelectedSequence(newSelectedSequence);
2253
- setAvailableItems([...availableItems, itemToRemove].sort((a, b) => items.findIndex((i) => i.id === a.id) - items.findIndex((i) => i.id === b.id)));
2254
- onAnswerChange(newSelectedSequence.length > 0 ? newSelectedSequence.map((i) => i.id) : null);
2255
- };
2256
- const handleResetSequence = () => {
2257
- if (showCorrectAnswer) return;
2258
- setSelectedSequence([]);
2259
- setAvailableItems([...items]);
2260
- onAnswerChange(null);
2261
- };
2262
- const getFeedbackIcon = (index) => {
2263
- if (!showCorrectAnswer || !Array.isArray(userAnswer) || userAnswer.length <= index) return null;
2264
- const userItemId = userAnswer[index];
2265
- const correctItemId = correctOrder[index];
2266
- return userItemId === correctItemId ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 text-green-500 ml-2 flex-shrink-0" }) : /* @__PURE__ */ jsx(XCircle, { className: "h-4 w-4 text-destructive ml-2 flex-shrink-0" });
2267
- };
2268
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
2269
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
2270
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
2271
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
2272
- "Points: ",
2273
- points
2274
- ] })
2275
- ] }),
2276
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0 space-y-6", children: [
2277
- /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2278
- /* @__PURE__ */ jsx(Label, { className: "font-semibold", children: "S\u1EAFp x\u1EBFp c\xE1c m\u1EE5c sau theo \u0111\xFAng th\u1EE9 t\u1EF1:" }),
2279
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2", children: availableItems.map((item) => /* @__PURE__ */ jsx(
2280
- Button,
2281
- {
2282
- variant: "outline",
2283
- onClick: () => handleSelectItem(item),
2284
- className: "justify-start text-left h-auto py-2 px-3 whitespace-normal",
2285
- disabled: showCorrectAnswer,
2286
- children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: item.content })
2287
- },
2288
- item.id
2289
- )) }),
2290
- availableItems.length === 0 && selectedSequence.length > 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: '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.' })
2291
- ] }),
2292
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2293
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center", children: [
2294
- /* @__PURE__ */ jsx(Label, { className: "font-semibold", children: "Th\u1EE9 t\u1EF1 b\u1EA1n \u0111\xE3 ch\u1ECDn:" }),
2295
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: handleResetSequence, disabled: showCorrectAnswer || selectedSequence.length === 0, children: [
2296
- /* @__PURE__ */ jsx(RotateCcw, { className: "mr-2 h-3.5 w-3.5" }),
2297
- " \u0110\u1EB7t l\u1EA1i"
2298
- ] })
2299
- ] }),
2300
- selectedSequence.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground p-3 border border-dashed rounded-md", children: "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__ */ jsx("ul", { className: "space-y-2", children: selectedSequence.map((item, index) => /* @__PURE__ */ jsxs(
2301
- "li",
2302
- {
2303
- onClick: () => handleRemoveFromSequence(item, index),
2304
- className: `flex items-center justify-between p-3 border rounded-md whitespace-normal ${showCorrectAnswer ? 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`,
2305
- children: [
2306
- /* @__PURE__ */ jsxs("div", { className: "flex-grow flex items-center", children: [
2307
- /* @__PURE__ */ jsxs("span", { className: "font-semibold mr-2", children: [
2308
- index + 1,
2309
- "."
2310
- ] }),
2311
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: item.content })
2312
- ] }),
2313
- showCorrectAnswer ? getFeedbackIcon(index) : /* @__PURE__ */ jsx(XCircle, { className: "h-4 w-4 text-muted-foreground hover:text-destructive flex-shrink-0" })
2314
- ]
2315
- },
2316
- item.id
2317
- )) })
2318
- ] }),
2319
- showCorrectAnswer && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
2320
- /* @__PURE__ */ jsxs("div", { className: "p-3 bg-accent/20 border border-accent rounded-md", children: [
2321
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Th\u1EE9 t\u1EF1 \u0111\xFAng:" }),
2322
- /* @__PURE__ */ jsx("ol", { className: "list-decimal list-inside text-sm text-accent-foreground/80 space-y-1 mt-1", children: correctOrder.map((itemId) => {
2323
- const item = items.find((i) => i.id === itemId);
2324
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: item ? item.content : "Kh\xF4ng t\xECm th\u1EA5y m\u1EE5c" }) }, itemId);
2325
- }) })
2326
- ] }),
2327
- explanation && /* @__PURE__ */ jsxs("div", { className: "mt-2 p-3 bg-muted/30 border border-muted rounded-md", children: [
2328
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: "Gi\u1EA3i th\xEDch:" }),
2329
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-muted-foreground" })
2330
- ] })
2331
- ] })
2332
- ] })
2333
- ] });
2334
- };
2335
- var Select = SelectPrimitive.Root;
2336
- var SelectValue = SelectPrimitive.Value;
2337
- var SelectTrigger = React9.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
2338
- SelectPrimitive.Trigger,
2339
- {
2340
- ref,
2341
- className: cn(
2342
- "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",
2343
- className
2344
- ),
2345
- ...props,
2346
- children: [
2347
- children,
2348
- /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 opacity-50" }) })
2349
- ]
2350
- }
2351
- ));
2352
- SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
2353
- var SelectScrollUpButton = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2354
- SelectPrimitive.ScrollUpButton,
2355
- {
2356
- ref,
2357
- className: cn(
2358
- "flex cursor-default items-center justify-center py-1",
2359
- className
2360
- ),
2361
- ...props,
2362
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4" })
2363
- }
2364
- ));
2365
- SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
2366
- var SelectScrollDownButton = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2367
- SelectPrimitive.ScrollDownButton,
2368
- {
2369
- ref,
2370
- className: cn(
2371
- "flex cursor-default items-center justify-center py-1",
2372
- className
2373
- ),
2374
- ...props,
2375
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" })
2376
- }
2377
- ));
2378
- SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
2379
- var SelectContent = React9.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
2380
- SelectPrimitive.Content,
2381
- {
2382
- ref,
2383
- className: cn(
2384
- "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",
2385
- 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",
2386
- className
2387
- ),
2388
- position,
2389
- ...props,
2390
- children: [
2391
- /* @__PURE__ */ jsx(SelectScrollUpButton, {}),
2392
- /* @__PURE__ */ jsx(
2393
- SelectPrimitive.Viewport,
2394
- {
2395
- className: cn(
2396
- "p-1",
2397
- position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
2398
- ),
2399
- children
2400
- }
2401
- ),
2402
- /* @__PURE__ */ jsx(SelectScrollDownButton, {})
2403
- ]
2404
- }
2405
- ) }));
2406
- SelectContent.displayName = SelectPrimitive.Content.displayName;
2407
- var SelectLabel = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2408
- SelectPrimitive.Label,
2409
- {
2410
- ref,
2411
- className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className),
2412
- ...props
2413
- }
2414
- ));
2415
- SelectLabel.displayName = SelectPrimitive.Label.displayName;
2416
- var SelectItem = React9.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
2417
- SelectPrimitive.Item,
2418
- {
2419
- ref,
2420
- className: cn(
2421
- "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",
2422
- className
2423
- ),
2424
- ...props,
2425
- children: [
2426
- /* @__PURE__ */ jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) }) }),
2427
- /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
2428
- ]
2429
- }
2430
- ));
2431
- SelectItem.displayName = SelectPrimitive.Item.displayName;
2432
- var SelectSeparator = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2433
- SelectPrimitive.Separator,
2434
- {
2435
- ref,
2436
- className: cn("-mx-1 my-1 h-px bg-muted", className),
2437
- ...props
2438
- }
2439
- ));
2440
- SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
2441
- var MatchingQuestionUI = ({
2442
- question,
2443
- onAnswerChange,
2444
- userAnswer,
2445
- showCorrectAnswer = false
2446
- }) => {
2447
- const { prompt, prompts, options: initialOptions, points, explanation, correctAnswerMap, id: questionId } = question;
2448
- const [currentAnswers, setCurrentAnswers] = useState({});
2449
- const [shuffledOptions, setShuffledOptions] = useState(initialOptions);
2450
- useEffect(() => {
2451
- if (question.shuffleOptions) {
2452
- setShuffledOptions([...initialOptions].sort(() => Math.random() - 0.5));
2453
- } else {
2454
- setShuffledOptions(initialOptions);
2455
- }
2456
- }, [initialOptions, question.shuffleOptions]);
2457
- useEffect(() => {
2458
- if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
2459
- setCurrentAnswers(userAnswer);
2460
- } else {
2461
- const initial = {};
2462
- prompts.forEach((p) => initial[p.id] = "");
2463
- setCurrentAnswers(initial);
2464
- }
2465
- }, [userAnswer, prompts]);
2466
- const handleSelectChange = (promptId, optionId) => {
2467
- const newAnswers = { ...currentAnswers, [promptId]: optionId };
2468
- setCurrentAnswers(newAnswers);
2469
- const hasSelection = Object.values(newAnswers).some((val) => val && val !== "");
2470
- onAnswerChange(hasSelection ? newAnswers : null);
2471
- };
2472
- const getCorrectOptionIdForPrompt = (promptId) => {
2473
- return correctAnswerMap.find((map) => map.promptId === promptId)?.optionId;
2474
- };
2475
- const getPlainText = (htmlString) => {
2476
- if (!htmlString) return "";
2477
- if (typeof document !== "undefined") {
2478
- const tempDiv = document.createElement("div");
2479
- tempDiv.innerHTML = htmlString;
2480
- return tempDiv.textContent || tempDiv.innerText || "";
2481
- }
2482
- return htmlString.replace(/<[^>]*>?/gm, "");
2483
- };
2484
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
2485
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
2486
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
2487
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
2488
- "Points: ",
2489
- points
2490
- ] })
2491
- ] }),
2492
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0 space-y-4", children: [
2493
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4", children: prompts.map((promptItem) => {
2494
- const selectedOptionId = currentAnswers[promptItem.id] || "";
2495
- const correctOptionId = getCorrectOptionIdForPrompt(promptItem.id);
2496
- const isSelectionCorrect = showCorrectAnswer && selectedOptionId ? selectedOptionId === correctOptionId : null;
2497
- let borderColor = "border-muted";
2498
- if (showCorrectAnswer && selectedOptionId) {
2499
- borderColor = isSelectionCorrect ? "border-green-500" : "border-destructive";
2500
- }
2501
- return /* @__PURE__ */ jsxs("div", { className: `p-3 border rounded-md ${borderColor} transition-colors bg-background`, children: [
2502
- /* @__PURE__ */ jsx(Label, { htmlFor: `select-prompt-${promptItem.id}`, className: "font-medium text-base block mb-2 whitespace-normal", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: promptItem.content }) }),
2503
- /* @__PURE__ */ jsxs(
2504
- Select,
2505
- {
2506
- value: selectedOptionId,
2507
- onValueChange: (value) => handleSelectChange(promptItem.id, value),
2508
- disabled: showCorrectAnswer,
2509
- children: [
2510
- /* @__PURE__ */ jsx(SelectTrigger, { id: `select-prompt-${promptItem.id}`, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Ch\u1ECDn \u0111\xE1p \xE1n..." }) }),
2511
- /* @__PURE__ */ jsx(SelectContent, { children: shuffledOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option.id, className: "whitespace-normal", children: getPlainText(option.content) }, option.id)) })
2512
- ]
2513
- }
2514
- ),
2515
- showCorrectAnswer && selectedOptionId && (isSelectionCorrect ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-5 w-5 text-green-500 mt-2 inline-block" }) : /* @__PURE__ */ jsx(XCircle, { className: "h-5 w-5 text-destructive mt-2 inline-block" })),
2516
- showCorrectAnswer && !selectedOptionId && correctOptionId && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [
2517
- "Ch\u01B0a ch\u1ECDn. \u0110\xE1p \xE1n \u0111\xFAng: ",
2518
- getPlainText(shuffledOptions.find((o) => o.id === correctOptionId)?.content)
2519
- ] })
2520
- ] }, promptItem.id);
2521
- }) }),
2522
- showCorrectAnswer && explanation && /* @__PURE__ */ jsxs("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md", children: [
2523
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Gi\u1EA3i th\xEDch:" }),
2524
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })
2525
- ] }),
2526
- showCorrectAnswer && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
2527
- /* @__PURE__ */ jsx("p", { className: "font-semibold text-md", children: "\u0110\xE1p \xE1n \u0111\xFAng:" }),
2528
- /* @__PURE__ */ jsx("ul", { className: "list-disc list-inside space-y-1 text-sm", children: correctAnswerMap.map((map) => {
2529
- const promptText = prompts.find((p) => p.id === map.promptId)?.content;
2530
- const optionText = initialOptions.find((o) => o.id === map.optionId)?.content;
2531
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: `<strong>${getPlainText(promptText) || "N/A"}</strong> gh\xE9p v\u1EDBi <strong>${getPlainText(optionText) || "N/A"}</strong>` }) }, map.promptId);
2532
- }) })
2533
- ] })
2534
- ] })
2535
- ] });
2536
- };
2537
- var DragAndDropQuestionUI = ({
2538
- question,
2539
- onAnswerChange,
2540
- userAnswer,
2541
- showCorrectAnswer = false
2542
- }) => {
2543
- const { prompt, draggableItems, dropZones, points, explanation, answerMap, id: questionId, backgroundImageUrl } = question;
2544
- const [currentAnswers, setCurrentAnswers] = useState({});
2545
- useEffect(() => {
2546
- if (userAnswer && typeof userAnswer === "object" && !Array.isArray(userAnswer)) {
2547
- setCurrentAnswers(userAnswer);
2548
- } else {
2549
- const initial = {};
2550
- draggableItems.forEach((item) => initial[item.id] = "");
2551
- setCurrentAnswers(initial);
2552
- }
2553
- }, [userAnswer, draggableItems]);
2554
- const handleSelectChange = (draggableItemId, dropZoneId) => {
2555
- const newAnswers = { ...currentAnswers, [draggableItemId]: dropZoneId };
2556
- setCurrentAnswers(newAnswers);
2557
- const hasSelection = Object.values(newAnswers).some((val) => val && val !== "");
2558
- onAnswerChange(hasSelection ? newAnswers : null);
2559
- };
2560
- const getCorrectDropZoneIdForDraggable = (draggableItemId) => {
2561
- return answerMap.find((map) => map.draggableId === draggableItemId)?.dropZoneId;
2562
- };
2563
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
2564
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
2565
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
2566
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
2567
- "Points: ",
2568
- points
2569
- ] })
2570
- ] }),
2571
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0 space-y-4", children: [
2572
- backgroundImageUrl && /* @__PURE__ */ jsx("div", { className: "mb-4 overflow-hidden rounded-md border", children: /* @__PURE__ */ jsx("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" }) }),
2573
- /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2574
- /* @__PURE__ */ jsx(Label, { className: "font-semibold", children: "Gh\xE9p c\xE1c m\u1EE5c sau v\xE0o \u0111\xFAng v\u1ECB tr\xED:" }),
2575
- draggableItems.map((item) => {
2576
- const selectedDropZoneId = currentAnswers[item.id] || "";
2577
- const correctDropZoneId = getCorrectDropZoneIdForDraggable(item.id);
2578
- const isSelectionCorrect = showCorrectAnswer && selectedDropZoneId ? selectedDropZoneId === correctDropZoneId : null;
2579
- let itemStyle = "flex flex-col sm:flex-row items-start sm:items-center justify-between p-3 border rounded-md transition-colors bg-background";
2580
- if (showCorrectAnswer && selectedDropZoneId) {
2581
- itemStyle += isSelectionCorrect ? " border-green-500" : " border-destructive";
2582
- } else {
2583
- itemStyle += " border-muted";
2584
- }
2585
- return /* @__PURE__ */ jsxs("div", { className: itemStyle, children: [
2586
- /* @__PURE__ */ jsx(Label, { htmlFor: `select-draggable-${item.id}`, className: "font-medium text-base mb-2 sm:mb-0 sm:mr-4 flex-1", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: item.content }) }),
2587
- /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2 w-full sm:w-auto", children: [
2588
- /* @__PURE__ */ jsxs(
2589
- Select,
2590
- {
2591
- value: selectedDropZoneId,
2592
- onValueChange: (value) => handleSelectChange(item.id, value),
2593
- disabled: showCorrectAnswer,
2594
- children: [
2595
- /* @__PURE__ */ jsx(SelectTrigger, { id: `select-draggable-${item.id}`, className: "w-full sm:min-w-[200px]", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Ch\u1ECDn v\u1ECB tr\xED..." }) }),
2596
- /* @__PURE__ */ jsx(SelectContent, { children: dropZones.map((zone) => /* @__PURE__ */ jsx(SelectItem, { value: zone.id, children: zone.label.replace(/<[^>]*>?/gm, "") }, zone.id)) })
2597
- ]
2598
- }
2599
- ),
2600
- showCorrectAnswer && selectedDropZoneId && (isSelectionCorrect ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-5 w-5 text-green-500 flex-shrink-0" }) : /* @__PURE__ */ jsx(XCircle, { className: "h-5 w-5 text-destructive flex-shrink-0" }))
2601
- ] })
2602
- ] }, item.id);
2603
- })
2604
- ] }),
2605
- showCorrectAnswer && explanation && /* @__PURE__ */ jsxs("div", { className: "mt-6 p-3 bg-accent/20 border border-accent rounded-md", children: [
2606
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Gi\u1EA3i th\xEDch:" }),
2607
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })
2608
- ] }),
2609
- showCorrectAnswer && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
2610
- /* @__PURE__ */ jsx("p", { className: "font-semibold text-md", children: "\u0110\xE1p \xE1n \u0111\xFAng:" }),
2611
- /* @__PURE__ */ jsx("ul", { className: "list-disc list-inside space-y-1 text-sm", children: answerMap.map((map) => {
2612
- const draggableText = draggableItems.find((d) => d.id === map.draggableId)?.content;
2613
- const dropZoneText = dropZones.find((z3) => z3.id === map.dropZoneId)?.label;
2614
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: `<strong>${draggableText || "N/A"}</strong> v\xE0o <strong>${dropZoneText || "N/A"}</strong>` }) }, map.draggableId);
2615
- }) })
2616
- ] })
2617
- ] })
2618
- ] });
2619
- };
2620
- var HotspotQuestionUI = ({
2621
- question,
2622
- onAnswerChange,
2623
- userAnswer,
2624
- showCorrectAnswer = false
2625
- }) => {
2626
- const { prompt, imageUrl, imageAltText, hotspots, points, explanation, correctAnswerIds, id: questionId } = question;
2627
- const [selectedIds, setSelectedIds] = useState([]);
2628
- useEffect(() => {
2629
- if (Array.isArray(userAnswer)) {
2630
- setSelectedIds(userAnswer);
2631
- } else {
2632
- setSelectedIds([]);
2633
- }
2634
- }, [userAnswer]);
2635
- const handleHotspotClick = (hotspotId) => {
2636
- if (showCorrectAnswer) return;
2637
- const newSelectedIds = selectedIds.includes(hotspotId) ? selectedIds.filter((id) => id !== hotspotId) : [...selectedIds, hotspotId];
2638
- setSelectedIds(newSelectedIds);
2639
- onAnswerChange(newSelectedIds.length > 0 ? newSelectedIds : null);
2640
- };
2641
- const getHotspotStyle = (hotspot) => {
2642
- const style = {
2643
- position: "absolute",
2644
- border: "2px dashed transparent",
2645
- cursor: showCorrectAnswer ? "default" : "pointer",
2646
- transition: "border-color 0.2s, background-color 0.2s"
2647
- };
2648
- if (hotspot.shape === "rect") {
2649
- style.left = `${hotspot.coords[0]}px`;
2650
- style.top = `${hotspot.coords[1]}px`;
2651
- style.width = `${hotspot.coords[2]}px`;
2652
- style.height = `${hotspot.coords[3]}px`;
2653
- } else if (hotspot.shape === "circle") {
2654
- style.left = `${hotspot.coords[0] - hotspot.coords[2]}px`;
2655
- style.top = `${hotspot.coords[1] - hotspot.coords[2]}px`;
2656
- style.width = `${hotspot.coords[2] * 2}px`;
2657
- style.height = `${hotspot.coords[2] * 2}px`;
2658
- style.borderRadius = "50%";
2659
- }
2660
- const isSelected = selectedIds.includes(hotspot.id);
2661
- const safeCorrectAnswerIds = correctAnswerIds || [];
2662
- const isCorrect = safeCorrectAnswerIds.includes(hotspot.id);
2663
- if (showCorrectAnswer) {
2664
- if (isCorrect && isSelected) {
2665
- style.borderColor = "hsl(var(--success-foreground, 142.1 70.6% 45.3%))";
2666
- style.backgroundColor = "hsla(var(--success-foreground, 142.1 70.6% 45.3%), 0.2)";
2667
- } else if (!isCorrect && isSelected) {
2668
- style.borderColor = "hsl(var(--destructive))";
2669
- style.backgroundColor = "hsla(var(--destructive), 0.2)";
2670
- } else if (isCorrect && !isSelected) {
2671
- style.borderColor = "hsl(var(--primary))";
2672
- style.borderStyle = "solid";
2673
- }
2674
- } else if (isSelected) {
2675
- style.borderColor = "hsl(var(--primary))";
2676
- style.backgroundColor = "hsla(var(--primary), 0.1)";
2677
- } else {
2678
- style.borderColor = "hsla(var(--muted-foreground), 0.7)";
2679
- }
2680
- return style;
2681
- };
2682
- const getPlainText = (htmlString) => {
2683
- if (!htmlString) return "";
2684
- const tempDiv = document.createElement("div");
2685
- tempDiv.innerHTML = htmlString;
2686
- return tempDiv.textContent || tempDiv.innerText || "";
2687
- };
2688
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
2689
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
2690
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: prompt }) }),
2691
- points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
2692
- "Points: ",
2693
- points
2694
- ] })
2695
- ] }),
2696
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0 space-y-4", children: [
2697
- /* @__PURE__ */ jsxs("div", { className: "relative w-full border border-muted rounded-md overflow-hidden", style: { maxWidth: "100%", maxHeight: "500px" }, children: [
2698
- /* @__PURE__ */ jsx(
2699
- "img",
2700
- {
2701
- src: imageUrl,
2702
- alt: imageAltText || "Hotspot image",
2703
- className: "block max-w-full max-h-full object-contain",
2704
- "data-ai-hint": question.imageAltText ? question.imageAltText.split(" ").slice(0, 2).join(" ") : "diagram illustration"
2705
- }
2706
- ),
2707
- hotspots.map((hotspot) => /* @__PURE__ */ jsx(
2708
- "div",
2709
- {
2710
- title: getPlainText(hotspot.description) || `Hotspot ${hotspot.id}`,
2711
- style: getHotspotStyle(hotspot),
2712
- onClick: () => handleHotspotClick(hotspot.id),
2713
- "aria-pressed": selectedIds.includes(hotspot.id),
2714
- role: "button",
2715
- tabIndex: showCorrectAnswer ? -1 : 0,
2716
- onKeyDown: (e) => {
2717
- if (e.key === "Enter" || e.key === " ") handleHotspotClick(hotspot.id);
2718
- }
2719
- },
2720
- hotspot.id
2721
- ))
2722
- ] }),
2723
- showCorrectAnswer && explanation && /* @__PURE__ */ jsxs("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md", children: [
2724
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Explanation:" }),
2725
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: explanation, className: "text-sm text-accent-foreground/80" })
2726
- ] }),
2727
- showCorrectAnswer && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
2728
- /* @__PURE__ */ jsx("p", { className: "font-semibold text-md", children: "Correct Hotspots:" }),
2729
- /* @__PURE__ */ jsx("ul", { className: "list-disc list-inside space-y-1 text-sm", children: (correctAnswerIds || []).map((id) => {
2730
- const hotspot = hotspots.find((h) => h.id === id);
2731
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: hotspot?.description || hotspot?.id || "N/A", className: "inline" }) }, id);
2732
- }) })
2733
- ] })
2734
- ] })
2735
- ] });
2736
- };
2737
- var loadScript = (src, async = true) => {
2738
- return new Promise((resolve, reject) => {
2739
- const existingScript = document.querySelector(`script[src="${src}"]`);
2740
- if (existingScript) {
2741
- const readyState = existingScript.readyState;
2742
- if (readyState && readyState !== "loaded" && readyState !== "complete") {
2743
- existingScript.addEventListener("load", () => resolve());
2744
- existingScript.addEventListener("error", () => reject(new Error(`Error event for existing script: ${src}`)));
2745
- } else if (window.Blockly && src.includes("blockly.min.js") && window.Blockly.Blocks && window.Blockly.JavaScript) {
2746
- resolve();
2747
- } else if (window.Blockly && src.includes("blockly.min.js") && !window.Blockly.Blocks && src.includes("blocks.min.js")) {
2748
- existingScript.addEventListener("load", () => resolve());
2749
- existingScript.addEventListener("error", () => reject(new Error(`Error event for existing script (blocks): ${src}`)));
2750
- } else if (window.Blockly && src.includes("blockly.min.js") && !window.Blockly.JavaScript && src.includes("javascript.min.js")) {
2751
- existingScript.addEventListener("load", () => resolve());
2752
- existingScript.addEventListener("error", () => reject(new Error(`Error event for existing script (JS gen): ${src}`)));
2753
- } else {
2754
- resolve();
2755
- }
2756
- return;
2757
- }
2758
- const script = document.createElement("script");
2759
- script.src = src;
2760
- script.async = async;
2761
- script.onload = () => resolve();
2762
- script.onerror = () => reject(new Error(`Failed to load new script: ${src}`));
2763
- document.head.appendChild(script);
2764
- });
2765
- };
2766
- var loadBlocklyScript = () => {
2767
- return new Promise((resolve, reject) => {
2768
- if (typeof window.Blockly?.Blocks !== "undefined" && typeof window.Blockly?.JavaScript !== "undefined") {
2769
- resolve();
2770
- return;
2771
- }
2772
- const cdnOptions = [
2773
- {
2774
- name: "cdnjs",
2775
- mainSrc: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/blockly.min.js",
2776
- blocksSrc: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/blocks.min.js",
2777
- generatorSrc: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/javascript.min.js",
2778
- mediaPath: "https://cdnjs.cloudflare.com/ajax/libs/blockly/9.0.0/media/"
2779
- },
2780
- {
2781
- name: "unpkg",
2782
- mainSrc: "https://unpkg.com/blockly@9.0.0/blockly.min.js",
2783
- blocksSrc: "https://unpkg.com/blockly@9.0.0/blocks.min.js",
2784
- generatorSrc: "https://unpkg.com/blockly@9.0.0/javascript.min.js",
2785
- mediaPath: "https://unpkg.com/blockly@9.0.0/media/"
2786
- },
2787
- {
2788
- name: "jsdelivr",
2789
- mainSrc: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/blockly.min.js",
2790
- blocksSrc: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/blocks.min.js",
2791
- generatorSrc: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/javascript.min.js",
2792
- mediaPath: "https://cdn.jsdelivr.net/npm/blockly@9.0.0/media/"
2793
- }
2794
- ];
2795
- const tryLoadFromCDN = async (cdnIndex) => {
2796
- if (cdnIndex >= cdnOptions.length) {
2797
- throw new Error("All Blockly CDN loading options failed");
2798
- }
2799
- const cdn = cdnOptions[cdnIndex];
2800
- try {
2801
- await loadScript(cdn.mainSrc);
2802
- const BlocklyGlobal = window.Blockly;
2803
- if (typeof BlocklyGlobal === "undefined") throw new Error(`Blockly global not found from ${cdn.name}.`);
2804
- if (BlocklyGlobal.utils?.global?.setPaths) {
2805
- BlocklyGlobal.utils.global.setPaths(cdn.mediaPath);
2806
- } else if (BlocklyGlobal.utils?.global) {
2807
- BlocklyGlobal.utils.global.blocklyPath = cdn.mediaPath;
2808
- BlocklyGlobal.MEDIA = cdn.mediaPath;
2809
- } else {
2810
- BlocklyGlobal.MEDIA = cdn.mediaPath;
2811
- }
2812
- await Promise.all([loadScript(cdn.blocksSrc), loadScript(cdn.generatorSrc)]);
2813
- if (typeof BlocklyGlobal.Blocks === "undefined") throw new Error(`Blockly.Blocks not found from ${cdn.name}.`);
2814
- if (typeof BlocklyGlobal.JavaScript === "undefined") throw new Error(`Blockly.JavaScript not found from ${cdn.name}.`);
2815
- resolve();
2816
- } catch (error) {
2817
- await tryLoadFromCDN(cdnIndex + 1);
2818
- }
2819
- };
2820
- tryLoadFromCDN(0).catch(reject);
2821
- });
2822
- };
2823
- var useBlocklyLoader = () => {
2824
- const [isLoading, setIsLoading] = useState(true);
2825
- const [loadError, setLoadError] = useState(null);
2826
- const [isReady, setIsReady] = useState(false);
2827
- const attemptLoad = useCallback(() => {
2828
- setLoadError(null);
2829
- setIsReady(false);
2830
- loadBlocklyScript().then(() => {
2831
- setIsReady(true);
2832
- setIsLoading(false);
2833
- setLoadError(null);
2834
- }).catch((error) => {
2835
- setLoadError(error.message || "Unknown error loading Blockly.");
2836
- setIsLoading(false);
2837
- setIsReady(false);
2838
- });
2839
- }, []);
2840
- useEffect(() => {
2841
- if (isLoading && !isReady && !loadError) attemptLoad();
2842
- }, [isLoading, isReady, loadError, attemptLoad]);
2843
- const retry = useCallback(() => {
2844
- setLoadError(null);
2845
- setIsReady(false);
2846
- setIsLoading(true);
2847
- }, []);
2848
- return { isLoading, loadError, isReady, retry };
2849
- };
2850
- var BlocklyProgrammingQuestionUI = React9__default.forwardRef(({
2851
- question,
2852
- userAnswer,
2853
- showCorrectAnswer = false
2854
- }, ref) => {
2855
- const blocklyDivRef = useRef(null);
2856
- const workspaceRef = useRef(null);
2857
- const [isInitializingComponent, setIsInitializingComponent] = useState(false);
2858
- const [componentError, setComponentError] = useState(null);
2859
- const { isLoading: blocklyLoading, loadError: blocklyLoadError, isReady: blocklyReady, retry } = useBlocklyLoader();
2860
- useImperativeHandle(ref, () => ({
2861
- getWorkspaceXml: () => {
2862
- if (workspaceRef.current && blocklyReady) {
2863
- const LocalBlockly = window.Blockly;
2864
- if (!LocalBlockly?.Xml?.workspaceToDom || !LocalBlockly?.Xml?.domToText) {
2865
- console.warn("Blockly.Xml methods not available for XML serialization in getWorkspaceXml.");
2866
- return null;
2867
- }
2868
- try {
2869
- const xml = LocalBlockly.Xml.workspaceToDom(workspaceRef.current);
2870
- return LocalBlockly.Xml.domToText(xml);
2871
- } catch (e) {
2872
- console.error("Error serializing Blockly workspace to XML in getWorkspaceXml:", e);
2873
- return null;
2874
- }
2875
- }
2876
- return null;
2877
- }
2878
- }));
2879
- const initializeBlocklyWorkspace = useCallback(() => {
2880
- if (!blocklyReady || !blocklyDivRef.current) return;
2881
- const LocalBlockly = window.Blockly;
2882
- if (!LocalBlockly?.inject || !LocalBlockly?.Xml || !LocalBlockly?.Events || !LocalBlockly?.Themes) {
2883
- setComponentError("Blockly library not fully loaded.");
2884
- setIsInitializingComponent(false);
2885
- return;
2886
- }
2887
- setComponentError(null);
2888
- let newXmlToLoad = null;
2889
- if (showCorrectAnswer && question.solutionWorkspaceXML) {
2890
- newXmlToLoad = question.solutionWorkspaceXML;
2891
- } else if (typeof userAnswer === "string" && userAnswer.trim().startsWith("<xml")) {
2892
- newXmlToLoad = userAnswer;
2893
- } else if (question.initialWorkspace) {
2894
- newXmlToLoad = question.initialWorkspace;
2895
- }
2896
- if (workspaceRef.current) {
2897
- let currentWorkspaceXML = "";
2898
- try {
2899
- currentWorkspaceXML = LocalBlockly.Xml.domToText(LocalBlockly.Xml.workspaceToDom(workspaceRef.current));
2900
- } catch (e) {
2901
- }
2902
- const readOnlyStateMatches = workspaceRef.current.options.readOnly === showCorrectAnswer;
2903
- if (currentWorkspaceXML === newXmlToLoad && readOnlyStateMatches) {
2904
- setIsInitializingComponent(false);
2905
- return;
2906
- }
2907
- if (currentWorkspaceXML === newXmlToLoad && !readOnlyStateMatches) {
2908
- workspaceRef.current.updateOptions({ readOnly: showCorrectAnswer, trashcan: !showCorrectAnswer });
2909
- setIsInitializingComponent(false);
2910
- return;
2911
- }
2912
- }
2913
- setIsInitializingComponent(true);
2914
- if (workspaceRef.current?.dispose) {
2915
- try {
2916
- workspaceRef.current.dispose();
2917
- } catch (e) {
2918
- console.error("Error disposing previous workspace:", e);
2919
- }
2920
- workspaceRef.current = null;
2921
- }
2922
- try {
2923
- const toolbox = question.toolboxDefinition || `<xml><category name="Logic" colour="210"><block type="controls_if"></block></category></xml>`;
2924
- const workspace = LocalBlockly.inject(blocklyDivRef.current, {
2925
- toolbox,
2926
- scrollbars: true,
2927
- trashcan: !showCorrectAnswer,
2928
- readOnly: showCorrectAnswer,
2929
- zoom: {
2930
- controls: true,
2931
- wheel: true,
2932
- startScale: 0.9,
2933
- maxScale: 3,
2934
- minScale: 0.3,
2935
- scaleSpeed: 1.2
2936
- },
2937
- grid: {
2938
- spacing: 20,
2939
- length: 3,
2940
- colour: "#374151",
2941
- snap: true
2942
- },
2943
- theme: LocalBlockly.Themes.Classic || void 0,
2944
- move: {
2945
- scrollbars: {
2946
- horizontal: true,
2947
- vertical: true
2948
- },
2949
- drag: true,
2950
- wheel: true
2951
- },
2952
- renderer: "geras"
2953
- });
2954
- workspaceRef.current = workspace;
2955
- if (newXmlToLoad) {
2956
- try {
2957
- const dom = LocalBlockly.Xml.textToDom(newXmlToLoad);
2958
- LocalBlockly.Xml.domToWorkspace(dom, workspace);
2959
- } catch (e) {
2960
- console.error("Error loading XML to workspace:", e, "XML:", newXmlToLoad);
2961
- setComponentError("Error loading blocks.");
2962
- }
2963
- }
2964
- if (workspace.scrollCenter) workspace.scrollCenter();
2965
- if (LocalBlockly.svgResize) LocalBlockly.svgResize(workspace);
2966
- setTimeout(() => {
2967
- if (workspace && LocalBlockly.svgResize) {
2968
- LocalBlockly.svgResize(workspace);
2969
- }
2970
- }, 100);
2971
- } catch (e) {
2972
- console.error("Error initializing Blockly workspace:", e);
2973
- setComponentError(`Init failed: ${e instanceof Error ? e.message : String(e)}`);
2974
- } finally {
2975
- setIsInitializingComponent(false);
2976
- }
2977
- }, [
2978
- blocklyReady,
2979
- question.id,
2980
- question.toolboxDefinition,
2981
- question.initialWorkspace,
2982
- question.solutionWorkspaceXML,
2983
- showCorrectAnswer,
2984
- userAnswer
2985
- ]);
2986
- useEffect(() => {
2987
- if (blocklyReady && blocklyDivRef.current) {
2988
- initializeBlocklyWorkspace();
2989
- }
2990
- return () => {
2991
- if (workspaceRef.current?.dispose) {
2992
- try {
2993
- workspaceRef.current.dispose();
2994
- } catch (disposeError) {
2995
- console.error("Error during Blockly workspace disposal on unmount:", disposeError);
2996
- }
2997
- workspaceRef.current = null;
2998
- }
2999
- };
3000
- }, [blocklyReady, question.id, initializeBlocklyWorkspace]);
3001
- useEffect(() => {
3002
- let resizeTimeout;
3003
- const handleResize = () => {
3004
- clearTimeout(resizeTimeout);
3005
- resizeTimeout = setTimeout(() => {
3006
- if (workspaceRef.current && blocklyReady) {
3007
- const LocalBlockly = window.Blockly;
3008
- if (LocalBlockly?.svgResize) {
3009
- LocalBlockly.svgResize(workspaceRef.current);
3010
- }
3011
- }
3012
- }, 150);
3013
- };
3014
- window.addEventListener("resize", handleResize);
3015
- return () => {
3016
- window.removeEventListener("resize", handleResize);
3017
- clearTimeout(resizeTimeout);
3018
- };
3019
- }, [blocklyReady]);
3020
- const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
3021
- const workspaceContainerId = `blockly-workspace-container-${question.id}`;
3022
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
3023
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
3024
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: question.prompt }) }),
3025
- question.points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
3026
- "Points: ",
3027
- question.points
3028
- ] })
3029
- ] }),
3030
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
3031
- blocklyLoading && /* @__PURE__ */ jsx(
3032
- "div",
3033
- {
3034
- style: {
3035
- height: workspaceHeight,
3036
- width: "100%",
3037
- borderRadius: "0.375rem",
3038
- border: "1px solid hsl(var(--border))",
3039
- backgroundColor: "hsl(var(--background))"
3040
- },
3041
- className: "flex items-center justify-center",
3042
- children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
3043
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground animate-pulse mb-2", children: "Loading Blockly Environment..." }),
3044
- /* @__PURE__ */ jsx("div", { className: "w-8 h-8 border-4 border-muted border-t-primary rounded-full animate-spin mx-auto" })
3045
- ] })
3046
- }
3047
- ),
3048
- blocklyLoadError && !blocklyLoading && /* @__PURE__ */ jsx(
3049
- "div",
3050
- {
3051
- style: {
3052
- height: workspaceHeight,
3053
- width: "100%",
3054
- borderRadius: "0.375rem",
3055
- border: "1px solid hsl(var(--destructive))",
3056
- backgroundColor: "hsl(var(--card))"
3057
- },
3058
- className: "flex items-center justify-center p-4",
3059
- children: /* @__PURE__ */ jsxs("div", { className: "text-destructive text-center", children: [
3060
- /* @__PURE__ */ jsx("p", { className: "font-semibold text-lg", children: "Failed to load Blockly" }),
3061
- /* @__PURE__ */ jsx("p", { className: "text-sm mt-2 mb-3", children: blocklyLoadError }),
3062
- /* @__PURE__ */ jsxs("div", { className: "space-x-2", children: [
3063
- /* @__PURE__ */ jsx(
3064
- "button",
3065
- {
3066
- onClick: retry,
3067
- className: "px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors text-sm",
3068
- children: "Try Again"
3069
- }
3070
- ),
3071
- /* @__PURE__ */ jsx(
3072
- "button",
3073
- {
3074
- onClick: () => window.location.reload(),
3075
- className: "px-4 py-2 bg-destructive text-destructive-foreground rounded hover:bg-destructive/90 transition-colors text-sm",
3076
- children: "Refresh Page"
3077
- }
3078
- )
3079
- ] })
3080
- ] })
3081
- }
3082
- ),
3083
- !blocklyLoading && !blocklyLoadError && /* @__PURE__ */ jsx(
3084
- "div",
3085
- {
3086
- id: workspaceContainerId,
3087
- ref: blocklyDivRef,
3088
- style: {
3089
- height: workspaceHeight,
3090
- width: "100%",
3091
- borderRadius: "0.375rem",
3092
- border: `1px solid ${componentError ? "hsl(var(--destructive))" : "hsl(var(--border))"}`,
3093
- backgroundColor: "hsl(var(--card))",
3094
- position: "relative",
3095
- userSelect: "none",
3096
- overflow: "hidden"
3097
- },
3098
- "aria-label": `Blockly programming workspace for question: ${question.prompt}`
3099
- }
3100
- ),
3101
- showCorrectAnswer && question.explanation && /* @__PURE__ */ jsxs("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md", children: [
3102
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Explanation:" }),
3103
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: question.explanation, className: "text-sm text-accent-foreground/80" })
3104
- ] })
3105
- ] })
3106
- ] });
3107
- });
3108
- BlocklyProgrammingQuestionUI.displayName = "BlocklyProgrammingQuestionUI";
3109
- var SCRATCH_JS_ENGINE_LAYOUT_PATH = "/static/scratch-blocks/js/blockly_compressed_vertical.js";
3110
- var SCRATCH_JS_BLOCK_DEFINITIONS_PATH = "/static/scratch-blocks/js/blocks_compressed_vertical.js";
3111
- var SCRATCH_JS_MSG_EN_PATH = "/static/scratch-blocks/msg/js/en.js";
3112
- var SCRATCH_MEDIA_PATH = "/static/scratch-blocks/media/";
3113
- var loadedScriptPromises = /* @__PURE__ */ new Map();
3114
- var loadScript2 = (src) => {
3115
- const fullSrc = src.startsWith("/") ? src : `/${src}`;
3116
- if (loadedScriptPromises.has(fullSrc)) {
3117
- const promise2 = loadedScriptPromises.get(fullSrc);
3118
- return promise2;
3119
- }
3120
- const promise = new Promise((resolve, reject) => {
3121
- const existingScript = document.querySelector(`script[src="${fullSrc}"]`);
3122
- if (existingScript) {
3123
- const status = existingScript.getAttribute("data-load-status");
3124
- if (status === "loaded") {
3125
- resolve();
3126
- return;
3127
- } else if (status === "loading") {
3128
- const onLoad2 = () => {
3129
- existingScript.setAttribute("data-load-status", "loaded");
3130
- existingScript.removeEventListener("load", onLoad2);
3131
- existingScript.removeEventListener("error", onError2);
3132
- resolve();
3133
- };
3134
- const onError2 = (ev) => {
3135
- existingScript.setAttribute("data-load-status", "error");
3136
- existingScript.removeEventListener("load", onLoad2);
3137
- existingScript.removeEventListener("error", onError2);
3138
- loadedScriptPromises.delete(fullSrc);
3139
- const errorMsg = `Error event for existing script tag ${src}. Full URL: ${existingScript.src}. Event: ${ev.type}.`;
3140
- console.error("ScratchUI: loadScript error (existing) -", errorMsg);
3141
- reject(new Error(errorMsg));
3142
- };
3143
- existingScript.addEventListener("load", onLoad2);
3144
- existingScript.addEventListener("error", onError2);
3145
- return;
3146
- } else if (status === "error") {
3147
- existingScript.remove();
3148
- }
3149
- }
3150
- const script = document.createElement("script");
3151
- script.src = fullSrc;
3152
- script.async = false;
3153
- script.setAttribute("data-load-status", "loading");
3154
- const onLoad = () => {
3155
- script.setAttribute("data-load-status", "loaded");
3156
- script.removeEventListener("load", onLoad);
3157
- script.removeEventListener("error", onError);
3158
- resolve();
3159
- };
3160
- const onError = (ev) => {
3161
- 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.`;
3162
- console.error("ScratchUI: loadScript error (new) -", errorMsg, "Full URL attempted:", script.src);
3163
- script.setAttribute("data-load-status", "error");
3164
- script.removeEventListener("load", onLoad);
3165
- script.removeEventListener("error", onError);
3166
- loadedScriptPromises.delete(fullSrc);
3167
- reject(new Error(errorMsg));
3168
- };
3169
- script.addEventListener("load", onLoad);
3170
- script.addEventListener("error", onError);
3171
- document.body.appendChild(script);
3172
- });
3173
- loadedScriptPromises.set(fullSrc, promise);
3174
- return promise;
3175
- };
3176
- var ScratchProgrammingQuestionUI = forwardRef(({
3177
- question,
3178
- userAnswer,
3179
- showCorrectAnswer = false
3180
- }, ref) => {
3181
- const blocklyDivRef = useRef(null);
3182
- const workspaceRef = useRef(null);
3183
- const [isBlocklyReady, setIsBlocklyReady] = useState(false);
3184
- const [componentError, setComponentError] = useState(null);
3185
- const [isLoadingScripts, setIsLoadingScripts] = useState(true);
3186
- const attemptLoadScripts = useCallback(async () => {
3187
- setIsLoadingScripts(true);
3188
- setComponentError(null);
3189
- console.log("ScratchUI: Starting script loading sequence...");
3190
- try {
3191
- console.log("ScratchUI: Attempting to load Scratch Engine/Layout:", SCRATCH_JS_ENGINE_LAYOUT_PATH);
3192
- await loadScript2(SCRATCH_JS_ENGINE_LAYOUT_PATH);
3193
- const BlocklyGlobalEngine = window.Blockly;
3194
- if (typeof BlocklyGlobalEngine === "undefined") {
3195
- throw new Error(`Blockly global object (window.Blockly) not found after loading engine script: ${SCRATCH_JS_ENGINE_LAYOUT_PATH}.`);
3196
- }
3197
- console.log(`ScratchUI: After engine/layout load: Blockly defined: ${!!BlocklyGlobalEngine}, Blockly.Blocks defined: ${!!BlocklyGlobalEngine?.Blocks}, Blockly.Msg defined: ${!!BlocklyGlobalEngine?.Msg}, Blockly.ScratchMsgs defined: ${!!BlocklyGlobalEngine?.ScratchMsgs}`);
3198
- console.log("ScratchUI: Attempting to load Messages:", SCRATCH_JS_MSG_EN_PATH);
3199
- await loadScript2(SCRATCH_JS_MSG_EN_PATH);
3200
- let BlocklyGlobalAfterMsg = window.Blockly;
3201
- if (!BlocklyGlobalAfterMsg) throw new Error("Blockly global disappeared after loading message script.");
3202
- console.log(`ScratchUI: After en.js load: Blockly defined: ${!!BlocklyGlobalAfterMsg}, Blockly.Msg defined: ${!!BlocklyGlobalAfterMsg?.Msg}, Keys in Blockly.Msg: ${BlocklyGlobalAfterMsg?.Msg ? Object.keys(BlocklyGlobalAfterMsg.Msg).length : "N/A"}. Sample Blockly.Msg.LOGIC_HUE: ${BlocklyGlobalAfterMsg?.Msg?.LOGIC_HUE}`);
3203
- if (!BlocklyGlobalAfterMsg.Msg || Object.keys(BlocklyGlobalAfterMsg.Msg).length === 0) {
3204
- console.warn("ScratchUI: Blockly.Msg appears unpopulated or empty. Checking if Blockly.ScratchMsgs.addLocaleData can be used...");
3205
- if (BlocklyGlobalAfterMsg.ScratchMsgs && typeof BlocklyGlobalAfterMsg.ScratchMsgs.addLocaleData === "function") {
3206
- console.log("ScratchUI: Blockly.ScratchMsgs.addLocaleData is available. Attempting to call Blockly.ScratchMsgs.addLocaleData('en').");
3207
- BlocklyGlobalAfterMsg.ScratchMsgs.addLocaleData("en");
3208
- BlocklyGlobalAfterMsg = window.Blockly;
3209
- console.log(`ScratchUI: After attempting addLocaleData('en'): Keys in Blockly.Msg: ${BlocklyGlobalAfterMsg?.Msg ? Object.keys(BlocklyGlobalAfterMsg.Msg).length : "N/A"}. Sample Blockly.Msg.LOGIC_HUE: ${BlocklyGlobalAfterMsg?.Msg?.LOGIC_HUE}`);
3210
- if (!BlocklyGlobalAfterMsg.Msg || Object.keys(BlocklyGlobalAfterMsg.Msg).length === 0) {
3211
- throw new Error("Blockly.Msg still empty after calling addLocaleData. Message loading failed critically.");
3212
- }
3213
- } else {
3214
- throw new Error("Blockly.Msg is empty, and Blockly.ScratchMsgs.addLocaleData is not available. Message loading failed.");
3215
- }
3216
- }
3217
- console.log("ScratchUI: Attempting to load Scratch Block Definitions:", SCRATCH_JS_BLOCK_DEFINITIONS_PATH);
3218
- await loadScript2(SCRATCH_JS_BLOCK_DEFINITIONS_PATH);
3219
- const BlocklyGlobalBlocks = window.Blockly;
3220
- if (!BlocklyGlobalBlocks || !BlocklyGlobalBlocks.Blocks) {
3221
- throw new Error(`Blockly.Blocks not defined after loading ${SCRATCH_JS_BLOCK_DEFINITIONS_PATH}.`);
3222
- }
3223
- 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: ${!!BlocklyGlobalBlocks.Blocks?.motion_movesteps}`);
3224
- if (typeof BlocklyGlobalBlocks.Blocks?.motion_movesteps === "undefined") {
3225
- throw new Error(`Essential Scratch blocks (e.g., motion_movesteps) not found after loading block definitions. Available blocks: ${Object.keys(BlocklyGlobalBlocks.Blocks || {}).join(", ")}`);
3226
- }
3227
- setIsBlocklyReady(true);
3228
- console.log("ScratchUI: All Scratch scripts loaded and essential checks passed. Blockly is ready for injection.");
3229
- } catch (error) {
3230
- console.error("ScratchUI: Error during Scratch/Blockly script loading sequence:", error);
3231
- setComponentError(error.message || "Failed to load critical Scratch/Blockly scripts.");
3232
- setIsBlocklyReady(false);
3233
- } finally {
3234
- setIsLoadingScripts(false);
3235
- }
3236
- }, []);
3237
- useEffect(() => {
3238
- attemptLoadScripts();
3239
- }, [attemptLoadScripts]);
3240
- useImperativeHandle(ref, () => ({
3241
- getWorkspaceXml: () => {
3242
- const LocalBlockly = window.Blockly;
3243
- if (workspaceRef.current && LocalBlockly?.Xml) {
3244
- try {
3245
- const xml = LocalBlockly.Xml.workspaceToDom(workspaceRef.current);
3246
- return LocalBlockly.Xml.domToText(xml);
3247
- } catch (e) {
3248
- console.error("ScratchUI: Error serializing Scratch workspace to XML:", e);
3249
- return null;
3250
- }
3251
- }
3252
- return null;
3253
- }
3254
- }));
3255
- const initializeWorkspace = useCallback(() => {
3256
- const LocalBlockly = window.Blockly;
3257
- if (!isBlocklyReady || !blocklyDivRef.current || !LocalBlockly) {
3258
- console.warn("ScratchUI: Conditions not met for workspace initialization. isBlocklyReady:", isBlocklyReady, "blocklyDivRef.current:", !!blocklyDivRef.current, "LocalBlockly:", !!LocalBlockly);
3259
- return;
3260
- }
3261
- if (!LocalBlockly.inject || !LocalBlockly.Xml || !LocalBlockly.Blocks || !LocalBlockly.Msg) {
3262
- setComponentError("ScratchUI: Essential Blockly library parts (inject, Xml, Blocks, Msg) are not available for injection.");
3263
- return;
3264
- }
3265
- if (Object.keys(LocalBlockly.Msg).length === 0 || !LocalBlockly.Msg.CATEGORY_MOTION) {
3266
- setComponentError("ScratchUI: Blockly.Msg is empty or essential messages (like CATEGORY_MOTION) are missing. Messages did not load correctly. Injection might fail.");
3267
- 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);
3268
- return;
3269
- }
3270
- if (typeof LocalBlockly.Blocks?.motion_movesteps === "undefined" || typeof LocalBlockly.Blocks?.event_whenflagclicked === "undefined") {
3271
- const availableBlocks = Object.keys(LocalBlockly.Blocks || {}).join(", ");
3272
- setComponentError(`ScratchUI: Essential Scratch block definitions (e.g., motion_movesteps, event_whenflagclicked) are missing before injection. Available blocks: ${availableBlocks}`);
3273
- return;
3274
- }
3275
- setComponentError(null);
3276
- if (workspaceRef.current && typeof workspaceRef.current.dispose === "function") {
3277
- try {
3278
- workspaceRef.current.dispose();
3279
- } catch (e) {
3280
- console.warn("ScratchUI: Minor error disposing previous workspace instance:", e);
3281
- }
3282
- workspaceRef.current = null;
3283
- }
3284
- try {
3285
- console.log("ScratchUI: Attempting to inject Blockly workspace...");
3286
- const simplifiedToolbox = `
3287
- <xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-simple-debug" style="display: none">
3288
- <category name="Events" colour="#FFD500" secondaryColour="#CCAA00">
3289
-
3290
- <block type="event_whenflagclicked"></block>
3291
- </category>
3292
- <category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
3293
-
3294
- <block type="motion_movesteps">
3295
- <value name="STEPS"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
3296
- </block>
3297
- <block type="motion_turnright">
3298
- <value name="DEGREES"><shadow type="math_number"><field name="NUM">15</field></shadow></value>
3299
- </block>
3300
- </category>
3301
-
3302
- <category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
3303
- <block type="looks_sayforsecs">
3304
- <value name="MESSAGE"><shadow type="text"><field name="TEXT">Hello!</field></shadow></value>
3305
- <value name="SECS"><shadow type="math_number"><field name="NUM">2</field></shadow></value>
3306
- </block>
3307
- </category>
3308
- </xml>`;
3309
- const actualToolbox = question.toolboxDefinition || simplifiedToolbox;
3310
- console.log("ScratchUI: Using Toolbox Definition:", actualToolbox);
3311
- console.log("ScratchUI: Media path:", SCRATCH_MEDIA_PATH);
3312
- console.log("ScratchUI: Blockly.Msg.CATEGORY_MOTION at inject time:", LocalBlockly.Msg.CATEGORY_MOTION);
3313
- console.log("ScratchUI: Blockly.Colours available:", !!LocalBlockly.Colours);
3314
- const workspace = LocalBlockly.inject(blocklyDivRef.current, {
3315
- toolbox: actualToolbox,
3316
- // Use the simplified one for debugging first
3317
- media: SCRATCH_MEDIA_PATH,
3318
- scrollbars: true,
3319
- trashcan: !showCorrectAnswer,
3320
- readOnly: showCorrectAnswer,
3321
- renderer: "zelos",
3322
- zoom: { controls: true, wheel: true, startScale: 0.75, maxScale: 3, minScale: 0.3, scaleSpeed: 1.2 },
3323
- colours: LocalBlockly.Colours
3324
- });
3325
- workspaceRef.current = workspace;
3326
- console.log("ScratchUI: Workspace injected successfully.");
3327
- let xmlToLoad = null;
3328
- if (showCorrectAnswer && question.solutionWorkspaceXML) {
3329
- xmlToLoad = question.solutionWorkspaceXML;
3330
- } else if (typeof userAnswer === "string" && userAnswer.trim().startsWith("<xml")) {
3331
- xmlToLoad = userAnswer;
3332
- } else if (question.initialWorkspace) {
3333
- xmlToLoad = question.initialWorkspace;
3334
- }
3335
- if (xmlToLoad) {
3336
- try {
3337
- console.log("ScratchUI: Attempting to load XML to workspace:", xmlToLoad.substring(0, 100) + "...");
3338
- const dom = LocalBlockly.Xml.textToDom(xmlToLoad);
3339
- LocalBlockly.Xml.domToWorkspace(dom, workspace);
3340
- console.log("ScratchUI: XML loaded to workspace.");
3341
- } catch (xmlError) {
3342
- console.error("ScratchUI: Error loading XML to workspace:", xmlError);
3343
- setComponentError(`ScratchUI: Error loading blocks from XML: ${xmlError.message || String(xmlError)}`);
3344
- }
3345
- }
3346
- if (workspace && LocalBlockly.svgResize) {
3347
- LocalBlockly.svgResize(workspace);
3348
- setTimeout(() => {
3349
- if (workspaceRef.current && LocalBlockly.svgResize) LocalBlockly.svgResize(workspaceRef.current);
3350
- }, 100);
3351
- }
3352
- } catch (e) {
3353
- console.error("ScratchUI: Error during Blockly.inject or subsequent workspace setup:", e);
3354
- console.error("ScratchUI: Error Details - Name:", e.name, "Message:", e.message, "Stack:", e.stack);
3355
- setComponentError(`ScratchUI: Workspace initialization failed: ${e.message || String(e)}. Check console for details. Toolbox used: ${question.toolboxDefinition ? "Custom" : "Default Simplified"}.`);
3356
- }
3357
- }, [question, showCorrectAnswer, userAnswer, isBlocklyReady]);
3358
- useEffect(() => {
3359
- if (isBlocklyReady) {
3360
- initializeWorkspace();
3361
- }
3362
- const handleResize = () => {
3363
- const LocalBlocklyResize = window.Blockly;
3364
- if (workspaceRef.current && LocalBlocklyResize?.svgResize) {
3365
- LocalBlocklyResize.svgResize(workspaceRef.current);
3366
- }
3367
- };
3368
- window.addEventListener("resize", handleResize);
3369
- return () => {
3370
- window.removeEventListener("resize", handleResize);
3371
- if (workspaceRef.current && typeof workspaceRef.current.dispose === "function") {
3372
- if (window.Blockly) {
3373
- try {
3374
- workspaceRef.current.dispose();
3375
- } catch (e) {
3376
- console.warn("ScratchUI: Error disposing workspace on unmount:", e);
3377
- }
3378
- }
3379
- workspaceRef.current = null;
3380
- }
3381
- };
3382
- }, [isBlocklyReady, initializeWorkspace]);
3383
- const workspaceHeight = showCorrectAnswer ? "300px" : "450px";
3384
- if (isLoadingScripts) {
3385
- return /* @__PURE__ */ jsx("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))" }, children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
3386
- /* @__PURE__ */ jsx("div", { className: "w-8 h-8 border-4 border-muted border-t-primary rounded-full animate-spin mx-auto mb-2" }),
3387
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "Loading Scratch Assets..." })
3388
- ] }) });
3389
- }
3390
- if (componentError) {
3391
- return /* @__PURE__ */ jsxs("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))" }, children: [
3392
- /* @__PURE__ */ jsx("p", { className: "font-semibold text-lg mb-2", children: "Failed to load Scratch Workspace." }),
3393
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-3 text-center", children: componentError }),
3394
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mb-4 text-center", children: [
3395
- "Please ensure all Scratch/Blockly JavaScript files are correctly copied to your",
3396
- /* @__PURE__ */ jsx("code", { children: "public/static/scratch-blocks/js" }),
3397
- " directory. Check browser console for more details.",
3398
- /* @__PURE__ */ jsx("br", {}),
3399
- /* @__PURE__ */ jsxs("strong", { children: [
3400
- "CRITICAL: Ensure you have copied the CSS files from ",
3401
- /* @__PURE__ */ jsx("code", { children: "node_modules/scratch-blocks/css/" }),
3402
- " (e.g., ",
3403
- /* @__PURE__ */ jsx("code", { children: "vertical.css" }),
3404
- ") to ",
3405
- /* @__PURE__ */ jsx("code", { children: "public/static/scratch-blocks/css/" }),
3406
- " and linked it in your main layout. Without CSS, blocks will not render correctly."
3407
- ] })
3408
- ] }),
3409
- /* @__PURE__ */ jsx(Button, { onClick: attemptLoadScripts, variant: "outline", children: "Try Reloading Scripts" })
3410
- ] });
3411
- }
3412
- if (!isBlocklyReady && !isLoadingScripts) {
3413
- return /* @__PURE__ */ jsx("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))" }, children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "Scratch environment did not initialize (Blockly not ready). Check console for script loading errors." }) });
3414
- }
3415
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
3416
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
3417
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: question.prompt }),
3418
- question.points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
3419
- "Points: ",
3420
- question.points
3421
- ] })
3422
- ] }),
3423
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
3424
- /* @__PURE__ */ jsx(
3425
- "div",
3426
- {
3427
- ref: blocklyDivRef,
3428
- style: {
3429
- height: workspaceHeight,
3430
- width: "100%",
3431
- borderRadius: "0.375rem",
3432
- backgroundColor: "hsl(var(--card))",
3433
- position: "relative",
3434
- userSelect: "none",
3435
- overflow: "hidden",
3436
- display: isLoadingScripts || componentError || !isBlocklyReady ? "none" : "block"
3437
- },
3438
- "aria-label": `Scratch programming workspace for question: ${question.prompt}`
3439
- }
3440
- ),
3441
- showCorrectAnswer && question.explanation && /* @__PURE__ */ jsxs("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md", children: [
3442
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Explanation:" }),
3443
- /* @__PURE__ */ jsx("p", { className: "text-sm text-accent-foreground/80", children: question.explanation })
3444
- ] })
3445
- ] })
3446
- ] });
3447
- });
3448
- ScratchProgrammingQuestionUI.displayName = "ScratchProgrammingQuestionUI";
3449
- var Tabs = TabsPrimitive.Root;
3450
- var TabsList = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
3451
- TabsPrimitive.List,
3452
- {
3453
- ref,
3454
- className: cn(
3455
- "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
3456
- className
3457
- ),
3458
- ...props
3459
- }
3460
- ));
3461
- TabsList.displayName = TabsPrimitive.List.displayName;
3462
- var TabsTrigger = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
3463
- TabsPrimitive.Trigger,
3464
- {
3465
- ref,
3466
- className: cn(
3467
- "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
3468
- className
3469
- ),
3470
- ...props
3471
- }
3472
- ));
3473
- TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
3474
- var TabsContent = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
3475
- TabsPrimitive.Content,
3476
- {
3477
- ref,
3478
- className: cn(
3479
- "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
3480
- className
3481
- ),
3482
- ...props
3483
- }
3484
- ));
3485
- TabsContent.displayName = TabsPrimitive.Content.displayName;
3486
- var TOAST_LIMIT = 1;
3487
- var TOAST_REMOVE_DELAY = 1e6;
3488
- var count = 0;
3489
- function genId() {
3490
- count = (count + 1) % Number.MAX_SAFE_INTEGER;
3491
- return count.toString();
3492
- }
3493
- var toastTimeouts = /* @__PURE__ */ new Map();
3494
- var addToRemoveQueue = (toastId) => {
3495
- if (toastTimeouts.has(toastId)) {
3496
- return;
3497
- }
3498
- const timeout = setTimeout(() => {
3499
- toastTimeouts.delete(toastId);
3500
- dispatch({
3501
- type: "REMOVE_TOAST",
3502
- toastId
3503
- });
3504
- }, TOAST_REMOVE_DELAY);
3505
- toastTimeouts.set(toastId, timeout);
3506
- };
3507
- var reducer = (state, action) => {
3508
- switch (action.type) {
3509
- case "ADD_TOAST":
3510
- return {
3511
- ...state,
3512
- toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
3513
- };
3514
- case "UPDATE_TOAST":
3515
- return {
3516
- ...state,
3517
- toasts: state.toasts.map(
3518
- (t) => t.id === action.toast.id ? { ...t, ...action.toast } : t
3519
- )
3520
- };
3521
- case "DISMISS_TOAST": {
3522
- const { toastId } = action;
3523
- if (toastId) {
3524
- addToRemoveQueue(toastId);
3525
- } else {
3526
- state.toasts.forEach((toast2) => {
3527
- addToRemoveQueue(toast2.id);
3528
- });
3529
- }
3530
- return {
3531
- ...state,
3532
- toasts: state.toasts.map(
3533
- (t) => t.id === toastId || toastId === void 0 ? {
3534
- ...t,
3535
- open: false
3536
- } : t
3537
- )
3538
- };
3539
- }
3540
- case "REMOVE_TOAST":
3541
- if (action.toastId === void 0) {
3542
- return {
3543
- ...state,
3544
- toasts: []
3545
- };
3546
- }
3547
- return {
3548
- ...state,
3549
- toasts: state.toasts.filter((t) => t.id !== action.toastId)
3550
- };
3551
- }
3552
- };
3553
- var listeners = [];
3554
- var memoryState = { toasts: [] };
3555
- function dispatch(action) {
3556
- memoryState = reducer(memoryState, action);
3557
- listeners.forEach((listener) => {
3558
- listener(memoryState);
3559
- });
3560
- }
3561
- function toast({ ...props }) {
3562
- const id = genId();
3563
- const update = (props2) => dispatch({
3564
- type: "UPDATE_TOAST",
3565
- toast: { ...props2, id }
3566
- });
3567
- const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
3568
- dispatch({
3569
- type: "ADD_TOAST",
3570
- toast: {
3571
- ...props,
3572
- id,
3573
- open: true,
3574
- onOpenChange: (open) => {
3575
- if (!open) dismiss();
3576
- }
3577
- }
3578
- });
3579
- return {
3580
- id,
3581
- dismiss,
3582
- update
3583
- };
3584
- }
3585
- function useToast() {
3586
- const [state, setState] = React9.useState(memoryState);
3587
- React9.useEffect(() => {
3588
- listeners.push(setState);
3589
- return () => {
3590
- const index = listeners.indexOf(setState);
3591
- if (index > -1) {
3592
- listeners.splice(index, 1);
3593
- }
3594
- };
3595
- }, [state]);
3596
- return {
3597
- ...state,
3598
- toast,
3599
- dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId })
3600
- };
3601
- }
3602
- var languageMap = {
3603
- cpp: cpp(),
3604
- javascript: javascript({ typescript: true }),
3605
- // Use TS extension for better JS support
3606
- python: python(),
3607
- swift: javascript({ typescript: true }),
3608
- // Fallback for Swift syntax highlighting
3609
- csharp: cpp()
3610
- // Fallback for C# syntax highlighting
3611
- };
3612
- var CodingQuestionUI = ({
3613
- question,
3614
- onAnswerChange,
3615
- userAnswer,
3616
- showCorrectAnswer = false
3617
- }) => {
3618
- const [code, setCode] = useState("");
3619
- const [isRunningTests, setIsRunningTests] = useState(false);
3620
- const [testResults, setTestResults] = useState([]);
3621
- const { toast: toast2 } = useToast();
3622
- useEffect(() => {
3623
- const initialCode = typeof userAnswer === "string" ? userAnswer : question.functionSignature || "";
3624
- setCode(initialCode);
3625
- }, [question.id, userAnswer, question.functionSignature]);
3626
- const handleCodeChange = useCallback((value) => {
3627
- setCode(value);
3628
- onAnswerChange(value);
3629
- }, [onAnswerChange]);
3630
- const handleRunPublicTests = async () => {
3631
- setIsRunningTests(true);
3632
- setTestResults([]);
3633
- try {
3634
- const evaluationService = new CodeEvaluationService();
3635
- const results = await evaluationService.evaluatePublicTestCases(question, code);
3636
- setTestResults(results);
3637
- } catch (error) {
3638
- toast2({
3639
- title: "Evaluation Error",
3640
- description: error instanceof Error ? error.message : "An unknown error occurred.",
3641
- variant: "destructive"
3642
- });
3643
- } finally {
3644
- setIsRunningTests(false);
3645
- }
3646
- };
3647
- const langExtension = languageMap[question.codingLanguage];
3648
- return /* @__PURE__ */ jsxs(Card, { className: "w-full border-none shadow-none", children: [
3649
- /* @__PURE__ */ jsxs(CardHeader, { className: "p-0 pb-4", children: [
3650
- /* @__PURE__ */ jsx(CardTitle, { className: "text-xl mb-1 font-body", children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: question.prompt }) }),
3651
- question.points && /* @__PURE__ */ jsxs(CardDescription, { className: "text-sm text-muted-foreground", children: [
3652
- "Points: ",
3653
- question.points
3654
- ] })
3655
- ] }),
3656
- /* @__PURE__ */ jsxs(CardContent, { className: "p-0 space-y-4", children: [
3657
- /* @__PURE__ */ jsx("div", { className: "font-mono text-sm border rounded-md overflow-hidden", children: /* @__PURE__ */ jsx(
3658
- CodeMirror,
3659
- {
3660
- value: code,
3661
- height: "300px",
3662
- extensions: [langExtension],
3663
- onChange: handleCodeChange,
3664
- readOnly: showCorrectAnswer,
3665
- theme: "dark"
3666
- }
3667
- ) }),
3668
- !showCorrectAnswer && /* @__PURE__ */ jsxs(Button, { onClick: handleRunPublicTests, disabled: isRunningTests, children: [
3669
- isRunningTests ? /* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Play, { className: "mr-2 h-4 w-4" }),
3670
- isRunningTests ? "Running..." : "Run Public Tests"
3671
- ] }),
3672
- /* @__PURE__ */ jsxs(Tabs, { defaultValue: "tests", className: "w-full", children: [
3673
- /* @__PURE__ */ jsxs(TabsList, { children: [
3674
- /* @__PURE__ */ jsx(TabsTrigger, { value: "tests", children: "Test Cases" }),
3675
- showCorrectAnswer && /* @__PURE__ */ jsx(TabsTrigger, { value: "solution", children: "Solution" })
3676
- ] }),
3677
- /* @__PURE__ */ jsx(TabsContent, { value: "tests", children: /* @__PURE__ */ jsx("div", { className: "space-y-2 p-2 border rounded-md min-h-[100px]", children: testResults.length > 0 ? testResults.map((result, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center text-sm", children: [
3678
- result.passed ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 text-green-500 mr-2" }) : /* @__PURE__ */ jsx(XCircle, { className: "h-4 w-4 text-destructive mr-2" }),
3679
- /* @__PURE__ */ jsxs("span", { children: [
3680
- "Test Case #",
3681
- index + 1,
3682
- ": ",
3683
- result.passed ? "Passed" : "Failed"
3684
- ] }),
3685
- !result.passed && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground ml-2", children: [
3686
- "- ",
3687
- result.reasoning
3688
- ] })
3689
- ] }, result.testCaseId)) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: showCorrectAnswer ? "Final results are shown in the quiz summary." : "Click 'Run Public Tests' to see results." }) }) }),
3690
- showCorrectAnswer && /* @__PURE__ */ jsx(TabsContent, { value: "solution", children: /* @__PURE__ */ jsx("div", { className: "font-mono text-sm border rounded-md overflow-hidden", children: /* @__PURE__ */ jsx(
3691
- CodeMirror,
3692
- {
3693
- value: question.solutionCode,
3694
- height: "300px",
3695
- extensions: [langExtension],
3696
- readOnly: true,
3697
- theme: "dark"
3698
- }
3699
- ) }) })
3700
- ] }),
3701
- showCorrectAnswer && question.explanation && /* @__PURE__ */ jsxs("div", { className: "mt-4 p-3 bg-accent/20 border border-accent rounded-md", children: [
3702
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-accent-foreground", children: "Explanation:" }),
3703
- /* @__PURE__ */ jsx(MarkdownRenderer, { content: question.explanation, className: "text-sm text-accent-foreground/80" })
3704
- ] })
3705
- ] })
3706
- ] });
3707
- };
3708
- var QuestionRenderer = React9__default.forwardRef(({
3709
- question,
3710
- onAnswerChange,
3711
- userAnswer,
3712
- showCorrectAnswer = false
3713
- }, ref) => {
3714
- const { t } = useTranslation();
3715
- const commonProps = {
3716
- question,
3717
- onAnswerChange,
3718
- userAnswer,
3719
- showCorrectAnswer
3720
- };
3721
- switch (question.questionType) {
3722
- case "multiple_choice":
3723
- return /* @__PURE__ */ jsx(MultipleChoiceQuestionUI, { ...commonProps, question });
3724
- case "true_false":
3725
- return /* @__PURE__ */ jsx(TrueFalseQuestionUI, { ...commonProps, question });
3726
- case "multiple_response":
3727
- return /* @__PURE__ */ jsx(MultipleResponseQuestionUI, { ...commonProps, question });
3728
- case "short_answer":
3729
- return /* @__PURE__ */ jsx(ShortAnswerQuestionUI, { ...commonProps, question });
3730
- case "numeric":
3731
- return /* @__PURE__ */ jsx(NumericQuestionUI, { ...commonProps, question });
3732
- case "fill_in_the_blanks":
3733
- return /* @__PURE__ */ jsx(FillInTheBlanksQuestionUI, { ...commonProps, question });
3734
- case "sequence":
3735
- return /* @__PURE__ */ jsx(SequenceQuestionUI, { ...commonProps, question });
3736
- case "matching":
3737
- return /* @__PURE__ */ jsx(MatchingQuestionUI, { ...commonProps, question });
3738
- case "drag_and_drop":
3739
- return /* @__PURE__ */ jsx(DragAndDropQuestionUI, { ...commonProps, question });
3740
- case "hotspot":
3741
- return /* @__PURE__ */ jsx(HotspotQuestionUI, { ...commonProps, question });
3742
- case "blockly_programming":
3743
- return /* @__PURE__ */ jsx(BlocklyProgrammingQuestionUI, { ...commonProps, question, ref });
3744
- case "scratch_programming":
3745
- return /* @__PURE__ */ jsx(ScratchProgrammingQuestionUI, { ...commonProps, question, ref });
3746
- case "coding":
3747
- return /* @__PURE__ */ jsx(CodingQuestionUI, { ...commonProps, question });
3748
- default:
3749
- return /* @__PURE__ */ jsxs("div", { className: "p-4 border border-destructive bg-destructive/10 rounded-md", children: [
3750
- /* @__PURE__ */ jsx("p", { className: "font-semibold text-destructive", children: t("unsupportedQuestionType") }),
3751
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("unsupportedQuestionTypeDescription") }),
3752
- /* @__PURE__ */ jsx("pre", { className: "mt-2 p-2 bg-muted rounded text-xs font-code overflow-x-auto", children: JSON.stringify(question, null, 2) })
3753
- ] });
3754
- }
3755
- });
3756
- QuestionRenderer.displayName = "QuestionRenderer";
3757
- var Progress = React9.forwardRef(({ className, value, ...props }, ref) => /* @__PURE__ */ jsx(
3758
- ProgressPrimitive.Root,
3759
- {
3760
- ref,
3761
- className: cn(
3762
- "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
3763
- className
3764
- ),
3765
- ...props,
3766
- children: /* @__PURE__ */ jsx(
3767
- ProgressPrimitive.Indicator,
3768
- {
3769
- className: "h-full w-full flex-1 bg-primary transition-all",
3770
- style: { transform: `translateX(-${100 - (value || 0)}%)` }
3771
- }
3772
- )
3773
- }
3774
- ));
3775
- Progress.displayName = ProgressPrimitive.Root.displayName;
3776
- var Accordion = AccordionPrimitive.Root;
3777
- var AccordionItem = React9.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
3778
- AccordionPrimitive.Item,
3779
- {
3780
- ref,
3781
- className: cn("border-b", className),
3782
- ...props
3783
- }
3784
- ));
3785
- AccordionItem.displayName = "AccordionItem";
3786
- var AccordionTrigger = React9.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsx(AccordionPrimitive.Header, { className: "flex", children: /* @__PURE__ */ jsxs(
3787
- AccordionPrimitive.Trigger,
3788
- {
3789
- ref,
3790
- className: cn(
3791
- "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
3792
- className
3793
- ),
3794
- ...props,
3795
- children: [
3796
- children,
3797
- /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 shrink-0 transition-transform duration-200" })
3798
- ]
3799
- }
3800
- ) }));
3801
- AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
3802
- var AccordionContent = React9.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsx(
3803
- AccordionPrimitive.Content,
3804
- {
3805
- ref,
3806
- className: "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
3807
- ...props,
3808
- children: /* @__PURE__ */ jsx("div", { className: cn("pb-4 pt-0", className), children })
3809
- }
3810
- ));
3811
- AccordionContent.displayName = AccordionPrimitive.Content.displayName;
3812
- var ScrollArea = React9.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
3813
- ScrollAreaPrimitive.Root,
3814
- {
3815
- ref,
3816
- className: cn("relative overflow-hidden", className),
3817
- ...props,
3818
- children: [
3819
- /* @__PURE__ */ jsx(ScrollAreaPrimitive.Viewport, { className: "h-full w-full rounded-[inherit]", children }),
3820
- /* @__PURE__ */ jsx(ScrollBar, {}),
3821
- /* @__PURE__ */ jsx(ScrollAreaPrimitive.Corner, {})
3822
- ]
3823
- }
3824
- ));
3825
- ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
3826
- var ScrollBar = React9.forwardRef(({ className, orientation = "vertical", ...props }, ref) => /* @__PURE__ */ jsx(
3827
- ScrollAreaPrimitive.ScrollAreaScrollbar,
3828
- {
3829
- ref,
3830
- orientation,
3831
- className: cn(
3832
- "flex touch-none select-none transition-colors",
3833
- orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
3834
- orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
3835
- className
3836
- ),
3837
- ...props,
3838
- children: /* @__PURE__ */ jsx(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border" })
3839
- }
3840
- ));
3841
- ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
3842
- var QuizResult = ({
3843
- result,
3844
- quizTitle,
3845
- onExitQuiz,
3846
- onGenerateReview,
3847
- showReviewButton = false,
3848
- isReviewLoading = false
3849
- }) => {
3850
- const { t } = useTranslation();
3851
- const getAnswerDisplay = (answer) => {
3852
- if (answer === null || answer === void 0) return t("practiceFlow.results.notAnswered");
3853
- if (typeof answer === "boolean") return answer ? "True" : "False";
3854
- if (Array.isArray(answer)) return answer.join(", ");
3855
- if (typeof answer === "object") {
3856
- if (answer.hasOwnProperty("value")) {
3857
- const value = answer.value;
3858
- if (value === null || value === void 0) return t("practiceFlow.results.notAnswered");
3859
- if (typeof value === "boolean") return value ? "True" : "False";
3860
- if (Array.isArray(value)) return value.join(", ");
3861
- if (typeof value === "object") return JSON.stringify(value, null, 2);
3862
- return String(value);
3863
- }
3864
- return JSON.stringify(answer, null, 2);
3865
- }
3866
- return String(answer);
3867
- };
3868
- return /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-3xl mx-auto shadow-xl", children: [
3869
- /* @__PURE__ */ jsxs(CardHeader, { children: [
3870
- /* @__PURE__ */ jsx(CardTitle, { className: "text-3xl font-headline text-center", children: t("practiceFlow.results.title", { quizTitle }) }),
3871
- /* @__PURE__ */ jsx(CardDescription, { className: "text-center text-lg", children: t("practiceFlow.results.description") })
3872
- ] }),
3873
- /* @__PURE__ */ jsxs(CardContent, { className: "space-y-6", children: [
3874
- /* @__PURE__ */ jsxs(Card, { className: "bg-secondary/50", children: [
3875
- /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, { className: "text-xl flex items-center", children: [
3876
- /* @__PURE__ */ jsx(BarChart2, { className: "mr-2 h-5 w-5 text-primary" }),
3877
- t("practiceFlow.results.overallScore")
3878
- ] }) }),
3879
- /* @__PURE__ */ jsxs(CardContent, { className: "grid grid-cols-1 md:grid-cols-3 gap-4 text-center", children: [
3880
- /* @__PURE__ */ jsxs("div", { children: [
3881
- /* @__PURE__ */ jsxs("p", { className: "text-3xl font-bold text-primary", children: [
3882
- result.score,
3883
- " / ",
3884
- result.maxScore
3885
- ] }),
3886
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("practiceFlow.results.points") })
3887
- ] }),
3888
- /* @__PURE__ */ jsxs("div", { children: [
3889
- /* @__PURE__ */ jsxs("p", { className: "text-3xl font-bold text-primary", children: [
3890
- result.percentage.toFixed(2),
3891
- "%"
3892
- ] }),
3893
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("practiceFlow.results.percentage") })
3894
- ] }),
3895
- /* @__PURE__ */ jsx("div", { children: result.passed !== void 0 && (result.passed ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center text-green-600", children: [
3896
- /* @__PURE__ */ jsx(CheckCircle, { className: "h-10 w-10" }),
3897
- /* @__PURE__ */ jsx("p", { className: "text-xl font-semibold mt-1", children: t("practiceFlow.results.passed") })
3898
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center text-destructive", children: [
3899
- /* @__PURE__ */ jsx(XCircle, { className: "h-10 w-10" }),
3900
- /* @__PURE__ */ jsx("p", { className: "text-xl font-semibold mt-1", children: t("practiceFlow.results.failed") })
3901
- ] })) })
3902
- ] })
3903
- ] }),
3904
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm", children: [
3905
- /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md", children: [
3906
- /* @__PURE__ */ jsx(Clock, { className: "h-5 w-5 text-primary" }),
3907
- /* @__PURE__ */ jsx("span", { children: t("practiceFlow.results.timeSpent") }),
3908
- /* @__PURE__ */ jsxs("span", { className: "font-semibold", children: [
3909
- result.totalTimeSpentSeconds?.toFixed(0) ?? "N/A",
3910
- " ",
3911
- t("practiceFlow.results.timeUnit")
3912
- ] })
3913
- ] }),
3914
- /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2 p-3 bg-muted rounded-md", children: [
3915
- /* @__PURE__ */ jsx(Percent, { className: "h-5 w-5 text-primary" }),
3916
- /* @__PURE__ */ jsx("span", { children: t("practiceFlow.results.avgTimePerQuestion") }),
3917
- /* @__PURE__ */ jsxs("span", { className: "font-semibold", children: [
3918
- result.averageTimePerQuestionSeconds?.toFixed(1) ?? "N/A",
3919
- " ",
3920
- t("practiceFlow.results.timeUnit")
3921
- ] })
3922
- ] })
3923
- ] }),
3924
- result.scormStatus && result.scormStatus !== "idle" && result.scormStatus !== "no_api" && /* @__PURE__ */ jsxs(Card, { children: [
3925
- /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-lg", children: "SCORM Sync Status" }) }),
3926
- /* @__PURE__ */ jsxs(CardContent, { children: [
3927
- /* @__PURE__ */ jsxs("p", { className: `flex items-center ${result.scormStatus === "error" ? "text-destructive" : "text-muted-foreground"}`, children: [
3928
- result.scormStatus === "error" && /* @__PURE__ */ jsx(AlertTriangle, { className: "mr-2 h-4 w-4" }),
3929
- "Status: ",
3930
- /* @__PURE__ */ jsx("span", { className: "font-semibold ml-1", children: result.scormStatus })
3931
- ] }),
3932
- result.scormError && /* @__PURE__ */ jsxs("p", { className: "text-xs text-destructive mt-1", children: [
3933
- "Details: ",
3934
- result.scormError
3935
- ] })
3936
- ] })
3937
- ] }),
3938
- result.webhookStatus && result.webhookStatus !== "idle" && /* @__PURE__ */ jsxs(Card, { children: [
3939
- /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-lg", children: "Webhook Sync Status" }) }),
3940
- /* @__PURE__ */ jsxs(CardContent, { children: [
3941
- /* @__PURE__ */ jsxs("p", { className: `flex items-center ${result.webhookStatus === "error" ? "text-destructive" : "text-muted-foreground"}`, children: [
3942
- result.webhookStatus === "error" && /* @__PURE__ */ jsx(AlertTriangle, { className: "mr-2 h-4 w-4" }),
3943
- "Status: ",
3944
- /* @__PURE__ */ jsx("span", { className: "font-semibold ml-1", children: result.webhookStatus })
3945
- ] }),
3946
- result.webhookError && /* @__PURE__ */ jsxs("p", { className: "text-xs text-destructive mt-1", children: [
3947
- "Details: ",
3948
- result.webhookError
3949
- ] })
3950
- ] })
3951
- ] }),
3952
- /* @__PURE__ */ jsx(Accordion, { type: "single", collapsible: true, className: "w-full", children: /* @__PURE__ */ jsxs(AccordionItem, { value: "question-breakdown", children: [
3953
- /* @__PURE__ */ jsx(AccordionTrigger, { className: "text-lg font-semibold", children: t("practiceFlow.results.questionBreakdown") }),
3954
- /* @__PURE__ */ jsx(AccordionContent, { children: /* @__PURE__ */ jsx(ScrollArea, { className: "h-[300px] pr-4", children: /* @__PURE__ */ jsx("ul", { className: "space-y-4", children: result.questionResults.map((qResult, index) => /* @__PURE__ */ jsxs("li", { className: "p-4 border rounded-md bg-background", children: [
3955
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-2", children: [
3956
- /* @__PURE__ */ jsx("h4", { className: "font-semibold", children: t("common.questions", { count: index + 1 }) }),
3957
- qResult.isCorrect ? /* @__PURE__ */ jsxs("span", { className: "text-green-600 font-medium flex items-center", children: [
3958
- /* @__PURE__ */ jsx(CheckCircle, { className: "mr-1 h-4 w-4" }),
3959
- " ",
3960
- t("practiceFlow.results.passed")
3961
- ] }) : /* @__PURE__ */ jsxs("span", { className: "text-destructive font-medium flex items-center", children: [
3962
- /* @__PURE__ */ jsx(XCircle, { className: "mr-1 h-4 w-4" }),
3963
- " ",
3964
- t("practiceFlow.results.failed")
3965
- ] })
3966
- ] }),
3967
- /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
3968
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: t("practiceFlow.results.yourAnswer") }),
3969
- " ",
3970
- getAnswerDisplay(qResult.userAnswer)
3971
- ] }),
3972
- !qResult.isCorrect && /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
3973
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: t("practiceFlow.results.correctAnswer") }),
3974
- " ",
3975
- getAnswerDisplay(qResult.correctAnswer)
3976
- ] }),
3977
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
3978
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: t("practiceFlow.results.pointsEarned") }),
3979
- " ",
3980
- qResult.pointsEarned
3981
- ] }),
3982
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
3983
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: t("practiceFlow.results.timeSpent") }),
3984
- " ",
3985
- qResult.timeSpentSeconds?.toFixed(0) ?? "N/A",
3986
- t("practiceFlow.results.timeUnit")
3987
- ] })
3988
- ] }, qResult.questionId)) }) }) })
3989
- ] }) })
3990
- ] }),
3991
- /* @__PURE__ */ jsxs(CardFooter, { className: "flex flex-col sm:flex-row justify-between gap-2", children: [
3992
- onExitQuiz && /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: onExitQuiz, className: "w-full sm:w-auto", children: [
3993
- /* @__PURE__ */ jsx(LogOut, { className: "mr-2 h-4 w-4" }),
3994
- t("common.exit")
3995
- ] }),
3996
- showReviewButton && onGenerateReview && /* @__PURE__ */ jsxs(
3997
- Button,
3998
- {
3999
- onClick: onGenerateReview,
4000
- disabled: isReviewLoading,
4001
- className: "w-full sm:w-auto",
4002
- children: [
4003
- isReviewLoading ? /* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Wand2, { className: "mr-2 h-4 w-4" }),
4004
- isReviewLoading ? t("practiceFlow.results.generatingReview") : t("practiceFlow.results.generateReview")
4005
- ]
4006
- }
4007
- )
4008
- ] })
4009
- ] });
4010
- };
4011
- var QuizPlayer = ({ quizConfig, onQuizComplete, onExitQuiz }) => {
4012
- const [engine, setEngine] = useState(null);
4013
- const [currentQuestion, setCurrentQuestion] = useState(null);
4014
- const [currentQuestionNumber, setCurrentQuestionNumber] = useState(0);
4015
- const [totalQuestions, setTotalQuestions] = useState(0);
4016
- const [userAnswer, setUserAnswer] = useState(null);
4017
- const [quizFinished, setQuizFinished] = useState(false);
4018
- const [finalResult, setFinalResult] = useState(null);
4019
- const [timeLeft, setTimeLeft] = useState(null);
4020
- const [isLoading, setIsLoading] = useState(true);
4021
- const [error, setError] = useState(null);
4022
- const { t } = useTranslation();
4023
- const programmingQuestionRef = useRef(null);
4024
- const engineRef = useRef(null);
4025
- const isInitializedRef = useRef(false);
4026
- const callbacks = useMemo(() => ({
4027
- onQuizStart: (initialData) => {
4028
- setCurrentQuestion(initialData.initialQuestion);
4029
- setCurrentQuestionNumber(initialData.currentQuestionNumber);
4030
- setTotalQuestions(initialData.totalQuestions);
4031
- setTimeLeft(initialData.timeLimitInSeconds);
4032
- setIsLoading(false);
4033
- },
4034
- onQuestionChange: (question, qNum, total) => {
4035
- setCurrentQuestion(question);
4036
- setCurrentQuestionNumber(qNum);
4037
- setTotalQuestions(total);
4038
- const existingAnswer = engineRef.current?.getUserAnswer(question?.id || "");
4039
- setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
4040
- },
4041
- onQuizFinish: (results) => {
4042
- setFinalResult(results);
4043
- setQuizFinished(true);
4044
- onQuizComplete(results);
4045
- setIsLoading(false);
4046
- },
4047
- onTimeTick: (timeLeftInSeconds) => {
4048
- setTimeLeft(timeLeftInSeconds);
4049
- },
4050
- onQuizTimeUp: () => {
4051
- setError("Time's up! Your quiz has been submitted automatically.");
4052
- }
4053
- }), [onQuizComplete]);
4054
- const handleAnswerChange = useCallback((answer) => {
4055
- if (currentQuestion?.questionType !== "blockly_programming" && currentQuestion?.questionType !== "scratch_programming") {
4056
- setUserAnswer(answer);
4057
- }
4058
- }, [currentQuestion?.questionType]);
4059
- const quizConfigKey = useMemo(() => {
4060
- return JSON.stringify({
4061
- id: quizConfig.id,
4062
- version: quizConfig.version,
4063
- title: quizConfig.title
4064
- });
4065
- }, [quizConfig.id, quizConfig.version, quizConfig.title]);
4066
- useEffect(() => {
4067
- if (isInitializedRef.current) {
4068
- return;
4069
- }
4070
- setIsLoading(true);
4071
- setError(null);
4072
- setQuizFinished(false);
4073
- setFinalResult(null);
4074
- setUserAnswer(null);
4075
- isInitializedRef.current = true;
4076
- let localQuizEngine = null;
4077
- try {
4078
- localQuizEngine = new QuizEngine({ config: quizConfig, callbacks });
4079
- engineRef.current = localQuizEngine;
4080
- setEngine(localQuizEngine);
4081
- const initialQ = localQuizEngine.getCurrentQuestion();
4082
- setCurrentQuestion(initialQ);
4083
- setCurrentQuestionNumber(localQuizEngine.getCurrentQuestionNumber());
4084
- setTotalQuestions(localQuizEngine.getTotalQuestions());
4085
- setTimeLeft(localQuizEngine.getTimeLeftInSeconds());
4086
- if (initialQ) {
4087
- const existingAnswer = localQuizEngine.getUserAnswer(initialQ.id);
4088
- setUserAnswer(existingAnswer !== void 0 ? existingAnswer : null);
4089
- }
4090
- setIsLoading(false);
4091
- } catch (e) {
4092
- setError(e instanceof Error ? e.message : "Failed to load quiz.");
4093
- setIsLoading(false);
4094
- isInitializedRef.current = false;
4095
- }
4096
- return () => {
4097
- if (localQuizEngine) localQuizEngine.destroy();
4098
- if (engineRef.current) engineRef.current = null;
4099
- isInitializedRef.current = false;
4100
- };
4101
- }, [quizConfigKey, callbacks, quizConfig]);
4102
- const handleSubmitAnswer = useCallback(() => {
4103
- const currentEngine = engineRef.current;
4104
- if (!currentEngine || !currentQuestion) return;
4105
- let answerToSubmit = null;
4106
- if (currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming") {
4107
- if (programmingQuestionRef.current && typeof programmingQuestionRef.current.getWorkspaceXml === "function") {
4108
- answerToSubmit = programmingQuestionRef.current.getWorkspaceXml();
4109
- } else {
4110
- answerToSubmit = currentEngine.getUserAnswer(currentQuestion.id) ?? null;
4111
- }
4112
- } else {
4113
- answerToSubmit = userAnswer;
4114
- }
4115
- if (answerToSubmit !== void 0) {
4116
- currentEngine.submitAnswer(currentQuestion.id, answerToSubmit);
4117
- }
4118
- }, [currentQuestion, userAnswer]);
4119
- const handleNext = useCallback(() => {
4120
- const currentEngine = engineRef.current;
4121
- if (!currentEngine) return;
4122
- handleSubmitAnswer();
4123
- if (currentEngine.getCurrentQuestionNumber() < currentEngine.getTotalQuestions()) {
4124
- currentEngine.nextQuestion();
4125
- } else {
4126
- handleFinishQuiz();
4127
- }
4128
- }, [handleSubmitAnswer]);
4129
- const handlePrevious = useCallback(() => {
4130
- const currentEngine = engineRef.current;
4131
- if (!currentEngine || currentEngine.getCurrentQuestionNumber() <= 1) return;
4132
- handleSubmitAnswer();
4133
- currentEngine.previousQuestion();
4134
- }, [handleSubmitAnswer]);
4135
- const handleFinishQuiz = useCallback(async () => {
4136
- const currentEngine = engineRef.current;
4137
- if (!currentEngine) return;
4138
- setIsLoading(true);
4139
- handleSubmitAnswer();
4140
- await currentEngine.calculateResults();
4141
- }, [handleSubmitAnswer]);
4142
- const progressPercent = useMemo(() => {
4143
- if (totalQuestions === 0) return 0;
4144
- return quizFinished ? 100 : Math.max(0, Math.min(100, (currentQuestionNumber - 1) / totalQuestions * 100));
4145
- }, [currentQuestionNumber, totalQuestions, quizFinished]);
4146
- const formatTime = useCallback((seconds) => {
4147
- if (seconds === null) return "-:--";
4148
- const mins = Math.floor(seconds / 60);
4149
- const secs = seconds % 60;
4150
- return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
4151
- }, []);
4152
- if (isLoading) {
4153
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-64", children: [
4154
- /* @__PURE__ */ jsx(Loader2, { className: "h-12 w-12 animate-spin text-primary" }),
4155
- /* @__PURE__ */ jsx("p", { className: "mt-4 text-muted-foreground", children: t("common.loading") })
4156
- ] });
4157
- }
4158
- if (error) {
4159
- return /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-2xl mx-auto shadow-xl", children: [
4160
- /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, { className: "text-destructive flex items-center", children: [
4161
- /* @__PURE__ */ jsx(AlertCircle, { className: "mr-2 h-6 w-6" }),
4162
- "Quiz Error"
4163
- ] }) }),
4164
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { children: error }) }),
4165
- /* @__PURE__ */ jsx(CardFooter, { children: onExitQuiz && /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: onExitQuiz, children: [
4166
- /* @__PURE__ */ jsx(LogOut, { className: "mr-2 h-4 w-4" }),
4167
- " ",
4168
- t("common.exit")
4169
- ] }) })
4170
- ] });
4171
- }
4172
- if (quizFinished && finalResult) {
4173
- return /* @__PURE__ */ jsx(QuizResult, { result: finalResult, onExitQuiz, quizTitle: quizConfig.title });
4174
- }
4175
- if (!currentQuestion) {
4176
- return /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-2xl mx-auto shadow-xl", children: [
4177
- /* @__PURE__ */ jsxs(CardHeader, { children: [
4178
- /* @__PURE__ */ jsx(CardTitle, { children: "Quiz Ended" }),
4179
- /* @__PURE__ */ jsx(CardDescription, { children: "No more questions, or quiz not loaded correctly." })
4180
- ] }),
4181
- /* @__PURE__ */ jsx(CardFooter, { children: onExitQuiz && /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: onExitQuiz, children: [
4182
- /* @__PURE__ */ jsx(LogOut, { className: "mr-2 h-4 w-4" }),
4183
- " ",
4184
- t("common.exit")
4185
- ] }) })
4186
- ] });
4187
- }
4188
- return /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-3xl mx-auto shadow-xl", children: [
4189
- /* @__PURE__ */ jsxs(CardHeader, { children: [
4190
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center", children: [
4191
- /* @__PURE__ */ jsx(CardTitle, { className: "text-2xl font-headline", children: quizConfig.title }),
4192
- timeLeft !== null && /* @__PURE__ */ jsxs("div", { className: "flex items-center text-sm text-muted-foreground", children: [
4193
- /* @__PURE__ */ jsx(Clock, { className: "mr-1 h-4 w-4" }),
4194
- t("practiceFlow.player.timeLeft", { time: formatTime(timeLeft) })
4195
- ] })
4196
- ] }),
4197
- quizConfig.description && /* @__PURE__ */ jsx(CardDescription, { children: quizConfig.description }),
4198
- /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
4199
- /* @__PURE__ */ jsx(
4200
- Progress,
4201
- {
4202
- value: progressPercent,
4203
- "aria-label": `Quiz progress: ${currentQuestionNumber} of ${totalQuestions} questions`,
4204
- className: "w-full"
4205
- }
4206
- ),
4207
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 text-right", children: t("practiceFlow.player.questionProgress", { current: currentQuestionNumber, total: totalQuestions }) })
4208
- ] })
4209
- ] }),
4210
- /* @__PURE__ */ jsx(CardContent, { className: "min-h-[200px]", children: /* @__PURE__ */ jsx(
4211
- QuestionRenderer,
4212
- {
4213
- question: currentQuestion,
4214
- onAnswerChange: handleAnswerChange,
4215
- userAnswer,
4216
- showCorrectAnswer: quizConfig.settings?.showCorrectAnswers === "immediately",
4217
- ref: currentQuestion.questionType === "blockly_programming" || currentQuestion.questionType === "scratch_programming" ? programmingQuestionRef : null
4218
- },
4219
- currentQuestion.id
4220
- ) }),
4221
- /* @__PURE__ */ jsxs(CardFooter, { className: "flex justify-between items-center", children: [
4222
- /* @__PURE__ */ jsxs(
4223
- Button,
4224
- {
4225
- variant: "outline",
4226
- onClick: handlePrevious,
4227
- disabled: currentQuestionNumber <= 1,
4228
- children: [
4229
- /* @__PURE__ */ jsx(ChevronLeft, { className: "mr-2 h-4 w-4" }),
4230
- " ",
4231
- t("common.previous")
4232
- ]
4233
- }
4234
- ),
4235
- onExitQuiz && /* @__PURE__ */ jsx(
4236
- Button,
4237
- {
4238
- variant: "ghost",
4239
- onClick: onExitQuiz,
4240
- className: "text-muted-foreground hover:text-destructive",
4241
- children: t("common.exit")
4242
- }
4243
- ),
4244
- /* @__PURE__ */ jsxs(Button, { onClick: handleNext, children: [
4245
- currentQuestionNumber === totalQuestions ? t("practiceFlow.player.finishQuiz") : t("common.next"),
4246
- currentQuestionNumber !== totalQuestions && /* @__PURE__ */ jsx(ChevronRight, { className: "ml-2 h-4 w-4" }),
4247
- currentQuestionNumber === totalQuestions && /* @__PURE__ */ jsx(CheckCircle, { className: "ml-2 h-4 w-4" })
4248
- ] })
4249
- ] })
4250
- ] });
4251
- };
4252
-
4253
- // src/player.ts
4254
- function mountQuizPlayer(targetElementId, quizConfig) {
4255
- const targetElement = document.getElementById(targetElementId);
4256
- if (!targetElement) {
4257
- console.error(`Quiz Player Mount Error: Element with ID "${targetElementId}" not found.`);
4258
- document.body.innerHTML = `<p style="color: red; text-align: center; padding: 20px;">Critical Error: Target render element #${targetElementId} not found.</p>`;
4259
- return;
4260
- }
4261
- const AppContainer = () => {
4262
- const [quizResult, setQuizResult] = useState(null);
4263
- const handleQuizComplete = (result) => {
4264
- console.log("Quiz Complete (captured inside React AppContainer):", result);
4265
- setQuizResult(result);
4266
- };
4267
- const handleExit = () => {
4268
- console.log("Quiz Exited");
4269
- const rootEl = document.getElementById(targetElementId);
4270
- if (rootEl) {
4271
- const root2 = rootEl._reactRootContainer;
4272
- if (root2) {
4273
- root2.unmount();
4274
- }
4275
- rootEl.innerHTML = '<p style="text-align: center; padding: 20px; color: #4b5563;">Quiz exited. You may now close this window.</p>';
4276
- }
4277
- };
4278
- if (quizResult) {
4279
- return React9__default.createElement(QuizResult, {
4280
- result: quizResult,
4281
- quizTitle: quizConfig.title,
4282
- onExitQuiz: handleExit
4283
- });
4284
- }
4285
- return React9__default.createElement(QuizPlayer, {
4286
- quizConfig,
4287
- onQuizComplete: handleQuizComplete,
4288
- onExitQuiz: handleExit
4289
- });
4290
- };
4291
- const root = ReactDOM.createRoot(targetElement);
4292
- root.render(React9__default.createElement(React9__default.StrictMode, null, React9__default.createElement(AppContainer)));
4293
- }
4294
-
4295
- export { mountQuizPlayer };