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