@superbuilders/incept-renderer 0.1.0
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/README.md +170 -0
- package/dist/actions/index.d.ts +27 -0
- package/dist/actions/index.js +1893 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/components/index.d.ts +113 -0
- package/dist/components/index.js +2495 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +3640 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-DxNEXGoq.d.ts +508 -0
- package/dist/types-MOyn9ktl.d.ts +102 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3640 @@
|
|
|
1
|
+
import * as React3 from 'react';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
|
7
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
8
|
+
import { cva } from 'class-variance-authority';
|
|
9
|
+
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
10
|
+
import '@radix-ui/react-separator';
|
|
11
|
+
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react';
|
|
12
|
+
import { useSensors, useSensor, PointerSensor, KeyboardSensor, defaultDropAnimationSideEffects, DndContext, closestCenter, DragOverlay, useDraggable, useDroppable } from '@dnd-kit/core';
|
|
13
|
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
14
|
+
import { sortableKeyboardCoordinates, SortableContext, horizontalListSortingStrategy, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
|
|
15
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
16
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
17
|
+
import { z as z$1 } from 'zod';
|
|
18
|
+
|
|
19
|
+
// src/components/qti-renderer.tsx
|
|
20
|
+
function cn(...inputs) {
|
|
21
|
+
return twMerge(clsx(inputs));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/html/sanitize.ts
|
|
25
|
+
var DEFAULT_CONFIG = {
|
|
26
|
+
// HTML content tags
|
|
27
|
+
allowedTags: /* @__PURE__ */ new Set([
|
|
28
|
+
// Text content
|
|
29
|
+
"p",
|
|
30
|
+
"span",
|
|
31
|
+
"div",
|
|
32
|
+
"br",
|
|
33
|
+
"hr",
|
|
34
|
+
// Formatting
|
|
35
|
+
"b",
|
|
36
|
+
"i",
|
|
37
|
+
"u",
|
|
38
|
+
"strong",
|
|
39
|
+
"em",
|
|
40
|
+
"mark",
|
|
41
|
+
"small",
|
|
42
|
+
"sub",
|
|
43
|
+
"sup",
|
|
44
|
+
"code",
|
|
45
|
+
"pre",
|
|
46
|
+
"kbd",
|
|
47
|
+
// Lists
|
|
48
|
+
"ul",
|
|
49
|
+
"ol",
|
|
50
|
+
"li",
|
|
51
|
+
"dl",
|
|
52
|
+
"dt",
|
|
53
|
+
"dd",
|
|
54
|
+
// Tables
|
|
55
|
+
"table",
|
|
56
|
+
"thead",
|
|
57
|
+
"tbody",
|
|
58
|
+
"tfoot",
|
|
59
|
+
"tr",
|
|
60
|
+
"th",
|
|
61
|
+
"td",
|
|
62
|
+
"caption",
|
|
63
|
+
"colgroup",
|
|
64
|
+
"col",
|
|
65
|
+
// Media
|
|
66
|
+
"img",
|
|
67
|
+
"audio",
|
|
68
|
+
"video",
|
|
69
|
+
"source",
|
|
70
|
+
"track",
|
|
71
|
+
// Semantic
|
|
72
|
+
"article",
|
|
73
|
+
"section",
|
|
74
|
+
"nav",
|
|
75
|
+
"aside",
|
|
76
|
+
"header",
|
|
77
|
+
"footer",
|
|
78
|
+
"main",
|
|
79
|
+
"figure",
|
|
80
|
+
"figcaption",
|
|
81
|
+
"blockquote",
|
|
82
|
+
"cite",
|
|
83
|
+
// Links
|
|
84
|
+
"a",
|
|
85
|
+
// Forms (for future interactive elements)
|
|
86
|
+
"label",
|
|
87
|
+
"button"
|
|
88
|
+
]),
|
|
89
|
+
allowedAttributes: {
|
|
90
|
+
// Global attributes
|
|
91
|
+
"*": /* @__PURE__ */ new Set(["class", "id", "lang", "dir", "title", "style"]),
|
|
92
|
+
// Specific attributes
|
|
93
|
+
img: /* @__PURE__ */ new Set(["src", "alt", "width", "height", "style"]),
|
|
94
|
+
a: /* @__PURE__ */ new Set(["href", "target", "rel"]),
|
|
95
|
+
audio: /* @__PURE__ */ new Set(["src", "controls", "loop", "muted"]),
|
|
96
|
+
video: /* @__PURE__ */ new Set(["src", "controls", "loop", "muted", "width", "height", "poster"]),
|
|
97
|
+
source: /* @__PURE__ */ new Set(["src", "type"]),
|
|
98
|
+
track: /* @__PURE__ */ new Set(["src", "kind", "srclang", "label"])
|
|
99
|
+
},
|
|
100
|
+
allowDataAttributes: false,
|
|
101
|
+
allowMathML: true
|
|
102
|
+
};
|
|
103
|
+
var MATHML_TAGS = /* @__PURE__ */ new Set([
|
|
104
|
+
// Root
|
|
105
|
+
"math",
|
|
106
|
+
// Token elements
|
|
107
|
+
"mi",
|
|
108
|
+
"mn",
|
|
109
|
+
"mo",
|
|
110
|
+
"mtext",
|
|
111
|
+
"mspace",
|
|
112
|
+
"ms",
|
|
113
|
+
// Layout
|
|
114
|
+
"mrow",
|
|
115
|
+
"mfrac",
|
|
116
|
+
"msqrt",
|
|
117
|
+
"mroot",
|
|
118
|
+
"mstyle",
|
|
119
|
+
"merror",
|
|
120
|
+
"mpadded",
|
|
121
|
+
"mphantom",
|
|
122
|
+
"mfenced",
|
|
123
|
+
"menclose",
|
|
124
|
+
// Scripts and limits
|
|
125
|
+
"msub",
|
|
126
|
+
"msup",
|
|
127
|
+
"msubsup",
|
|
128
|
+
"munder",
|
|
129
|
+
"mover",
|
|
130
|
+
"munderover",
|
|
131
|
+
"mmultiscripts",
|
|
132
|
+
"mprescripts",
|
|
133
|
+
"none",
|
|
134
|
+
// Tables
|
|
135
|
+
"mtable",
|
|
136
|
+
"mtr",
|
|
137
|
+
"mtd",
|
|
138
|
+
"maligngroup",
|
|
139
|
+
"malignmark",
|
|
140
|
+
// Elementary math
|
|
141
|
+
"mstack",
|
|
142
|
+
"mlongdiv",
|
|
143
|
+
"msgroup",
|
|
144
|
+
"msrow",
|
|
145
|
+
"mscarries",
|
|
146
|
+
"mscarry",
|
|
147
|
+
"msline",
|
|
148
|
+
// Semantic
|
|
149
|
+
"semantics",
|
|
150
|
+
"annotation",
|
|
151
|
+
"annotation-xml"
|
|
152
|
+
]);
|
|
153
|
+
function sanitizeHtml(html, config = {}) {
|
|
154
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
155
|
+
const dangerousPatterns = [
|
|
156
|
+
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
|
157
|
+
/javascript:/gi,
|
|
158
|
+
/on\w+\s*=/gi,
|
|
159
|
+
// Event handlers
|
|
160
|
+
/<iframe\b/gi,
|
|
161
|
+
/<embed\b/gi,
|
|
162
|
+
/<object\b/gi,
|
|
163
|
+
/data:text\/html/gi
|
|
164
|
+
];
|
|
165
|
+
let sanitized = html;
|
|
166
|
+
for (const pattern of dangerousPatterns) {
|
|
167
|
+
sanitized = sanitized.replace(pattern, "");
|
|
168
|
+
}
|
|
169
|
+
const cleaned = cleanHtml(sanitized, cfg);
|
|
170
|
+
return cleaned;
|
|
171
|
+
}
|
|
172
|
+
function cleanHtml(html, config) {
|
|
173
|
+
let cleaned = html;
|
|
174
|
+
const tagPattern = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g;
|
|
175
|
+
cleaned = cleaned.replace(tagPattern, (match, tagName, _attrs) => {
|
|
176
|
+
const tag = tagName.toLowerCase();
|
|
177
|
+
const isMathML = tag === "math" || cleaned.includes("<math");
|
|
178
|
+
if (isMathML && config.allowMathML && MATHML_TAGS.has(tag)) {
|
|
179
|
+
return match;
|
|
180
|
+
}
|
|
181
|
+
if (config.allowedTags.has(tag)) {
|
|
182
|
+
return cleanAttributesString(match, tag, config);
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
});
|
|
186
|
+
return cleaned;
|
|
187
|
+
}
|
|
188
|
+
function cleanAttributesString(tagString, tagName, config) {
|
|
189
|
+
const attrPattern = /\s+([a-zA-Z][a-zA-Z0-9-:]*)(?:="([^"]*)"|'([^']*)'|=([^\s>]+)|(?=\s|>))/g;
|
|
190
|
+
let cleanedTag = `<${tagString.startsWith("</") ? "/" : ""}${tagName}`;
|
|
191
|
+
let match = attrPattern.exec(tagString);
|
|
192
|
+
while (match !== null) {
|
|
193
|
+
const attrName = (match[1] || "").toLowerCase();
|
|
194
|
+
const attrValue = match[2] || match[3] || match[4] || "";
|
|
195
|
+
const globalAttrs = config.allowedAttributes["*"] || /* @__PURE__ */ new Set();
|
|
196
|
+
const tagAttrs = config.allowedAttributes[tagName] || /* @__PURE__ */ new Set();
|
|
197
|
+
let isAllowed = globalAttrs.has(attrName) || tagAttrs.has(attrName);
|
|
198
|
+
if (tagName === "img" && (attrName === "width" || attrName === "height" || attrName === "style")) {
|
|
199
|
+
isAllowed = false;
|
|
200
|
+
}
|
|
201
|
+
if (!isAllowed && config.allowDataAttributes && attrName.startsWith("data-")) {
|
|
202
|
+
isAllowed = true;
|
|
203
|
+
}
|
|
204
|
+
if (isAllowed && !isDangerousAttributeValue(attrName, attrValue)) {
|
|
205
|
+
cleanedTag += ` ${attrName}="${attrValue.replace(/"/g, """)}"`;
|
|
206
|
+
}
|
|
207
|
+
match = attrPattern.exec(tagString);
|
|
208
|
+
}
|
|
209
|
+
cleanedTag += ">";
|
|
210
|
+
return cleanedTag;
|
|
211
|
+
}
|
|
212
|
+
function isDangerousAttributeValue(name, value) {
|
|
213
|
+
const valueLower = value.toLowerCase().trim();
|
|
214
|
+
if (name === "href" || name === "src") {
|
|
215
|
+
if (valueLower.startsWith("javascript:") || valueLower.startsWith("data:text/html")) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (valueLower.includes("javascript:") || valueLower.includes("onerror=")) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
function sanitizeForDisplay(html) {
|
|
225
|
+
const hasMathML = html.toLowerCase().includes("<math");
|
|
226
|
+
return sanitizeHtml(html, {
|
|
227
|
+
allowMathML: hasMathML,
|
|
228
|
+
allowDataAttributes: true
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/html/serialize.ts
|
|
233
|
+
function serializeNodes(nodes) {
|
|
234
|
+
return nodes.map((node) => {
|
|
235
|
+
if (typeof node === "string") {
|
|
236
|
+
return escapeHtml(node);
|
|
237
|
+
}
|
|
238
|
+
return serializeNode(node);
|
|
239
|
+
}).join("");
|
|
240
|
+
}
|
|
241
|
+
function serializeInner(node) {
|
|
242
|
+
return node.children.map((child) => {
|
|
243
|
+
if (typeof child === "string") {
|
|
244
|
+
return escapeHtml(child);
|
|
245
|
+
}
|
|
246
|
+
return serializeNode(child);
|
|
247
|
+
}).join("");
|
|
248
|
+
}
|
|
249
|
+
function serializeNode(node) {
|
|
250
|
+
const selfClosing = ["br", "hr", "img", "input", "meta", "link"];
|
|
251
|
+
const tagName = stripQtiPrefix(node.tagName);
|
|
252
|
+
if (tagName === "math" || isMathMLElement(tagName)) {
|
|
253
|
+
return serializeVerbatim(node);
|
|
254
|
+
}
|
|
255
|
+
const attrs = serializeAttributes(node.attrs);
|
|
256
|
+
const attrString = attrs ? ` ${attrs}` : "";
|
|
257
|
+
if (selfClosing.includes(tagName) && node.children.length === 0) {
|
|
258
|
+
return `<${tagName}${attrString} />`;
|
|
259
|
+
}
|
|
260
|
+
const inner = serializeInner(node);
|
|
261
|
+
return `<${tagName}${attrString}>${inner}</${tagName}>`;
|
|
262
|
+
}
|
|
263
|
+
function detectContentType(html) {
|
|
264
|
+
const mathmlIndicators = ["<math", "<mrow", "<msup", "<msub", "<mfrac", "<msqrt", "<mi>", "<mo>", "<mn>", "<mtext"];
|
|
265
|
+
const normalized = html.toLowerCase();
|
|
266
|
+
for (const indicator of mathmlIndicators) {
|
|
267
|
+
if (normalized.includes(indicator)) {
|
|
268
|
+
return "mathml";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return "html";
|
|
272
|
+
}
|
|
273
|
+
function stripQtiPrefix(tagName) {
|
|
274
|
+
const qtiToHtml = {
|
|
275
|
+
"qti-item-body": "div",
|
|
276
|
+
"qti-choice-interaction": "div",
|
|
277
|
+
"qti-simple-choice": "div",
|
|
278
|
+
"qti-feedback-block": "div",
|
|
279
|
+
"qti-content-body": "div",
|
|
280
|
+
"qti-prompt": "div",
|
|
281
|
+
"qti-value": "span",
|
|
282
|
+
"qti-variable": "span",
|
|
283
|
+
"qti-correct-response": "div"
|
|
284
|
+
};
|
|
285
|
+
if (qtiToHtml[tagName]) {
|
|
286
|
+
return qtiToHtml[tagName];
|
|
287
|
+
}
|
|
288
|
+
if (tagName.startsWith("qti-")) {
|
|
289
|
+
return tagName.slice(4);
|
|
290
|
+
}
|
|
291
|
+
return tagName;
|
|
292
|
+
}
|
|
293
|
+
function serializeAttributes(attrs) {
|
|
294
|
+
const excludeAttrs = [
|
|
295
|
+
"identifier",
|
|
296
|
+
"response-identifier",
|
|
297
|
+
"shuffle",
|
|
298
|
+
"max-choices",
|
|
299
|
+
"min-choices",
|
|
300
|
+
"cardinality",
|
|
301
|
+
"base-type",
|
|
302
|
+
"outcome-identifier",
|
|
303
|
+
"show-hide",
|
|
304
|
+
"time-dependent",
|
|
305
|
+
"adaptive",
|
|
306
|
+
"xml-lang"
|
|
307
|
+
];
|
|
308
|
+
const parts = [];
|
|
309
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
310
|
+
if (excludeAttrs.includes(key)) continue;
|
|
311
|
+
if (value == null) continue;
|
|
312
|
+
const htmlKey = key === "xml:lang" ? "lang" : key;
|
|
313
|
+
const escapedValue = escapeHtml(String(value));
|
|
314
|
+
parts.push(`${htmlKey}="${escapedValue}"`);
|
|
315
|
+
}
|
|
316
|
+
return parts.join(" ");
|
|
317
|
+
}
|
|
318
|
+
function escapeHtml(text) {
|
|
319
|
+
const escapeMap = {
|
|
320
|
+
"&": "&",
|
|
321
|
+
"<": "<",
|
|
322
|
+
">": ">",
|
|
323
|
+
'"': """,
|
|
324
|
+
"'": "'"
|
|
325
|
+
};
|
|
326
|
+
return text.replace(/[&<>"']/g, (char) => escapeMap[char] || char);
|
|
327
|
+
}
|
|
328
|
+
function isMathMLElement(tagName) {
|
|
329
|
+
const mathMLTags = [
|
|
330
|
+
"mi",
|
|
331
|
+
"mn",
|
|
332
|
+
"mo",
|
|
333
|
+
"mtext",
|
|
334
|
+
"mspace",
|
|
335
|
+
"ms",
|
|
336
|
+
"mrow",
|
|
337
|
+
"mfrac",
|
|
338
|
+
"msqrt",
|
|
339
|
+
"mroot",
|
|
340
|
+
"mstyle",
|
|
341
|
+
"msub",
|
|
342
|
+
"msup",
|
|
343
|
+
"msubsup",
|
|
344
|
+
"munder",
|
|
345
|
+
"mover",
|
|
346
|
+
"munderover",
|
|
347
|
+
"mtable",
|
|
348
|
+
"mtr",
|
|
349
|
+
"mtd"
|
|
350
|
+
];
|
|
351
|
+
return mathMLTags.includes(tagName);
|
|
352
|
+
}
|
|
353
|
+
function serializeVerbatim(node) {
|
|
354
|
+
const attrs = Object.entries(node.attrs).map(([key, value]) => {
|
|
355
|
+
if (value == null) return "";
|
|
356
|
+
return ` ${key}="${String(value).replace(/"/g, """)}"`;
|
|
357
|
+
}).join("");
|
|
358
|
+
if (node.children.length === 0) {
|
|
359
|
+
return `<${node.tagName}${attrs} />`;
|
|
360
|
+
}
|
|
361
|
+
const inner = node.children.map((child) => {
|
|
362
|
+
if (typeof child === "string") {
|
|
363
|
+
return escapeHtml(child);
|
|
364
|
+
}
|
|
365
|
+
return serializeVerbatim(child);
|
|
366
|
+
}).join("");
|
|
367
|
+
return `<${node.tagName}${attrs}>${inner}</${node.tagName}>`;
|
|
368
|
+
}
|
|
369
|
+
function HTMLContent({
|
|
370
|
+
html,
|
|
371
|
+
className,
|
|
372
|
+
inlineEmbeds,
|
|
373
|
+
textEmbeds,
|
|
374
|
+
gapEmbeds,
|
|
375
|
+
renderInline,
|
|
376
|
+
renderTextEntry,
|
|
377
|
+
renderGap
|
|
378
|
+
}) {
|
|
379
|
+
const containerRef = React3.useRef(null);
|
|
380
|
+
const [targets, setTargets] = React3.useState({});
|
|
381
|
+
const safeHtml = React3.useMemo(() => sanitizeForDisplay(html), [html]);
|
|
382
|
+
React3.useEffect(() => {
|
|
383
|
+
const el = containerRef.current;
|
|
384
|
+
if (!el) return;
|
|
385
|
+
el.innerHTML = safeHtml;
|
|
386
|
+
const newTargets = {};
|
|
387
|
+
if (inlineEmbeds) {
|
|
388
|
+
const nodes = el.querySelectorAll("[data-qti-inline]");
|
|
389
|
+
for (const node of nodes) {
|
|
390
|
+
const responseId = node.getAttribute("data-qti-inline");
|
|
391
|
+
if (responseId && inlineEmbeds[responseId]) {
|
|
392
|
+
newTargets[responseId] = node;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (textEmbeds) {
|
|
397
|
+
const nodes = el.querySelectorAll("[data-qti-text-entry]");
|
|
398
|
+
for (const node of nodes) {
|
|
399
|
+
const responseId = node.getAttribute("data-qti-text-entry");
|
|
400
|
+
if (responseId && textEmbeds[responseId]) {
|
|
401
|
+
newTargets[responseId] = node;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (gapEmbeds) {
|
|
406
|
+
const nodes = el.querySelectorAll("[data-qti-gap]");
|
|
407
|
+
for (const node of nodes) {
|
|
408
|
+
const gapId = node.getAttribute("data-qti-gap");
|
|
409
|
+
if (gapId && gapEmbeds[gapId]) {
|
|
410
|
+
newTargets[`gap-${gapId}`] = node;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
setTargets(newTargets);
|
|
415
|
+
}, [safeHtml, inlineEmbeds, textEmbeds, gapEmbeds]);
|
|
416
|
+
const portals = React3.useMemo(() => {
|
|
417
|
+
const items = [];
|
|
418
|
+
if (inlineEmbeds && renderInline) {
|
|
419
|
+
for (const [id, embed] of Object.entries(inlineEmbeds)) {
|
|
420
|
+
const target = targets[id];
|
|
421
|
+
if (target) {
|
|
422
|
+
items.push(createPortal(renderInline(embed), target, id));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (textEmbeds && renderTextEntry) {
|
|
427
|
+
for (const [id, embed] of Object.entries(textEmbeds)) {
|
|
428
|
+
const target = targets[id];
|
|
429
|
+
if (target) {
|
|
430
|
+
items.push(createPortal(renderTextEntry(embed), target, id));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (gapEmbeds && renderGap) {
|
|
435
|
+
for (const [id, gap] of Object.entries(gapEmbeds)) {
|
|
436
|
+
const target = targets[`gap-${id}`];
|
|
437
|
+
if (target) {
|
|
438
|
+
items.push(createPortal(renderGap(gap), target, `gap-${id}`));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return items;
|
|
443
|
+
}, [targets, inlineEmbeds, textEmbeds, gapEmbeds, renderInline, renderTextEntry, renderGap]);
|
|
444
|
+
React3.useEffect(() => {
|
|
445
|
+
const checkMathMLSupport = () => {
|
|
446
|
+
const mml = document.createElement("math");
|
|
447
|
+
mml.innerHTML = "<mspace/>";
|
|
448
|
+
const firstChild = mml.firstChild;
|
|
449
|
+
return firstChild instanceof Element && firstChild.namespaceURI === "http://www.w3.org/1998/Math/MathML";
|
|
450
|
+
};
|
|
451
|
+
if (!checkMathMLSupport() && containerRef.current?.querySelector("math")) ;
|
|
452
|
+
}, []);
|
|
453
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
454
|
+
/* @__PURE__ */ jsx(
|
|
455
|
+
"div",
|
|
456
|
+
{
|
|
457
|
+
ref: containerRef,
|
|
458
|
+
className: cn(
|
|
459
|
+
"qti-html-content text-foreground text-lg font-medium leading-relaxed",
|
|
460
|
+
// Ensure images never stretch or overflow
|
|
461
|
+
"[&_img]:max-w-full [&_img]:h-auto [&_img]:w-auto [&_img]:object-contain [&_img]:block [&_img]:mx-auto",
|
|
462
|
+
className
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
),
|
|
466
|
+
portals
|
|
467
|
+
] });
|
|
468
|
+
}
|
|
469
|
+
function FeedbackMessage({
|
|
470
|
+
responseId,
|
|
471
|
+
showFeedback,
|
|
472
|
+
perResponseFeedback
|
|
473
|
+
}) {
|
|
474
|
+
if (!showFeedback) return null;
|
|
475
|
+
const entry = perResponseFeedback?.[responseId];
|
|
476
|
+
const messageHtml = entry?.messageHtml;
|
|
477
|
+
if (!messageHtml) return null;
|
|
478
|
+
const isCorrect = entry?.isCorrect === true;
|
|
479
|
+
return /* @__PURE__ */ jsx("div", { className: "qti-feedback mt-3 p-6", "data-correct": isCorrect, children: /* @__PURE__ */ jsx(HTMLContent, { html: messageHtml }) });
|
|
480
|
+
}
|
|
481
|
+
var choiceIndicatorVariants = cva(
|
|
482
|
+
[
|
|
483
|
+
"cursor-pointer shrink-0 rounded-xs size-6 text-xs font-extrabold flex items-center justify-center outline-none",
|
|
484
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
485
|
+
"data-[state=unchecked]:bg-background data-[state=unchecked]:border-2 data-[state=unchecked]:border-input data-[state=unchecked]:text-muted-foreground",
|
|
486
|
+
"data-[state=checked]:border-2 data-[state=checked]:border-[var(--choice-complement)] data-[state=checked]:text-[var(--choice-foreground)]",
|
|
487
|
+
"data-[filled=true]:data-[state=checked]:bg-[var(--choice-complement)]",
|
|
488
|
+
"data-[filled=true]:data-[state=checked]:shadow-[inset_0_0_0_2px_rgb(255_255_255)] dark:data-[filled=true]:data-[state=checked]:shadow-[inset_0_0_0_2px_rgb(255_255_255)]",
|
|
489
|
+
"focus-visible:ring-[var(--choice-complement)] focus-visible:ring-[3px]",
|
|
490
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
|
491
|
+
],
|
|
492
|
+
{
|
|
493
|
+
variants: {
|
|
494
|
+
palette: {
|
|
495
|
+
default: [
|
|
496
|
+
"[--choice-foreground:var(--color-foreground)]",
|
|
497
|
+
"[--choice-complement:var(--color-muted-foreground)]"
|
|
498
|
+
],
|
|
499
|
+
betta: [
|
|
500
|
+
"[--choice-foreground:var(--color-betta)]",
|
|
501
|
+
"[--choice-complement:var(--color-butterfly)]"
|
|
502
|
+
],
|
|
503
|
+
cardinal: [
|
|
504
|
+
"[--choice-foreground:var(--color-cardinal)]",
|
|
505
|
+
"[--choice-complement:var(--color-fire-ant)]"
|
|
506
|
+
],
|
|
507
|
+
bee: [
|
|
508
|
+
"[--choice-foreground:var(--color-bee)]",
|
|
509
|
+
"[--choice-complement:var(--color-lion)]"
|
|
510
|
+
],
|
|
511
|
+
owl: [
|
|
512
|
+
"[--choice-foreground:var(--color-owl)]",
|
|
513
|
+
"[--choice-complement:var(--color-tree-frog)]"
|
|
514
|
+
],
|
|
515
|
+
macaw: [
|
|
516
|
+
"[--choice-foreground:var(--color-macaw)]",
|
|
517
|
+
"[--choice-complement:var(--color-whale)]"
|
|
518
|
+
]
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
defaultVariants: {
|
|
522
|
+
palette: "default"
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
function ChoiceIndicator({
|
|
527
|
+
className,
|
|
528
|
+
palette = "default",
|
|
529
|
+
letter,
|
|
530
|
+
type = "radio",
|
|
531
|
+
showLetter = true,
|
|
532
|
+
...props
|
|
533
|
+
}) {
|
|
534
|
+
const baseClassName = cn(choiceIndicatorVariants({ palette }), className);
|
|
535
|
+
if (type === "checkbox") {
|
|
536
|
+
const checkboxProps = props;
|
|
537
|
+
return /* @__PURE__ */ jsx(
|
|
538
|
+
CheckboxPrimitive.Root,
|
|
539
|
+
{
|
|
540
|
+
"data-slot": "choice-indicator",
|
|
541
|
+
"data-palette": palette,
|
|
542
|
+
"data-filled": !showLetter,
|
|
543
|
+
className: baseClassName,
|
|
544
|
+
...checkboxProps,
|
|
545
|
+
children: showLetter && /* @__PURE__ */ jsx("span", { className: "data-[state=unchecked]:block data-[state=checked]:hidden", children: letter })
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const radioProps = props;
|
|
550
|
+
return /* @__PURE__ */ jsx(
|
|
551
|
+
RadioGroupPrimitive.Item,
|
|
552
|
+
{
|
|
553
|
+
"data-slot": "choice-indicator",
|
|
554
|
+
"data-palette": palette,
|
|
555
|
+
className: baseClassName,
|
|
556
|
+
...radioProps,
|
|
557
|
+
children: letter
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
function Label({
|
|
562
|
+
className,
|
|
563
|
+
...props
|
|
564
|
+
}) {
|
|
565
|
+
return /* @__PURE__ */ jsx(
|
|
566
|
+
LabelPrimitive.Root,
|
|
567
|
+
{
|
|
568
|
+
"data-slot": "label",
|
|
569
|
+
className: cn(
|
|
570
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
571
|
+
className
|
|
572
|
+
),
|
|
573
|
+
...props
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
function FieldSet({ className, ...props }) {
|
|
578
|
+
return /* @__PURE__ */ jsx(
|
|
579
|
+
"fieldset",
|
|
580
|
+
{
|
|
581
|
+
"data-slot": "field-set",
|
|
582
|
+
className: cn(
|
|
583
|
+
"flex flex-col gap-6",
|
|
584
|
+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
585
|
+
className
|
|
586
|
+
),
|
|
587
|
+
...props
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
function FieldGroup({ className, ...props }) {
|
|
592
|
+
return /* @__PURE__ */ jsx(
|
|
593
|
+
"div",
|
|
594
|
+
{
|
|
595
|
+
"data-slot": "field-group",
|
|
596
|
+
className: cn(
|
|
597
|
+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
598
|
+
className
|
|
599
|
+
),
|
|
600
|
+
...props
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
var fieldVariants = cva(
|
|
605
|
+
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
|
606
|
+
{
|
|
607
|
+
variants: {
|
|
608
|
+
orientation: {
|
|
609
|
+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
610
|
+
horizontal: [
|
|
611
|
+
"flex-row items-center",
|
|
612
|
+
"[&>[data-slot=field-label]]:flex-auto",
|
|
613
|
+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px"
|
|
614
|
+
],
|
|
615
|
+
responsive: [
|
|
616
|
+
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
|
617
|
+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
618
|
+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px"
|
|
619
|
+
]
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
defaultVariants: {
|
|
623
|
+
orientation: "vertical"
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
function Field({
|
|
628
|
+
className,
|
|
629
|
+
orientation = "vertical",
|
|
630
|
+
...props
|
|
631
|
+
}) {
|
|
632
|
+
return /* @__PURE__ */ jsx(
|
|
633
|
+
"div",
|
|
634
|
+
{
|
|
635
|
+
role: "group",
|
|
636
|
+
"data-slot": "field",
|
|
637
|
+
"data-orientation": orientation,
|
|
638
|
+
className: cn(fieldVariants({ orientation }), className),
|
|
639
|
+
...props
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
function FieldContent({ className, ...props }) {
|
|
644
|
+
return /* @__PURE__ */ jsx(
|
|
645
|
+
"div",
|
|
646
|
+
{
|
|
647
|
+
"data-slot": "field-content",
|
|
648
|
+
className: cn(
|
|
649
|
+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
650
|
+
className
|
|
651
|
+
),
|
|
652
|
+
...props
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
function FieldLabel({
|
|
657
|
+
className,
|
|
658
|
+
palette = "default",
|
|
659
|
+
...props
|
|
660
|
+
}) {
|
|
661
|
+
return /* @__PURE__ */ jsx(
|
|
662
|
+
Label,
|
|
663
|
+
{
|
|
664
|
+
"data-slot": "field-label",
|
|
665
|
+
"data-palette": palette,
|
|
666
|
+
className: cn(
|
|
667
|
+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug cursor-pointer",
|
|
668
|
+
"group-data-[disabled=true]/field:opacity-50",
|
|
669
|
+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-xs has-[>[data-slot=field]]:p-4",
|
|
670
|
+
"has-[>[data-slot=field]]:border-2 has-[>[data-slot=field]]:shadow-[0_2px_0_var(--field-complement)]",
|
|
671
|
+
"has-[>[data-slot=field]]:hover:brightness-90 dark:has-[>[data-slot=field]]:hover:brightness-75",
|
|
672
|
+
"has-[>[data-slot=field]]:active:shadow-none has-[>[data-slot=field]]:active:translate-y-[2px]",
|
|
673
|
+
"[--field-background:var(--color-background)]",
|
|
674
|
+
"[--field-complement:var(--color-accent)]",
|
|
675
|
+
"has-[>[data-slot=field]]:bg-[var(--field-background)] has-[>[data-slot=field]]:border-accent has-[>[data-slot=field]]:text-foreground",
|
|
676
|
+
"has-[>[data-slot=field]]:has-data-[state=checked]:bg-[var(--field-foreground)]/10 has-[>[data-slot=field]]:has-data-[state=checked]:border-[var(--field-complement)] has-[>[data-slot=field]]:has-data-[state=checked]:text-[var(--field-foreground)]",
|
|
677
|
+
"has-data-[state=checked]:[--field-complement:var(--field-shadow)]",
|
|
678
|
+
"data-[palette=default]:[--field-foreground:var(--color-foreground)] data-[palette=default]:[--field-shadow:var(--color-muted-foreground)]",
|
|
679
|
+
"data-[palette=betta]:[--field-foreground:var(--color-betta)] data-[palette=betta]:[--field-shadow:var(--color-butterfly)]",
|
|
680
|
+
"data-[palette=cardinal]:[--field-foreground:var(--color-cardinal)] data-[palette=cardinal]:[--field-shadow:var(--color-fire-ant)]",
|
|
681
|
+
"data-[palette=bee]:[--field-foreground:var(--color-bee)] data-[palette=bee]:[--field-shadow:var(--color-lion)]",
|
|
682
|
+
"data-[palette=owl]:[--field-foreground:var(--color-owl)] data-[palette=owl]:[--field-shadow:var(--color-tree-frog)]",
|
|
683
|
+
"data-[palette=macaw]:[--field-foreground:var(--color-macaw)] data-[palette=macaw]:[--field-shadow:var(--color-whale)]",
|
|
684
|
+
className
|
|
685
|
+
),
|
|
686
|
+
...props
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
function FieldTitle({ className, ...props }) {
|
|
691
|
+
return /* @__PURE__ */ jsx(
|
|
692
|
+
"div",
|
|
693
|
+
{
|
|
694
|
+
"data-slot": "field-label",
|
|
695
|
+
className: cn(
|
|
696
|
+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
697
|
+
className
|
|
698
|
+
),
|
|
699
|
+
...props
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
cva(
|
|
704
|
+
[
|
|
705
|
+
"cursor-pointer",
|
|
706
|
+
"hover:brightness-90 dark:hover:brightness-75",
|
|
707
|
+
"text-[var(--radio-group-item-foreground)] bg-[var(--radio-group-item-background)] dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border outline-none",
|
|
708
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
709
|
+
"data-[state=unchecked]:border-input",
|
|
710
|
+
"data-[state=checked]:border-[var(--radio-group-item-complement)]",
|
|
711
|
+
"focus-visible:ring-[var(--radio-group-item-foreground)] focus-visible:ring-[3px]",
|
|
712
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
|
713
|
+
],
|
|
714
|
+
{
|
|
715
|
+
variants: {
|
|
716
|
+
palette: {
|
|
717
|
+
default: [
|
|
718
|
+
"[--radio-group-item-background:var(--color-accent)]",
|
|
719
|
+
"[--radio-group-item-foreground:var(--color-foreground)]",
|
|
720
|
+
"[--radio-group-item-complement:var(--color-foreground)]"
|
|
721
|
+
],
|
|
722
|
+
betta: [
|
|
723
|
+
"[--radio-group-item-background:var(--color-accent)]",
|
|
724
|
+
"[--radio-group-item-foreground:var(--color-betta)]",
|
|
725
|
+
"[--radio-group-item-complement:var(--color-butterfly)]"
|
|
726
|
+
],
|
|
727
|
+
cardinal: [
|
|
728
|
+
"[--radio-group-item-background:var(--color-accent)]",
|
|
729
|
+
"[--radio-group-item-foreground:var(--color-cardinal)]",
|
|
730
|
+
"[--radio-group-item-complement:var(--color-fire-ant)]"
|
|
731
|
+
],
|
|
732
|
+
bee: [
|
|
733
|
+
"[--radio-group-item-background:var(--color-accent)]",
|
|
734
|
+
"[--radio-group-item-foreground:var(--color-bee)]",
|
|
735
|
+
"[--radio-group-item-complement:var(--color-lion)]"
|
|
736
|
+
],
|
|
737
|
+
owl: [
|
|
738
|
+
"[--radio-group-item-background:var(--color-accent)]",
|
|
739
|
+
"[--radio-group-item-foreground:var(--color-owl)]",
|
|
740
|
+
"[--radio-group-item-complement:var(--color-tree-frog)]"
|
|
741
|
+
],
|
|
742
|
+
macaw: [
|
|
743
|
+
"[--radio-group-item-background:var(--color-accent)]",
|
|
744
|
+
"[--radio-group-item-foreground:var(--color-macaw)]",
|
|
745
|
+
"[--radio-group-item-complement:var(--color-whale)]"
|
|
746
|
+
]
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
defaultVariants: {
|
|
750
|
+
palette: "default"
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
);
|
|
754
|
+
function RadioGroup({
|
|
755
|
+
className,
|
|
756
|
+
...props
|
|
757
|
+
}) {
|
|
758
|
+
return /* @__PURE__ */ jsx(
|
|
759
|
+
RadioGroupPrimitive.Root,
|
|
760
|
+
{
|
|
761
|
+
"data-slot": "radio-group",
|
|
762
|
+
className: cn("grid gap-3", className),
|
|
763
|
+
...props
|
|
764
|
+
}
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
function ChoiceInteractionRenderer({
|
|
768
|
+
interaction,
|
|
769
|
+
response,
|
|
770
|
+
onAnswerSelect,
|
|
771
|
+
disabled = false,
|
|
772
|
+
hasSubmitted,
|
|
773
|
+
palette = "macaw",
|
|
774
|
+
isCorrect,
|
|
775
|
+
selectedChoicesCorrectness
|
|
776
|
+
}) {
|
|
777
|
+
const isMultiple = interaction.maxChoices > 1;
|
|
778
|
+
const selectedValues = React3.useMemo(() => {
|
|
779
|
+
if (!response) return [];
|
|
780
|
+
return Array.isArray(response) ? response : [response];
|
|
781
|
+
}, [response]);
|
|
782
|
+
const isImagesOnly = React3.useMemo(() => {
|
|
783
|
+
const hasImg = (html) => html.toLowerCase().includes("<img");
|
|
784
|
+
const stripped = (html) => html.replace(/<[^>]*>/g, "").trim();
|
|
785
|
+
return interaction.choices.length > 0 && interaction.choices.every((c) => hasImg(c.contentHtml) && stripped(c.contentHtml) === "");
|
|
786
|
+
}, [interaction.choices]);
|
|
787
|
+
const choiceCount = interaction.choices.length;
|
|
788
|
+
const gridClass = React3.useMemo(() => {
|
|
789
|
+
if (!isImagesOnly) return "space-y-2";
|
|
790
|
+
switch (choiceCount) {
|
|
791
|
+
case 4:
|
|
792
|
+
return "grid grid-cols-2 gap-4";
|
|
793
|
+
case 3:
|
|
794
|
+
return "grid grid-cols-3 gap-4";
|
|
795
|
+
case 2:
|
|
796
|
+
return "max-w-3xl mx-auto grid grid-cols-2 gap-6";
|
|
797
|
+
default:
|
|
798
|
+
return "grid grid-cols-2 md:grid-cols-3 gap-4";
|
|
799
|
+
}
|
|
800
|
+
}, [isImagesOnly, choiceCount]);
|
|
801
|
+
const imageCardClass = React3.useMemo(() => {
|
|
802
|
+
if (!isImagesOnly) return "";
|
|
803
|
+
return choiceCount === 2 ? "justify-center text-center [&_img]:max-h-52 md:[&_img]:max-h-60 ![&_img]:w-auto ![&_img]:h-auto ![&_img]:max-w-full ![&_img]:block [&_img]:mx-auto min-h-[220px] px-8 py-8" : "justify-center text-center [&_img]:max-h-40 md:[&_img]:max-h-48 ![&_img]:w-auto ![&_img]:h-auto ![&_img]:max-w-full ![&_img]:block [&_img]:mx-auto min-h-[180px]";
|
|
804
|
+
}, [isImagesOnly, choiceCount]);
|
|
805
|
+
const handleSingleChoice = (value) => {
|
|
806
|
+
if (!disabled && onAnswerSelect) {
|
|
807
|
+
onAnswerSelect(value);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
const promptElement = interaction.promptHtml ? /* @__PURE__ */ jsx(HTMLContent, { html: interaction.promptHtml, className: "mb-4" }) : null;
|
|
811
|
+
if (!isMultiple) {
|
|
812
|
+
const submitted2 = hasSubmitted === true;
|
|
813
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
814
|
+
promptElement,
|
|
815
|
+
/* @__PURE__ */ jsx(
|
|
816
|
+
RadioGroup,
|
|
817
|
+
{
|
|
818
|
+
value: selectedValues[0] || "",
|
|
819
|
+
onValueChange: (value) => handleSingleChoice(value),
|
|
820
|
+
disabled: disabled || submitted2,
|
|
821
|
+
children: /* @__PURE__ */ jsx(FieldGroup, { children: /* @__PURE__ */ jsx(FieldSet, { children: isImagesOnly ? /* @__PURE__ */ jsx("div", { className: gridClass, children: interaction.choices.map((choice, index) => {
|
|
822
|
+
const isSelected = selectedValues.includes(choice.identifier);
|
|
823
|
+
const choicePalette = submitted2 && isSelected ? isCorrect ? "owl" : "cardinal" : palette;
|
|
824
|
+
const domId = `${interaction.responseIdentifier}-${choice.identifier}`;
|
|
825
|
+
return /* @__PURE__ */ jsx(FieldLabel, { htmlFor: domId, palette: choicePalette, children: /* @__PURE__ */ jsxs(Field, { orientation: "horizontal", "data-disabled": disabled || submitted2, children: [
|
|
826
|
+
/* @__PURE__ */ jsx(
|
|
827
|
+
ChoiceIndicator,
|
|
828
|
+
{
|
|
829
|
+
value: choice.identifier,
|
|
830
|
+
id: domId,
|
|
831
|
+
letter: String.fromCharCode(65 + index),
|
|
832
|
+
palette: choicePalette,
|
|
833
|
+
disabled: disabled || submitted2
|
|
834
|
+
}
|
|
835
|
+
),
|
|
836
|
+
/* @__PURE__ */ jsx(FieldContent, { className: cn(imageCardClass), children: /* @__PURE__ */ jsx(FieldTitle, { children: /* @__PURE__ */ jsx(HTMLContent, { html: choice.contentHtml }) }) })
|
|
837
|
+
] }) }, choice.identifier);
|
|
838
|
+
}) }) : interaction.choices.map((choice, index) => {
|
|
839
|
+
const isSelected = selectedValues.includes(choice.identifier);
|
|
840
|
+
const choicePalette = submitted2 && isSelected ? isCorrect ? "owl" : "cardinal" : palette;
|
|
841
|
+
const domId = `${interaction.responseIdentifier}-${choice.identifier}`;
|
|
842
|
+
return /* @__PURE__ */ jsx(FieldLabel, { htmlFor: domId, palette: choicePalette, children: /* @__PURE__ */ jsxs(Field, { orientation: "horizontal", "data-disabled": disabled || submitted2, children: [
|
|
843
|
+
/* @__PURE__ */ jsx(
|
|
844
|
+
ChoiceIndicator,
|
|
845
|
+
{
|
|
846
|
+
value: choice.identifier,
|
|
847
|
+
id: domId,
|
|
848
|
+
"aria-describedby": submitted2 && isSelected && choice.inlineFeedbackHtml ? `${domId}-fb` : void 0,
|
|
849
|
+
letter: String.fromCharCode(65 + index),
|
|
850
|
+
palette: choicePalette,
|
|
851
|
+
disabled: disabled || submitted2
|
|
852
|
+
}
|
|
853
|
+
),
|
|
854
|
+
/* @__PURE__ */ jsxs(FieldContent, { children: [
|
|
855
|
+
/* @__PURE__ */ jsx(FieldTitle, { children: /* @__PURE__ */ jsx(HTMLContent, { html: choice.contentHtml }) }),
|
|
856
|
+
submitted2 && isSelected && choice.inlineFeedbackHtml ? /* @__PURE__ */ jsx(
|
|
857
|
+
"output",
|
|
858
|
+
{
|
|
859
|
+
id: `${domId}-fb`,
|
|
860
|
+
"aria-live": "polite",
|
|
861
|
+
className: cn(
|
|
862
|
+
"mt-2 pl-3 border-l-4 text-sm text-muted-foreground",
|
|
863
|
+
isCorrect ? "border-owl" : "border-cardinal"
|
|
864
|
+
),
|
|
865
|
+
children: /* @__PURE__ */ jsx(HTMLContent, { html: choice.inlineFeedbackHtml })
|
|
866
|
+
}
|
|
867
|
+
) : null
|
|
868
|
+
] })
|
|
869
|
+
] }) }, choice.identifier);
|
|
870
|
+
}) }) })
|
|
871
|
+
}
|
|
872
|
+
)
|
|
873
|
+
] });
|
|
874
|
+
}
|
|
875
|
+
const submitted = hasSubmitted === true;
|
|
876
|
+
const atMax = selectedValues.length >= interaction.maxChoices;
|
|
877
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
878
|
+
promptElement,
|
|
879
|
+
/* @__PURE__ */ jsx(FieldGroup, { children: /* @__PURE__ */ jsx(FieldSet, { children: isImagesOnly ? /* @__PURE__ */ jsx("div", { className: gridClass, children: interaction.choices.map((choice, _index) => {
|
|
880
|
+
const isChecked = selectedValues.includes(choice.identifier);
|
|
881
|
+
const perSelection = selectedChoicesCorrectness?.find((c) => c.id === choice.identifier)?.isCorrect;
|
|
882
|
+
const selectedCorrect = perSelection ?? (isChecked ? isCorrect ?? false : false);
|
|
883
|
+
const choicePalette = submitted && isChecked ? selectedCorrect ? "owl" : "cardinal" : palette;
|
|
884
|
+
const domId = `${interaction.responseIdentifier}-${choice.identifier}`;
|
|
885
|
+
return /* @__PURE__ */ jsx(FieldLabel, { htmlFor: domId, palette: choicePalette, children: /* @__PURE__ */ jsxs(Field, { orientation: "horizontal", "data-disabled": disabled || submitted, children: [
|
|
886
|
+
/* @__PURE__ */ jsx(
|
|
887
|
+
ChoiceIndicator,
|
|
888
|
+
{
|
|
889
|
+
type: "checkbox",
|
|
890
|
+
id: domId,
|
|
891
|
+
showLetter: false,
|
|
892
|
+
palette: choicePalette,
|
|
893
|
+
checked: isChecked,
|
|
894
|
+
onCheckedChange: (checked) => {
|
|
895
|
+
if (disabled || submitted || !onAnswerSelect) return;
|
|
896
|
+
if (checked === true && !isChecked && atMax) return;
|
|
897
|
+
const next = checked === true ? [...selectedValues, choice.identifier] : selectedValues.filter((v) => v !== choice.identifier);
|
|
898
|
+
onAnswerSelect(next);
|
|
899
|
+
},
|
|
900
|
+
disabled: disabled || submitted || !isChecked && atMax,
|
|
901
|
+
"aria-describedby": submitted && isChecked && choice.inlineFeedbackHtml ? `${domId}-fb` : void 0
|
|
902
|
+
}
|
|
903
|
+
),
|
|
904
|
+
/* @__PURE__ */ jsxs(FieldContent, { className: cn(imageCardClass), children: [
|
|
905
|
+
/* @__PURE__ */ jsx(FieldTitle, { children: /* @__PURE__ */ jsx(HTMLContent, { html: choice.contentHtml }) }),
|
|
906
|
+
submitted && isChecked && choice.inlineFeedbackHtml ? /* @__PURE__ */ jsx(
|
|
907
|
+
"output",
|
|
908
|
+
{
|
|
909
|
+
id: `${domId}-fb`,
|
|
910
|
+
"aria-live": "polite",
|
|
911
|
+
className: cn(
|
|
912
|
+
"mt-2 pl-3 border-l-4 text-sm text-muted-foreground",
|
|
913
|
+
selectedCorrect ? "border-owl" : "border-cardinal"
|
|
914
|
+
),
|
|
915
|
+
children: /* @__PURE__ */ jsx(HTMLContent, { html: choice.inlineFeedbackHtml })
|
|
916
|
+
}
|
|
917
|
+
) : null
|
|
918
|
+
] })
|
|
919
|
+
] }) }, choice.identifier);
|
|
920
|
+
}) }) : interaction.choices.map((choice, _index) => {
|
|
921
|
+
const isChecked = selectedValues.includes(choice.identifier);
|
|
922
|
+
const perSelection = selectedChoicesCorrectness?.find((c) => c.id === choice.identifier)?.isCorrect;
|
|
923
|
+
const selectedCorrect = perSelection ?? (isChecked ? isCorrect ?? false : false);
|
|
924
|
+
const choicePalette = submitted && isChecked ? selectedCorrect ? "owl" : "cardinal" : palette;
|
|
925
|
+
const domId = `${interaction.responseIdentifier}-${choice.identifier}`;
|
|
926
|
+
return /* @__PURE__ */ jsx(FieldLabel, { htmlFor: domId, palette: choicePalette, children: /* @__PURE__ */ jsxs(Field, { orientation: "horizontal", "data-disabled": disabled || submitted, children: [
|
|
927
|
+
/* @__PURE__ */ jsx(
|
|
928
|
+
ChoiceIndicator,
|
|
929
|
+
{
|
|
930
|
+
type: "checkbox",
|
|
931
|
+
id: domId,
|
|
932
|
+
showLetter: false,
|
|
933
|
+
palette: choicePalette,
|
|
934
|
+
checked: isChecked,
|
|
935
|
+
onCheckedChange: (checked) => {
|
|
936
|
+
if (disabled || submitted || !onAnswerSelect) return;
|
|
937
|
+
if (checked === true && !isChecked && atMax) return;
|
|
938
|
+
const next = checked === true ? [...selectedValues, choice.identifier] : selectedValues.filter((v) => v !== choice.identifier);
|
|
939
|
+
onAnswerSelect(next);
|
|
940
|
+
},
|
|
941
|
+
disabled: disabled || submitted || !isChecked && atMax,
|
|
942
|
+
"aria-describedby": submitted && isChecked && choice.inlineFeedbackHtml ? `${domId}-fb` : void 0
|
|
943
|
+
}
|
|
944
|
+
),
|
|
945
|
+
/* @__PURE__ */ jsxs(FieldContent, { children: [
|
|
946
|
+
/* @__PURE__ */ jsx(FieldTitle, { children: /* @__PURE__ */ jsx(HTMLContent, { html: choice.contentHtml }) }),
|
|
947
|
+
submitted && isChecked && choice.inlineFeedbackHtml ? /* @__PURE__ */ jsx(
|
|
948
|
+
"output",
|
|
949
|
+
{
|
|
950
|
+
id: `${domId}-fb`,
|
|
951
|
+
"aria-live": "polite",
|
|
952
|
+
className: cn(
|
|
953
|
+
"mt-2 pl-3 border-l-4 text-sm text-muted-foreground",
|
|
954
|
+
selectedCorrect ? "border-owl" : "border-cardinal"
|
|
955
|
+
),
|
|
956
|
+
children: /* @__PURE__ */ jsx(HTMLContent, { html: choice.inlineFeedbackHtml })
|
|
957
|
+
}
|
|
958
|
+
) : null
|
|
959
|
+
] })
|
|
960
|
+
] }) }, choice.identifier);
|
|
961
|
+
}) }) })
|
|
962
|
+
] });
|
|
963
|
+
}
|
|
964
|
+
function SafeInlineHTML({ html, className }) {
|
|
965
|
+
const ref = React3.useRef(null);
|
|
966
|
+
React3.useLayoutEffect(() => {
|
|
967
|
+
if (ref.current) {
|
|
968
|
+
ref.current.innerHTML = html;
|
|
969
|
+
}
|
|
970
|
+
}, [html]);
|
|
971
|
+
return /* @__PURE__ */ jsx("span", { ref, className });
|
|
972
|
+
}
|
|
973
|
+
function toQtiResponse(state) {
|
|
974
|
+
return Object.entries(state).map(([gapId, sourceId]) => `${sourceId} ${gapId}`);
|
|
975
|
+
}
|
|
976
|
+
function fromQtiResponse(response) {
|
|
977
|
+
const state = {};
|
|
978
|
+
for (const pair of response) {
|
|
979
|
+
const parts = pair.split(" ");
|
|
980
|
+
const sourceId = parts[0];
|
|
981
|
+
const gapId = parts[1];
|
|
982
|
+
if (sourceId && gapId) {
|
|
983
|
+
state[gapId] = sourceId;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return state;
|
|
987
|
+
}
|
|
988
|
+
var gapSlotVariants = cva(
|
|
989
|
+
[
|
|
990
|
+
"inline-flex items-center justify-center",
|
|
991
|
+
"min-w-[4rem] min-h-[1.75rem] px-2 mx-1 my-1",
|
|
992
|
+
"border rounded-xs align-middle",
|
|
993
|
+
"transition-all duration-150"
|
|
994
|
+
],
|
|
995
|
+
{
|
|
996
|
+
variants: {
|
|
997
|
+
state: {
|
|
998
|
+
empty: "border-muted-foreground/30 bg-muted/20",
|
|
999
|
+
hover: "border-macaw bg-macaw/10",
|
|
1000
|
+
filled: "border-macaw bg-macaw/10",
|
|
1001
|
+
correct: "border-owl bg-owl/15",
|
|
1002
|
+
incorrect: "border-cardinal bg-cardinal/15"
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
defaultVariants: {
|
|
1006
|
+
state: "empty"
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
);
|
|
1010
|
+
var sourceTokenVariants = cva(
|
|
1011
|
+
[
|
|
1012
|
+
"inline-flex items-center justify-center",
|
|
1013
|
+
"h-7 px-2 rounded-xs border-2",
|
|
1014
|
+
"text-sm font-semibold",
|
|
1015
|
+
"transition-all duration-150",
|
|
1016
|
+
"select-none"
|
|
1017
|
+
],
|
|
1018
|
+
{
|
|
1019
|
+
variants: {
|
|
1020
|
+
state: {
|
|
1021
|
+
idle: "bg-background border-border shadow-[0_2px_0_var(--color-border)] cursor-grab hover:border-macaw hover:shadow-[0_2px_0_var(--color-macaw)]",
|
|
1022
|
+
selected: "bg-macaw/15 border-macaw shadow-[0_2px_0_var(--color-whale)] ring-2 ring-macaw/30",
|
|
1023
|
+
dragging: "opacity-50 cursor-grabbing",
|
|
1024
|
+
disabled: "opacity-50 cursor-not-allowed bg-muted/50 border-muted shadow-none"
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
defaultVariants: {
|
|
1028
|
+
state: "idle"
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
1032
|
+
function SourceToken({ id, contentHtml, disabled, isSelected, onClick, remainingUses }) {
|
|
1033
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
1034
|
+
id: `source-${id}`,
|
|
1035
|
+
data: { type: "source", sourceId: id },
|
|
1036
|
+
disabled: disabled || remainingUses <= 0
|
|
1037
|
+
});
|
|
1038
|
+
if (remainingUses <= 0) return null;
|
|
1039
|
+
const getState = () => {
|
|
1040
|
+
if (disabled) return "disabled";
|
|
1041
|
+
if (isDragging) return "dragging";
|
|
1042
|
+
if (isSelected) return "selected";
|
|
1043
|
+
return "idle";
|
|
1044
|
+
};
|
|
1045
|
+
return /* @__PURE__ */ jsxs(
|
|
1046
|
+
"button",
|
|
1047
|
+
{
|
|
1048
|
+
ref: setNodeRef,
|
|
1049
|
+
type: "button",
|
|
1050
|
+
onClick: disabled ? void 0 : onClick,
|
|
1051
|
+
className: cn(sourceTokenVariants({ state: getState() }), "active:cursor-grabbing"),
|
|
1052
|
+
disabled,
|
|
1053
|
+
...attributes,
|
|
1054
|
+
...listeners,
|
|
1055
|
+
children: [
|
|
1056
|
+
/* @__PURE__ */ jsx(SafeInlineHTML, { html: contentHtml }),
|
|
1057
|
+
remainingUses > 1 && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs text-muted-foreground", children: [
|
|
1058
|
+
"\xD7",
|
|
1059
|
+
remainingUses
|
|
1060
|
+
] })
|
|
1061
|
+
]
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
function GapSlot({ gapId, filledWith, onClear, onClick, disabled, isCorrect, hasSubmitted }) {
|
|
1066
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
1067
|
+
id: `gap-${gapId}`,
|
|
1068
|
+
data: { type: "gap", gapId },
|
|
1069
|
+
disabled
|
|
1070
|
+
});
|
|
1071
|
+
const getState = () => {
|
|
1072
|
+
if (hasSubmitted && filledWith) {
|
|
1073
|
+
return isCorrect ? "correct" : "incorrect";
|
|
1074
|
+
}
|
|
1075
|
+
if (isOver) return "hover";
|
|
1076
|
+
if (filledWith) return "filled";
|
|
1077
|
+
return "empty";
|
|
1078
|
+
};
|
|
1079
|
+
return /* @__PURE__ */ jsx("span", { ref: setNodeRef, className: cn(gapSlotVariants({ state: getState() })), children: filledWith ? /* @__PURE__ */ jsxs(
|
|
1080
|
+
"button",
|
|
1081
|
+
{
|
|
1082
|
+
type: "button",
|
|
1083
|
+
onClick: disabled ? void 0 : onClear,
|
|
1084
|
+
disabled,
|
|
1085
|
+
className: cn(
|
|
1086
|
+
"flex items-center gap-1",
|
|
1087
|
+
!disabled && "hover:text-cardinal cursor-pointer",
|
|
1088
|
+
disabled && "cursor-default"
|
|
1089
|
+
),
|
|
1090
|
+
children: [
|
|
1091
|
+
/* @__PURE__ */ jsx(SafeInlineHTML, { html: filledWith.contentHtml }),
|
|
1092
|
+
!disabled && /* @__PURE__ */ jsx("span", { className: "text-xs opacity-60", children: "\xD7" })
|
|
1093
|
+
]
|
|
1094
|
+
}
|
|
1095
|
+
) : /* @__PURE__ */ jsx(
|
|
1096
|
+
"button",
|
|
1097
|
+
{
|
|
1098
|
+
type: "button",
|
|
1099
|
+
onClick: disabled ? void 0 : onClick,
|
|
1100
|
+
disabled,
|
|
1101
|
+
className: cn(
|
|
1102
|
+
"w-full h-full min-h-[1.5em] flex items-center justify-center",
|
|
1103
|
+
!disabled && "cursor-pointer hover:bg-macaw/5"
|
|
1104
|
+
),
|
|
1105
|
+
children: /* @__PURE__ */ jsx("span", { className: "opacity-30", children: "___" })
|
|
1106
|
+
}
|
|
1107
|
+
) });
|
|
1108
|
+
}
|
|
1109
|
+
function GapMatchInteraction({
|
|
1110
|
+
interaction,
|
|
1111
|
+
response = [],
|
|
1112
|
+
onAnswerSelect,
|
|
1113
|
+
disabled = false,
|
|
1114
|
+
hasSubmitted = false,
|
|
1115
|
+
gapCorrectness
|
|
1116
|
+
}) {
|
|
1117
|
+
const currentPairs = React3.useMemo(() => fromQtiResponse(response), [response]);
|
|
1118
|
+
const [selectedSourceId, setSelectedSourceId] = React3.useState(null);
|
|
1119
|
+
const [activeDragId, setActiveDragId] = React3.useState(null);
|
|
1120
|
+
const activeSource = React3.useMemo(
|
|
1121
|
+
() => interaction.gapTexts.find((gt) => gt.id === activeDragId),
|
|
1122
|
+
[activeDragId, interaction.gapTexts]
|
|
1123
|
+
);
|
|
1124
|
+
const getRemainingUses = (sourceId) => {
|
|
1125
|
+
const source = interaction.gapTexts.find((gt) => gt.id === sourceId);
|
|
1126
|
+
if (!source) return 0;
|
|
1127
|
+
if (source.matchMax === 0) return 999;
|
|
1128
|
+
const usedCount = Object.values(currentPairs).filter((id) => id === sourceId).length;
|
|
1129
|
+
return source.matchMax - usedCount;
|
|
1130
|
+
};
|
|
1131
|
+
const handleSourceClick = (sourceId) => {
|
|
1132
|
+
if (disabled || hasSubmitted) return;
|
|
1133
|
+
setSelectedSourceId((prev) => prev === sourceId ? null : sourceId);
|
|
1134
|
+
};
|
|
1135
|
+
const handleGapClick = (gapId) => {
|
|
1136
|
+
if (disabled || hasSubmitted || !selectedSourceId) return;
|
|
1137
|
+
if (getRemainingUses(selectedSourceId) <= 0) {
|
|
1138
|
+
setSelectedSourceId(null);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const newPairs = { ...currentPairs, [gapId]: selectedSourceId };
|
|
1142
|
+
onAnswerSelect?.(toQtiResponse(newPairs));
|
|
1143
|
+
setSelectedSourceId(null);
|
|
1144
|
+
};
|
|
1145
|
+
const handleClearGap = (gapId) => {
|
|
1146
|
+
if (disabled || hasSubmitted) return;
|
|
1147
|
+
const newPairs = { ...currentPairs };
|
|
1148
|
+
delete newPairs[gapId];
|
|
1149
|
+
onAnswerSelect?.(toQtiResponse(newPairs));
|
|
1150
|
+
};
|
|
1151
|
+
const handleDragEnd = (event) => {
|
|
1152
|
+
setActiveDragId(null);
|
|
1153
|
+
const { active, over } = event;
|
|
1154
|
+
if (!over) return;
|
|
1155
|
+
const sourceData = active.data.current;
|
|
1156
|
+
const targetData = over.data.current;
|
|
1157
|
+
if (sourceData?.type === "source" && targetData?.type === "gap") {
|
|
1158
|
+
const sourceId = sourceData.sourceId;
|
|
1159
|
+
const gapId = targetData.gapId;
|
|
1160
|
+
if (typeof sourceId === "string" && typeof gapId === "string") {
|
|
1161
|
+
if (getRemainingUses(sourceId) <= 0) return;
|
|
1162
|
+
const newPairs = { ...currentPairs, [gapId]: sourceId };
|
|
1163
|
+
onAnswerSelect?.(toQtiResponse(newPairs));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
const gapEmbeds = React3.useMemo(() => {
|
|
1168
|
+
const embeds = {};
|
|
1169
|
+
for (const gap of interaction.gaps) {
|
|
1170
|
+
embeds[gap.id] = gap;
|
|
1171
|
+
}
|
|
1172
|
+
return embeds;
|
|
1173
|
+
}, [interaction.gaps]);
|
|
1174
|
+
return /* @__PURE__ */ jsxs(
|
|
1175
|
+
DndContext,
|
|
1176
|
+
{
|
|
1177
|
+
collisionDetection: closestCenter,
|
|
1178
|
+
onDragStart: (e) => {
|
|
1179
|
+
const id = e.active.id;
|
|
1180
|
+
const sourceId = typeof id === "string" ? id.replace("source-", "") : null;
|
|
1181
|
+
setActiveDragId(sourceId);
|
|
1182
|
+
setSelectedSourceId(null);
|
|
1183
|
+
},
|
|
1184
|
+
onDragEnd: handleDragEnd,
|
|
1185
|
+
children: [
|
|
1186
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
1187
|
+
/* @__PURE__ */ jsxs(
|
|
1188
|
+
"div",
|
|
1189
|
+
{
|
|
1190
|
+
className: cn(
|
|
1191
|
+
"p-4 rounded-xl border-2 border-dashed",
|
|
1192
|
+
"flex flex-wrap gap-2 min-h-[3.5rem]",
|
|
1193
|
+
disabled || hasSubmitted ? "bg-muted/30 border-muted" : "bg-muted/10 border-muted-foreground/20"
|
|
1194
|
+
),
|
|
1195
|
+
children: [
|
|
1196
|
+
interaction.gapTexts.map((source) => /* @__PURE__ */ jsx(
|
|
1197
|
+
SourceToken,
|
|
1198
|
+
{
|
|
1199
|
+
id: source.id,
|
|
1200
|
+
contentHtml: source.contentHtml,
|
|
1201
|
+
disabled: disabled || hasSubmitted,
|
|
1202
|
+
isSelected: selectedSourceId === source.id,
|
|
1203
|
+
onClick: () => handleSourceClick(source.id),
|
|
1204
|
+
remainingUses: getRemainingUses(source.id)
|
|
1205
|
+
},
|
|
1206
|
+
source.id
|
|
1207
|
+
)),
|
|
1208
|
+
interaction.gapTexts.every((s) => getRemainingUses(s.id) <= 0)
|
|
1209
|
+
]
|
|
1210
|
+
}
|
|
1211
|
+
),
|
|
1212
|
+
selectedSourceId && !disabled && !hasSubmitted && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground animate-pulse", children: "Click a gap to place the selected token" }),
|
|
1213
|
+
/* @__PURE__ */ jsx("div", { className: "leading-relaxed text-lg", children: /* @__PURE__ */ jsx(
|
|
1214
|
+
HTMLContent,
|
|
1215
|
+
{
|
|
1216
|
+
html: interaction.contentHtml,
|
|
1217
|
+
gapEmbeds,
|
|
1218
|
+
renderGap: (gap) => {
|
|
1219
|
+
const sourceId = currentPairs[gap.id];
|
|
1220
|
+
const source = interaction.gapTexts.find((gt) => gt.id === sourceId);
|
|
1221
|
+
return /* @__PURE__ */ jsx(
|
|
1222
|
+
GapSlot,
|
|
1223
|
+
{
|
|
1224
|
+
gapId: gap.id,
|
|
1225
|
+
filledWith: source,
|
|
1226
|
+
onClear: () => handleClearGap(gap.id),
|
|
1227
|
+
onClick: () => handleGapClick(gap.id),
|
|
1228
|
+
disabled: disabled || hasSubmitted,
|
|
1229
|
+
isCorrect: gapCorrectness?.[gap.id],
|
|
1230
|
+
hasSubmitted
|
|
1231
|
+
}
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
) })
|
|
1236
|
+
] }),
|
|
1237
|
+
/* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: activeSource ? /* @__PURE__ */ jsx("div", { className: cn(sourceTokenVariants({ state: "selected" }), "shadow-xl cursor-grabbing"), children: /* @__PURE__ */ jsx(SafeInlineHTML, { html: activeSource.contentHtml }) }) : null })
|
|
1238
|
+
]
|
|
1239
|
+
}
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
var selectTriggerVariants = cva(
|
|
1243
|
+
[
|
|
1244
|
+
"cursor-pointer",
|
|
1245
|
+
"hover:brightness-90 dark:hover:brightness-75",
|
|
1246
|
+
"border-[var(--select-complement)] text-[var(--select-foreground)] bg-[var(--select-background)] inline-flex items-center justify-between gap-2 align-baseline rounded-xs px-3 py-2 text-sm whitespace-nowrap outline-none",
|
|
1247
|
+
"data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
|
|
1248
|
+
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
1249
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
1250
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
|
1251
|
+
],
|
|
1252
|
+
{
|
|
1253
|
+
variants: {
|
|
1254
|
+
palette: {
|
|
1255
|
+
default: [
|
|
1256
|
+
"[--select-background:var(--color-background)]",
|
|
1257
|
+
"[--select-foreground:var(--color-foreground)]",
|
|
1258
|
+
"[--select-complement:var(--color-accent)]"
|
|
1259
|
+
],
|
|
1260
|
+
betta: [
|
|
1261
|
+
"[--select-background:var(--color-background)]",
|
|
1262
|
+
"[--select-foreground:var(--color-betta)]",
|
|
1263
|
+
"[--select-complement:var(--color-butterfly)]"
|
|
1264
|
+
],
|
|
1265
|
+
cardinal: [
|
|
1266
|
+
"[--select-background:var(--color-background)]",
|
|
1267
|
+
"[--select-foreground:var(--color-cardinal)]",
|
|
1268
|
+
"[--select-complement:var(--color-fire-ant)]"
|
|
1269
|
+
],
|
|
1270
|
+
bee: [
|
|
1271
|
+
"[--select-background:var(--color-background)]",
|
|
1272
|
+
"[--select-foreground:var(--color-bee)]",
|
|
1273
|
+
"[--select-complement:var(--color-lion)]"
|
|
1274
|
+
],
|
|
1275
|
+
owl: [
|
|
1276
|
+
"[--select-background:var(--color-background)]",
|
|
1277
|
+
"[--select-foreground:var(--color-owl)]",
|
|
1278
|
+
"[--select-complement:var(--color-tree-frog)]"
|
|
1279
|
+
],
|
|
1280
|
+
macaw: [
|
|
1281
|
+
"[--select-background:var(--color-background)]",
|
|
1282
|
+
"[--select-foreground:var(--color-macaw)]",
|
|
1283
|
+
"[--select-complement:var(--color-whale)]"
|
|
1284
|
+
]
|
|
1285
|
+
},
|
|
1286
|
+
outline: {
|
|
1287
|
+
true: ["border-2 shadow-[0_2px_0_var(--select-complement)]"],
|
|
1288
|
+
false: ["border-none shadow-[0_3px_0_var(--select-complement)]"]
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
defaultVariants: {
|
|
1292
|
+
palette: "default",
|
|
1293
|
+
outline: false
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
);
|
|
1297
|
+
function Select({ ...props }) {
|
|
1298
|
+
return /* @__PURE__ */ jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
|
|
1299
|
+
}
|
|
1300
|
+
function SelectValue({
|
|
1301
|
+
className,
|
|
1302
|
+
...props
|
|
1303
|
+
}) {
|
|
1304
|
+
return /* @__PURE__ */ jsx(
|
|
1305
|
+
SelectPrimitive.Value,
|
|
1306
|
+
{
|
|
1307
|
+
"data-slot": "select-value",
|
|
1308
|
+
className: cn("text-[var(--select-foreground)]", className),
|
|
1309
|
+
...props
|
|
1310
|
+
}
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
function SelectTrigger({
|
|
1314
|
+
className,
|
|
1315
|
+
size = "default",
|
|
1316
|
+
palette = "default",
|
|
1317
|
+
children,
|
|
1318
|
+
outline = true,
|
|
1319
|
+
style,
|
|
1320
|
+
...props
|
|
1321
|
+
}) {
|
|
1322
|
+
return /* @__PURE__ */ jsxs(
|
|
1323
|
+
SelectPrimitive.Trigger,
|
|
1324
|
+
{
|
|
1325
|
+
"data-slot": "select-trigger",
|
|
1326
|
+
"data-size": size,
|
|
1327
|
+
"data-palette": palette,
|
|
1328
|
+
className: cn(selectTriggerVariants({ palette, outline, className })),
|
|
1329
|
+
style: { color: "var(--select-foreground)", ...style },
|
|
1330
|
+
...props,
|
|
1331
|
+
children: [
|
|
1332
|
+
children,
|
|
1333
|
+
/* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4 opacity-50" }) })
|
|
1334
|
+
]
|
|
1335
|
+
}
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
function SelectContent({
|
|
1339
|
+
className,
|
|
1340
|
+
children,
|
|
1341
|
+
position = "popper",
|
|
1342
|
+
align = "center",
|
|
1343
|
+
...props
|
|
1344
|
+
}) {
|
|
1345
|
+
return /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
|
|
1346
|
+
SelectPrimitive.Content,
|
|
1347
|
+
{
|
|
1348
|
+
"data-slot": "select-content",
|
|
1349
|
+
className: cn(
|
|
1350
|
+
"bg-popover text-popover-foreground 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xs border border-accent shadow-[0_2px_0_var(--color-accent)]",
|
|
1351
|
+
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",
|
|
1352
|
+
className
|
|
1353
|
+
),
|
|
1354
|
+
position,
|
|
1355
|
+
align,
|
|
1356
|
+
...props,
|
|
1357
|
+
children: [
|
|
1358
|
+
/* @__PURE__ */ jsx(SelectScrollUpButton, {}),
|
|
1359
|
+
/* @__PURE__ */ jsx(
|
|
1360
|
+
SelectPrimitive.Viewport,
|
|
1361
|
+
{
|
|
1362
|
+
className: cn(
|
|
1363
|
+
"p-1",
|
|
1364
|
+
position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
1365
|
+
),
|
|
1366
|
+
children
|
|
1367
|
+
}
|
|
1368
|
+
),
|
|
1369
|
+
/* @__PURE__ */ jsx(SelectScrollDownButton, {})
|
|
1370
|
+
]
|
|
1371
|
+
}
|
|
1372
|
+
) });
|
|
1373
|
+
}
|
|
1374
|
+
function SelectItem({
|
|
1375
|
+
className,
|
|
1376
|
+
children,
|
|
1377
|
+
...props
|
|
1378
|
+
}) {
|
|
1379
|
+
return /* @__PURE__ */ jsxs(
|
|
1380
|
+
SelectPrimitive.Item,
|
|
1381
|
+
{
|
|
1382
|
+
"data-slot": "select-item",
|
|
1383
|
+
className: cn(
|
|
1384
|
+
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-xs py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
1385
|
+
className
|
|
1386
|
+
),
|
|
1387
|
+
...props,
|
|
1388
|
+
children: [
|
|
1389
|
+
/* @__PURE__ */ jsx("span", { className: "absolute right-2 flex size-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-4" }) }) }),
|
|
1390
|
+
/* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
|
|
1391
|
+
]
|
|
1392
|
+
}
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
function SelectScrollUpButton({
|
|
1396
|
+
className,
|
|
1397
|
+
...props
|
|
1398
|
+
}) {
|
|
1399
|
+
return /* @__PURE__ */ jsx(
|
|
1400
|
+
SelectPrimitive.ScrollUpButton,
|
|
1401
|
+
{
|
|
1402
|
+
"data-slot": "select-scroll-up-button",
|
|
1403
|
+
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
1404
|
+
...props,
|
|
1405
|
+
children: /* @__PURE__ */ jsx(ChevronUpIcon, { className: "size-4" })
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
function SelectScrollDownButton({
|
|
1410
|
+
className,
|
|
1411
|
+
...props
|
|
1412
|
+
}) {
|
|
1413
|
+
return /* @__PURE__ */ jsx(
|
|
1414
|
+
SelectPrimitive.ScrollDownButton,
|
|
1415
|
+
{
|
|
1416
|
+
"data-slot": "select-scroll-down-button",
|
|
1417
|
+
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
1418
|
+
...props,
|
|
1419
|
+
children: /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4" })
|
|
1420
|
+
}
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/shared/shuffle.ts
|
|
1425
|
+
function xmur3(str) {
|
|
1426
|
+
let h = 1779033703 ^ str.length;
|
|
1427
|
+
for (let i = 0; i < str.length; i++) {
|
|
1428
|
+
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
|
1429
|
+
h = h << 13 | h >>> 19;
|
|
1430
|
+
}
|
|
1431
|
+
return () => {
|
|
1432
|
+
h = Math.imul(h ^ h >>> 16, 2246822507);
|
|
1433
|
+
h = Math.imul(h ^ h >>> 13, 3266489909);
|
|
1434
|
+
const shifted = h >>> 16;
|
|
1435
|
+
h = h ^ shifted;
|
|
1436
|
+
return h >>> 0;
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function mulberry32(seed) {
|
|
1440
|
+
let state = seed;
|
|
1441
|
+
return () => {
|
|
1442
|
+
state = state + 1831565813;
|
|
1443
|
+
let t = state;
|
|
1444
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1445
|
+
t = t ^ t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1446
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function shuffleWithSeed(items, seed) {
|
|
1450
|
+
const out = items.slice();
|
|
1451
|
+
const seedFn = xmur3(seed);
|
|
1452
|
+
const rng = mulberry32(seedFn());
|
|
1453
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
1454
|
+
const j2 = Math.floor(rng() * (i + 1));
|
|
1455
|
+
const vi = out[i];
|
|
1456
|
+
const vj = out[j2];
|
|
1457
|
+
if (vi === void 0 || vj === void 0) continue;
|
|
1458
|
+
out[i] = vj;
|
|
1459
|
+
out[j2] = vi;
|
|
1460
|
+
}
|
|
1461
|
+
return out;
|
|
1462
|
+
}
|
|
1463
|
+
function SafeInlineHTML2({ html, className, style }) {
|
|
1464
|
+
const ref = React3.useRef(null);
|
|
1465
|
+
React3.useEffect(() => {
|
|
1466
|
+
if (ref.current) {
|
|
1467
|
+
ref.current.innerHTML = sanitizeForDisplay(html);
|
|
1468
|
+
}
|
|
1469
|
+
}, [html]);
|
|
1470
|
+
return /* @__PURE__ */ jsx("span", { ref, className, style });
|
|
1471
|
+
}
|
|
1472
|
+
function InlineInteraction({
|
|
1473
|
+
embed,
|
|
1474
|
+
response,
|
|
1475
|
+
onAnswerSelect,
|
|
1476
|
+
disabled = false,
|
|
1477
|
+
hasSubmitted,
|
|
1478
|
+
palette = "macaw",
|
|
1479
|
+
isCorrect
|
|
1480
|
+
}) {
|
|
1481
|
+
const hasValue = typeof response === "string" && response.length > 0;
|
|
1482
|
+
const neutralTriggerClass = hasValue ? "" : "border-border shadow-[0_3px_0_var(--color-border)] focus-visible:ring-0";
|
|
1483
|
+
const items = React3.useMemo(() => {
|
|
1484
|
+
return embed.shuffle ? shuffleWithSeed(embed.choices, embed.responseId) : embed.choices;
|
|
1485
|
+
}, [embed]);
|
|
1486
|
+
const feedbackPalette = hasSubmitted && hasValue ? isCorrect ? "owl" : "cardinal" : palette;
|
|
1487
|
+
const ariaInvalid = hasSubmitted && hasValue && isCorrect === false ? true : void 0;
|
|
1488
|
+
const selectedLabelHtml = React3.useMemo(() => {
|
|
1489
|
+
if (!hasValue) return "";
|
|
1490
|
+
const found = items.find((c) => c.id === response);
|
|
1491
|
+
return found?.contentHtml ?? "";
|
|
1492
|
+
}, [hasValue, items, response]);
|
|
1493
|
+
return /* @__PURE__ */ jsxs(
|
|
1494
|
+
Select,
|
|
1495
|
+
{
|
|
1496
|
+
value: hasValue ? response : "",
|
|
1497
|
+
onValueChange: (value) => {
|
|
1498
|
+
if (!disabled && onAnswerSelect) onAnswerSelect(value);
|
|
1499
|
+
},
|
|
1500
|
+
disabled: disabled || hasSubmitted,
|
|
1501
|
+
children: [
|
|
1502
|
+
/* @__PURE__ */ jsx(
|
|
1503
|
+
SelectTrigger,
|
|
1504
|
+
{
|
|
1505
|
+
size: "sm",
|
|
1506
|
+
className: neutralTriggerClass,
|
|
1507
|
+
palette: hasValue ? feedbackPalette : "default",
|
|
1508
|
+
"aria-invalid": ariaInvalid,
|
|
1509
|
+
children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select...", children: hasValue ? /* @__PURE__ */ jsx(
|
|
1510
|
+
SafeInlineHTML2,
|
|
1511
|
+
{
|
|
1512
|
+
html: selectedLabelHtml,
|
|
1513
|
+
className: "text-[var(--select-foreground)]",
|
|
1514
|
+
style: { color: "var(--select-foreground)" }
|
|
1515
|
+
}
|
|
1516
|
+
) : null })
|
|
1517
|
+
}
|
|
1518
|
+
),
|
|
1519
|
+
/* @__PURE__ */ jsx(SelectContent, { position: "item-aligned", children: items.map((c) => /* @__PURE__ */ jsx(SelectItem, { value: c.id, children: /* @__PURE__ */ jsx(SafeInlineHTML2, { html: c.contentHtml }) }, c.id)) })
|
|
1520
|
+
]
|
|
1521
|
+
}
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
function SafeInlineHTML3({ html, className }) {
|
|
1525
|
+
const ref = React3.useRef(null);
|
|
1526
|
+
React3.useLayoutEffect(() => {
|
|
1527
|
+
if (ref.current) {
|
|
1528
|
+
ref.current.innerHTML = html;
|
|
1529
|
+
}
|
|
1530
|
+
}, [html]);
|
|
1531
|
+
return /* @__PURE__ */ jsx("span", { ref, className });
|
|
1532
|
+
}
|
|
1533
|
+
function createPairKey(sourceId, targetId) {
|
|
1534
|
+
return `${sourceId} ${targetId}`;
|
|
1535
|
+
}
|
|
1536
|
+
function toQtiResponse2(state) {
|
|
1537
|
+
const pairs = [];
|
|
1538
|
+
for (const [targetId, sourceIds] of Object.entries(state)) {
|
|
1539
|
+
for (const sourceId of sourceIds) {
|
|
1540
|
+
pairs.push(createPairKey(sourceId, targetId));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return pairs;
|
|
1544
|
+
}
|
|
1545
|
+
function fromQtiResponse2(response) {
|
|
1546
|
+
const state = {};
|
|
1547
|
+
for (const pair of response) {
|
|
1548
|
+
const parts = pair.split(" ");
|
|
1549
|
+
const sourceId = parts[0];
|
|
1550
|
+
const targetId = parts[1];
|
|
1551
|
+
if (sourceId && targetId) {
|
|
1552
|
+
if (!state[targetId]) {
|
|
1553
|
+
state[targetId] = [];
|
|
1554
|
+
}
|
|
1555
|
+
state[targetId].push(sourceId);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return state;
|
|
1559
|
+
}
|
|
1560
|
+
var targetBoxVariants = cva(
|
|
1561
|
+
["flex flex-col gap-2 p-3 min-h-[8rem] min-w-[10rem]", "border-2 rounded-xs", "transition-all duration-150"],
|
|
1562
|
+
{
|
|
1563
|
+
variants: {
|
|
1564
|
+
state: {
|
|
1565
|
+
empty: "border-border border-dashed bg-muted/10",
|
|
1566
|
+
hover: "border-macaw border-solid bg-macaw/10 shadow-[0_2px_0_var(--color-macaw)]",
|
|
1567
|
+
filled: "border-macaw border-solid bg-macaw/5 shadow-[0_2px_0_var(--color-whale)]",
|
|
1568
|
+
disabled: "opacity-60 bg-muted/30 border-dashed"
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
defaultVariants: {
|
|
1572
|
+
state: "empty"
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
);
|
|
1576
|
+
var sourceTokenVariants2 = cva(
|
|
1577
|
+
[
|
|
1578
|
+
"inline-flex items-center justify-center",
|
|
1579
|
+
"h-7 px-2 rounded-xs border-2",
|
|
1580
|
+
"text-sm font-semibold",
|
|
1581
|
+
"transition-all duration-150",
|
|
1582
|
+
"select-none"
|
|
1583
|
+
],
|
|
1584
|
+
{
|
|
1585
|
+
variants: {
|
|
1586
|
+
state: {
|
|
1587
|
+
idle: "bg-background border-border shadow-[0_2px_0_var(--color-border)] cursor-grab hover:border-macaw hover:shadow-[0_2px_0_var(--color-macaw)]",
|
|
1588
|
+
dragging: "opacity-50 cursor-grabbing",
|
|
1589
|
+
placed: "bg-macaw/10 border-macaw shadow-[0_2px_0_var(--color-whale)]",
|
|
1590
|
+
correct: "bg-owl/15 border-owl shadow-[0_2px_0_var(--color-tree-frog)]",
|
|
1591
|
+
incorrect: "bg-cardinal/15 border-cardinal shadow-[0_2px_0_var(--color-fire-ant)]",
|
|
1592
|
+
disabled: "opacity-50 cursor-not-allowed bg-muted/50 border-muted shadow-none"
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
defaultVariants: {
|
|
1596
|
+
state: "idle"
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
);
|
|
1600
|
+
function PlacedToken({
|
|
1601
|
+
instanceId,
|
|
1602
|
+
sourceId,
|
|
1603
|
+
fromTargetId,
|
|
1604
|
+
fromIndex,
|
|
1605
|
+
contentHtml,
|
|
1606
|
+
disabled,
|
|
1607
|
+
state,
|
|
1608
|
+
onRemove
|
|
1609
|
+
}) {
|
|
1610
|
+
const canInteract = !disabled && state !== "correct" && state !== "incorrect";
|
|
1611
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
1612
|
+
id: instanceId,
|
|
1613
|
+
data: { type: "placed", sourceId, fromTargetId, fromIndex },
|
|
1614
|
+
disabled: !canInteract
|
|
1615
|
+
});
|
|
1616
|
+
const tokenState = isDragging ? "dragging" : state;
|
|
1617
|
+
return /* @__PURE__ */ jsxs(
|
|
1618
|
+
"button",
|
|
1619
|
+
{
|
|
1620
|
+
type: "button",
|
|
1621
|
+
ref: setNodeRef,
|
|
1622
|
+
...attributes,
|
|
1623
|
+
...listeners,
|
|
1624
|
+
onClick: canInteract ? onRemove : void 0,
|
|
1625
|
+
"aria-disabled": !canInteract,
|
|
1626
|
+
className: cn(
|
|
1627
|
+
sourceTokenVariants2({ state: tokenState }),
|
|
1628
|
+
canInteract && "cursor-grab hover:bg-muted/50 group",
|
|
1629
|
+
!canInteract && "cursor-default pointer-events-none"
|
|
1630
|
+
),
|
|
1631
|
+
children: [
|
|
1632
|
+
/* @__PURE__ */ jsx(SafeInlineHTML3, { html: contentHtml }),
|
|
1633
|
+
canInteract && /* @__PURE__ */ jsx("span", { className: "text-xs opacity-40 group-hover:opacity-100 ml-1 transition-opacity", children: "\xD7" })
|
|
1634
|
+
]
|
|
1635
|
+
}
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
function TargetBox({ target, placedSources, disabled, onRemoveSource, pairCorrectness, hasSubmitted }) {
|
|
1639
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
1640
|
+
id: `target-${target.id}`,
|
|
1641
|
+
data: { type: "target", targetId: target.id },
|
|
1642
|
+
disabled
|
|
1643
|
+
});
|
|
1644
|
+
const hasSources = placedSources.length > 0;
|
|
1645
|
+
const getState = () => {
|
|
1646
|
+
if (disabled) return "disabled";
|
|
1647
|
+
if (isOver) return "hover";
|
|
1648
|
+
if (hasSources) return "filled";
|
|
1649
|
+
return "empty";
|
|
1650
|
+
};
|
|
1651
|
+
const getTokenState = (sourceId) => {
|
|
1652
|
+
if (hasSubmitted) {
|
|
1653
|
+
const pairKey = createPairKey(sourceId, target.id);
|
|
1654
|
+
const isCorrect = pairCorrectness?.[pairKey];
|
|
1655
|
+
if (isCorrect === true) return "correct";
|
|
1656
|
+
if (isCorrect === false) return "incorrect";
|
|
1657
|
+
}
|
|
1658
|
+
if (disabled) return "disabled";
|
|
1659
|
+
return "placed";
|
|
1660
|
+
};
|
|
1661
|
+
return /* @__PURE__ */ jsxs("div", { ref: setNodeRef, className: cn(targetBoxVariants({ state: getState() })), children: [
|
|
1662
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-center pb-2 border-b border-border/50 mb-2", children: /* @__PURE__ */ jsx(SafeInlineHTML3, { html: target.contentHtml }) }),
|
|
1663
|
+
placedSources.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground/50 text-sm", children: "Drop here" }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: placedSources.map(({ instanceId, source, index }) => /* @__PURE__ */ jsx(
|
|
1664
|
+
PlacedToken,
|
|
1665
|
+
{
|
|
1666
|
+
instanceId,
|
|
1667
|
+
sourceId: source.id,
|
|
1668
|
+
fromTargetId: target.id,
|
|
1669
|
+
fromIndex: index,
|
|
1670
|
+
contentHtml: source.contentHtml,
|
|
1671
|
+
disabled: disabled || hasSubmitted,
|
|
1672
|
+
state: getTokenState(source.id),
|
|
1673
|
+
onRemove: () => onRemoveSource(instanceId)
|
|
1674
|
+
},
|
|
1675
|
+
instanceId
|
|
1676
|
+
)) })
|
|
1677
|
+
] });
|
|
1678
|
+
}
|
|
1679
|
+
function SourcePoolToken({ source, remainingUses, disabled }) {
|
|
1680
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
1681
|
+
id: `pool-${source.id}`,
|
|
1682
|
+
data: { type: "source", sourceId: source.id },
|
|
1683
|
+
disabled: disabled || remainingUses <= 0
|
|
1684
|
+
});
|
|
1685
|
+
if (remainingUses <= 0) return null;
|
|
1686
|
+
const getState = () => {
|
|
1687
|
+
if (isDragging) return "dragging";
|
|
1688
|
+
if (disabled) return "disabled";
|
|
1689
|
+
return "idle";
|
|
1690
|
+
};
|
|
1691
|
+
return /* @__PURE__ */ jsxs("div", { ref: setNodeRef, className: cn(sourceTokenVariants2({ state: getState() })), ...attributes, ...listeners, children: [
|
|
1692
|
+
/* @__PURE__ */ jsx(SafeInlineHTML3, { html: source.contentHtml }),
|
|
1693
|
+
remainingUses > 1 && /* @__PURE__ */ jsxs("span", { className: "ml-2 text-xs text-muted-foreground", children: [
|
|
1694
|
+
"\xD7",
|
|
1695
|
+
remainingUses
|
|
1696
|
+
] })
|
|
1697
|
+
] });
|
|
1698
|
+
}
|
|
1699
|
+
function DroppablePool({ disabled, children }) {
|
|
1700
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
1701
|
+
id: "source-pool",
|
|
1702
|
+
data: { type: "pool" },
|
|
1703
|
+
disabled
|
|
1704
|
+
});
|
|
1705
|
+
return /* @__PURE__ */ jsx(
|
|
1706
|
+
"div",
|
|
1707
|
+
{
|
|
1708
|
+
ref: setNodeRef,
|
|
1709
|
+
className: cn(
|
|
1710
|
+
"p-4 rounded-xl border-2 border-dashed",
|
|
1711
|
+
"flex flex-wrap justify-center gap-3 min-h-[4rem]",
|
|
1712
|
+
"transition-all duration-150",
|
|
1713
|
+
disabled ? "bg-muted/30 border-muted" : "bg-muted/10 border-muted-foreground/20",
|
|
1714
|
+
isOver && !disabled && "border-macaw bg-macaw/10"
|
|
1715
|
+
),
|
|
1716
|
+
children
|
|
1717
|
+
}
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
function MatchInteraction({
|
|
1721
|
+
interaction,
|
|
1722
|
+
response = [],
|
|
1723
|
+
onAnswerSelect,
|
|
1724
|
+
disabled = false,
|
|
1725
|
+
hasSubmitted = false,
|
|
1726
|
+
pairCorrectness
|
|
1727
|
+
}) {
|
|
1728
|
+
const sensors = useSensors(
|
|
1729
|
+
useSensor(PointerSensor, {
|
|
1730
|
+
activationConstraint: {
|
|
1731
|
+
distance: 5
|
|
1732
|
+
}
|
|
1733
|
+
})
|
|
1734
|
+
);
|
|
1735
|
+
const currentState = React3.useMemo(() => fromQtiResponse2(response), [response]);
|
|
1736
|
+
const [activeDragId, setActiveDragId] = React3.useState(null);
|
|
1737
|
+
const activeSource = React3.useMemo(() => {
|
|
1738
|
+
if (!activeDragId) return null;
|
|
1739
|
+
const sourceId = activeDragId.startsWith("pool-") ? activeDragId.replace("pool-", "") : activeDragId.split("-")[0];
|
|
1740
|
+
return interaction.sourceChoices.find((s) => s.id === sourceId);
|
|
1741
|
+
}, [activeDragId, interaction.sourceChoices]);
|
|
1742
|
+
const getSourceUsageCount = (sourceId) => {
|
|
1743
|
+
let count = 0;
|
|
1744
|
+
for (const sources of Object.values(currentState)) {
|
|
1745
|
+
count += sources.filter((id) => id === sourceId).length;
|
|
1746
|
+
}
|
|
1747
|
+
return count;
|
|
1748
|
+
};
|
|
1749
|
+
const getRemainingUses = (source) => {
|
|
1750
|
+
if (source.matchMax === 0) return 999;
|
|
1751
|
+
return source.matchMax - getSourceUsageCount(source.id);
|
|
1752
|
+
};
|
|
1753
|
+
const canTargetAcceptMore = (target) => {
|
|
1754
|
+
const placed = currentState[target.id]?.length ?? 0;
|
|
1755
|
+
if (target.matchMax === 0) return true;
|
|
1756
|
+
return placed < target.matchMax;
|
|
1757
|
+
};
|
|
1758
|
+
const handleDragEnd = (event) => {
|
|
1759
|
+
setActiveDragId(null);
|
|
1760
|
+
const { active, over } = event;
|
|
1761
|
+
if (!over) return;
|
|
1762
|
+
const activeData = active.data.current;
|
|
1763
|
+
const overData = over.data.current;
|
|
1764
|
+
if (overData?.type === "pool" && activeData?.type === "placed") {
|
|
1765
|
+
const fromTargetId = activeData.fromTargetId;
|
|
1766
|
+
const fromIndex = activeData.fromIndex;
|
|
1767
|
+
if (typeof fromTargetId !== "string" || typeof fromIndex !== "number") return;
|
|
1768
|
+
const newState = { ...currentState };
|
|
1769
|
+
const oldSources = newState[fromTargetId];
|
|
1770
|
+
if (oldSources) {
|
|
1771
|
+
newState[fromTargetId] = oldSources.filter((_2, i) => i !== fromIndex);
|
|
1772
|
+
if (newState[fromTargetId].length === 0) {
|
|
1773
|
+
delete newState[fromTargetId];
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
onAnswerSelect?.(toQtiResponse2(newState));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (overData?.type !== "target") return;
|
|
1780
|
+
const toTargetId = overData.targetId;
|
|
1781
|
+
if (typeof toTargetId !== "string") return;
|
|
1782
|
+
const toTarget = interaction.targetChoices.find((t) => t.id === toTargetId);
|
|
1783
|
+
if (!toTarget) return;
|
|
1784
|
+
if (activeData?.type === "source") {
|
|
1785
|
+
const sourceId = activeData.sourceId;
|
|
1786
|
+
if (typeof sourceId !== "string") return;
|
|
1787
|
+
const source = interaction.sourceChoices.find((s) => s.id === sourceId);
|
|
1788
|
+
if (!source) return;
|
|
1789
|
+
if (getRemainingUses(source) <= 0) return;
|
|
1790
|
+
if (!canTargetAcceptMore(toTarget)) return;
|
|
1791
|
+
const newState = { ...currentState };
|
|
1792
|
+
if (!newState[toTargetId]) {
|
|
1793
|
+
newState[toTargetId] = [];
|
|
1794
|
+
}
|
|
1795
|
+
newState[toTargetId] = [...newState[toTargetId], sourceId];
|
|
1796
|
+
onAnswerSelect?.(toQtiResponse2(newState));
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (activeData?.type === "placed") {
|
|
1800
|
+
const sourceId = activeData.sourceId;
|
|
1801
|
+
const fromTargetId = activeData.fromTargetId;
|
|
1802
|
+
const fromIndex = activeData.fromIndex;
|
|
1803
|
+
if (typeof sourceId !== "string" || typeof fromTargetId !== "string" || typeof fromIndex !== "number") return;
|
|
1804
|
+
if (fromTargetId === toTargetId) return;
|
|
1805
|
+
if (!canTargetAcceptMore(toTarget)) return;
|
|
1806
|
+
const newState = { ...currentState };
|
|
1807
|
+
const oldSources = newState[fromTargetId];
|
|
1808
|
+
if (oldSources) {
|
|
1809
|
+
newState[fromTargetId] = oldSources.filter((_2, i) => i !== fromIndex);
|
|
1810
|
+
if (newState[fromTargetId].length === 0) {
|
|
1811
|
+
delete newState[fromTargetId];
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (!newState[toTargetId]) {
|
|
1815
|
+
newState[toTargetId] = [];
|
|
1816
|
+
}
|
|
1817
|
+
newState[toTargetId] = [...newState[toTargetId], sourceId];
|
|
1818
|
+
onAnswerSelect?.(toQtiResponse2(newState));
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const handleRemoveSource = (targetId, index) => {
|
|
1822
|
+
if (disabled || hasSubmitted) return;
|
|
1823
|
+
const newState = { ...currentState };
|
|
1824
|
+
const sources = newState[targetId];
|
|
1825
|
+
if (sources) {
|
|
1826
|
+
newState[targetId] = sources.filter((_2, i) => i !== index);
|
|
1827
|
+
if (newState[targetId].length === 0) {
|
|
1828
|
+
delete newState[targetId];
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
onAnswerSelect?.(toQtiResponse2(newState));
|
|
1832
|
+
};
|
|
1833
|
+
const getPlacedSources = (targetId) => {
|
|
1834
|
+
const sourceIds = currentState[targetId] ?? [];
|
|
1835
|
+
return sourceIds.map((sourceId, index) => {
|
|
1836
|
+
const source = interaction.sourceChoices.find((s) => s.id === sourceId);
|
|
1837
|
+
return {
|
|
1838
|
+
instanceId: `${sourceId}-${targetId}-${index}`,
|
|
1839
|
+
source: source ?? { id: sourceId, contentHtml: sourceId, matchMax: 1 },
|
|
1840
|
+
index
|
|
1841
|
+
};
|
|
1842
|
+
});
|
|
1843
|
+
};
|
|
1844
|
+
return /* @__PURE__ */ jsxs(
|
|
1845
|
+
DndContext,
|
|
1846
|
+
{
|
|
1847
|
+
sensors,
|
|
1848
|
+
collisionDetection: closestCenter,
|
|
1849
|
+
onDragStart: (e) => {
|
|
1850
|
+
setActiveDragId(String(e.active.id));
|
|
1851
|
+
},
|
|
1852
|
+
onDragEnd: handleDragEnd,
|
|
1853
|
+
children: [
|
|
1854
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
1855
|
+
interaction.promptHtml && /* @__PURE__ */ jsx("div", { className: "text-center mb-4", children: /* @__PURE__ */ jsx(SafeInlineHTML3, { html: interaction.promptHtml }) }),
|
|
1856
|
+
!hasSubmitted && /* @__PURE__ */ jsxs(DroppablePool, { disabled, children: [
|
|
1857
|
+
interaction.sourceChoices.map((source) => /* @__PURE__ */ jsx(
|
|
1858
|
+
SourcePoolToken,
|
|
1859
|
+
{
|
|
1860
|
+
source,
|
|
1861
|
+
remainingUses: getRemainingUses(source),
|
|
1862
|
+
disabled
|
|
1863
|
+
},
|
|
1864
|
+
source.id
|
|
1865
|
+
)),
|
|
1866
|
+
interaction.sourceChoices.every((s) => getRemainingUses(s) <= 0) && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "All items placed" })
|
|
1867
|
+
] }),
|
|
1868
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-center gap-4", children: interaction.targetChoices.map((target) => /* @__PURE__ */ jsx(
|
|
1869
|
+
TargetBox,
|
|
1870
|
+
{
|
|
1871
|
+
target,
|
|
1872
|
+
placedSources: getPlacedSources(target.id),
|
|
1873
|
+
disabled: disabled || hasSubmitted,
|
|
1874
|
+
onRemoveSource: (instanceId) => {
|
|
1875
|
+
const index = getPlacedSources(target.id).findIndex((p) => p.instanceId === instanceId);
|
|
1876
|
+
if (index !== -1) {
|
|
1877
|
+
handleRemoveSource(target.id, index);
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
1880
|
+
pairCorrectness,
|
|
1881
|
+
hasSubmitted
|
|
1882
|
+
},
|
|
1883
|
+
target.id
|
|
1884
|
+
)) })
|
|
1885
|
+
] }),
|
|
1886
|
+
/* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: activeSource ? /* @__PURE__ */ jsx("div", { className: cn(sourceTokenVariants2({ state: "idle" }), "shadow-xl cursor-grabbing"), children: /* @__PURE__ */ jsx(SafeInlineHTML3, { html: activeSource.contentHtml }) }) : null })
|
|
1887
|
+
]
|
|
1888
|
+
}
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
function DragHandleIcon({ className }) {
|
|
1892
|
+
return /* @__PURE__ */ jsxs(
|
|
1893
|
+
"svg",
|
|
1894
|
+
{
|
|
1895
|
+
width: "12",
|
|
1896
|
+
height: "12",
|
|
1897
|
+
viewBox: "0 0 15 15",
|
|
1898
|
+
fill: "none",
|
|
1899
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1900
|
+
className,
|
|
1901
|
+
"aria-hidden": "true",
|
|
1902
|
+
role: "img",
|
|
1903
|
+
children: [
|
|
1904
|
+
/* @__PURE__ */ jsx("title", { children: "Drag handle" }),
|
|
1905
|
+
/* @__PURE__ */ jsx(
|
|
1906
|
+
"path",
|
|
1907
|
+
{
|
|
1908
|
+
d: "M5.5 3C5.5 3.27614 5.27614 3.5 5 3.5C4.72386 3.5 4.5 3.27614 4.5 3C4.5 2.72386 4.72386 2.5 5 2.5C5.27614 2.5 5.5 2.72386 5.5 3ZM5.5 7.5C5.5 7.77614 5.27614 8 5 8C4.72386 8 4.5 7.77614 4.5 7.5C4.5 7.22386 4.72386 7 5 7C5.27614 7 5.5 7.22386 5.5 7.5ZM5.5 12C5.5 12.2761 5.27614 12.5 5 12.5C4.72386 12.5 4.5 12.2761 4.5 12C4.5 11.7239 4.72386 11.5 5 11.5C5.27614 11.5 5.5 11.7239 5.5 12ZM9.5 3C9.5 3.27614 9.27614 3.5 9 3.5C8.72386 3.5 8.5 3.27614 8.5 3C8.5 2.72386 8.72386 2.5 9 2.5C9.27614 2.5 9.5 2.72386 9.5 3ZM9.5 7.5C9.5 7.77614 9.27614 8 9 8C8.72386 8 8.5 7.77614 8.5 7.5C8.5 7.22386 8.72386 7 9 7C9.27614 7 9.5 7.22386 9.5 7.5ZM9.5 12C9.5 12.2761 9.27614 12.5 9 12.5C8.72386 12.5 8.5 12.2761 8.5 12C8.5 11.7239 8.72386 11.5 9 11.5C9.27614 11.5 9.5 11.7239 9.5 12Z",
|
|
1909
|
+
fill: "currentColor",
|
|
1910
|
+
fillRule: "evenodd",
|
|
1911
|
+
clipRule: "evenodd"
|
|
1912
|
+
}
|
|
1913
|
+
)
|
|
1914
|
+
]
|
|
1915
|
+
}
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
function SortableItem({
|
|
1919
|
+
id,
|
|
1920
|
+
contentHtml,
|
|
1921
|
+
disabled,
|
|
1922
|
+
isHorizontal
|
|
1923
|
+
}) {
|
|
1924
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled });
|
|
1925
|
+
const style = {
|
|
1926
|
+
transform: CSS.Transform.toString(transform),
|
|
1927
|
+
transition,
|
|
1928
|
+
opacity: isDragging ? 0.4 : 1,
|
|
1929
|
+
zIndex: isDragging ? 50 : "auto"
|
|
1930
|
+
};
|
|
1931
|
+
return /* @__PURE__ */ jsxs(
|
|
1932
|
+
"div",
|
|
1933
|
+
{
|
|
1934
|
+
ref: setNodeRef,
|
|
1935
|
+
style,
|
|
1936
|
+
...attributes,
|
|
1937
|
+
...listeners,
|
|
1938
|
+
className: cn(
|
|
1939
|
+
"bg-background border rounded-lg shadow-sm touch-none select-none",
|
|
1940
|
+
// Styling differs based on orientation
|
|
1941
|
+
isHorizontal ? "px-4 py-2 min-w-[100px] flex items-center justify-center text-center" : "p-4 w-full flex items-center gap-3",
|
|
1942
|
+
disabled ? "cursor-default opacity-90 bg-muted/50" : "cursor-grab active:cursor-grabbing hover:border-macaw/50 hover:shadow-md"
|
|
1943
|
+
),
|
|
1944
|
+
children: [
|
|
1945
|
+
!isHorizontal && /* @__PURE__ */ jsx("div", { className: "text-muted-foreground shrink-0", children: /* @__PURE__ */ jsx(DragHandleIcon, {}) }),
|
|
1946
|
+
/* @__PURE__ */ jsx(HTMLContent, { html: contentHtml })
|
|
1947
|
+
]
|
|
1948
|
+
}
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
function DragOverlayItem({ contentHtml, isHorizontal }) {
|
|
1952
|
+
return /* @__PURE__ */ jsxs(
|
|
1953
|
+
"div",
|
|
1954
|
+
{
|
|
1955
|
+
className: cn(
|
|
1956
|
+
"bg-background border-2 border-macaw rounded-lg shadow-xl cursor-grabbing z-50",
|
|
1957
|
+
isHorizontal ? "px-4 py-2 min-w-[100px] flex items-center justify-center" : "p-4 w-full flex items-center gap-3"
|
|
1958
|
+
),
|
|
1959
|
+
children: [
|
|
1960
|
+
!isHorizontal && /* @__PURE__ */ jsx("div", { className: "text-macaw shrink-0", children: /* @__PURE__ */ jsx(DragHandleIcon, {}) }),
|
|
1961
|
+
/* @__PURE__ */ jsx(HTMLContent, { html: contentHtml })
|
|
1962
|
+
]
|
|
1963
|
+
}
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
function OrderInteraction({
|
|
1967
|
+
interaction,
|
|
1968
|
+
response,
|
|
1969
|
+
onAnswerSelect,
|
|
1970
|
+
disabled,
|
|
1971
|
+
hasSubmitted,
|
|
1972
|
+
isCorrect
|
|
1973
|
+
}) {
|
|
1974
|
+
const items = React3.useMemo(() => {
|
|
1975
|
+
if (response && response.length > 0) {
|
|
1976
|
+
const mapped = [];
|
|
1977
|
+
for (const id of response) {
|
|
1978
|
+
const found = interaction.choices.find((c) => c.id === id);
|
|
1979
|
+
if (found) mapped.push(found);
|
|
1980
|
+
}
|
|
1981
|
+
return mapped;
|
|
1982
|
+
}
|
|
1983
|
+
return interaction.choices;
|
|
1984
|
+
}, [response, interaction.choices]);
|
|
1985
|
+
const [activeId, setActiveId] = React3.useState(null);
|
|
1986
|
+
const activeItem = React3.useMemo(() => items.find((i) => i.id === activeId), [activeId, items]);
|
|
1987
|
+
const sensors = useSensors(
|
|
1988
|
+
useSensor(PointerSensor),
|
|
1989
|
+
useSensor(KeyboardSensor, {
|
|
1990
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
1991
|
+
})
|
|
1992
|
+
);
|
|
1993
|
+
const handleDragStart = (event) => {
|
|
1994
|
+
const id = event.active.id;
|
|
1995
|
+
setActiveId(typeof id === "string" ? id : String(id));
|
|
1996
|
+
};
|
|
1997
|
+
const handleDragEnd = (event) => {
|
|
1998
|
+
const { active, over } = event;
|
|
1999
|
+
setActiveId(null);
|
|
2000
|
+
if (over && active.id !== over.id) {
|
|
2001
|
+
const oldIndex = items.findIndex((item) => item.id === active.id);
|
|
2002
|
+
const newIndex = items.findIndex((item) => item.id === over.id);
|
|
2003
|
+
const newOrder = arrayMove(items, oldIndex, newIndex);
|
|
2004
|
+
onAnswerSelect?.(newOrder.map((item) => item.id));
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
const isHorizontal = interaction.orientation === "horizontal";
|
|
2008
|
+
const strategy = isHorizontal ? horizontalListSortingStrategy : verticalListSortingStrategy;
|
|
2009
|
+
const dropAnimation = {
|
|
2010
|
+
sideEffects: defaultDropAnimationSideEffects({
|
|
2011
|
+
styles: { active: { opacity: "0.4" } }
|
|
2012
|
+
})
|
|
2013
|
+
};
|
|
2014
|
+
const getContainerStyles = () => {
|
|
2015
|
+
if (!hasSubmitted) {
|
|
2016
|
+
return "bg-muted/30 border-transparent";
|
|
2017
|
+
}
|
|
2018
|
+
if (isCorrect) {
|
|
2019
|
+
return "bg-owl/10 border-owl";
|
|
2020
|
+
}
|
|
2021
|
+
return "bg-cardinal/10 border-cardinal";
|
|
2022
|
+
};
|
|
2023
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
2024
|
+
interaction.promptHtml && /* @__PURE__ */ jsx(HTMLContent, { html: interaction.promptHtml }),
|
|
2025
|
+
/* @__PURE__ */ jsxs(
|
|
2026
|
+
DndContext,
|
|
2027
|
+
{
|
|
2028
|
+
sensors,
|
|
2029
|
+
collisionDetection: closestCenter,
|
|
2030
|
+
onDragStart: handleDragStart,
|
|
2031
|
+
onDragEnd: handleDragEnd,
|
|
2032
|
+
children: [
|
|
2033
|
+
/* @__PURE__ */ jsx(SortableContext, { items: items.map((i) => i.id), strategy, disabled: disabled || hasSubmitted, children: /* @__PURE__ */ jsx(
|
|
2034
|
+
"div",
|
|
2035
|
+
{
|
|
2036
|
+
className: cn(
|
|
2037
|
+
"rounded-xl p-4 transition-colors border-2",
|
|
2038
|
+
isHorizontal ? "flex flex-wrap gap-3" : "flex flex-col gap-2",
|
|
2039
|
+
getContainerStyles()
|
|
2040
|
+
),
|
|
2041
|
+
children: items.map((item) => /* @__PURE__ */ jsx(
|
|
2042
|
+
SortableItem,
|
|
2043
|
+
{
|
|
2044
|
+
id: item.id,
|
|
2045
|
+
contentHtml: item.contentHtml,
|
|
2046
|
+
disabled: !!(disabled || hasSubmitted),
|
|
2047
|
+
isHorizontal
|
|
2048
|
+
},
|
|
2049
|
+
item.id
|
|
2050
|
+
))
|
|
2051
|
+
}
|
|
2052
|
+
) }),
|
|
2053
|
+
/* @__PURE__ */ jsx(DragOverlay, { dropAnimation, children: activeItem ? /* @__PURE__ */ jsx(DragOverlayItem, { contentHtml: activeItem.contentHtml, isHorizontal }) : null })
|
|
2054
|
+
]
|
|
2055
|
+
}
|
|
2056
|
+
)
|
|
2057
|
+
] });
|
|
2058
|
+
}
|
|
2059
|
+
var textEntryVariants = cva(
|
|
2060
|
+
[
|
|
2061
|
+
// Base styles matching Select trigger
|
|
2062
|
+
"inline-flex items-center justify-center",
|
|
2063
|
+
"h-7 px-2 rounded-xs text-sm font-semibold",
|
|
2064
|
+
"bg-[var(--text-entry-background)] text-[var(--text-entry-foreground)]",
|
|
2065
|
+
"border border-[var(--text-entry-complement)]",
|
|
2066
|
+
// border = 1px width, border-[...] = color
|
|
2067
|
+
"shadow-[0_3px_0_var(--text-entry-complement)]",
|
|
2068
|
+
"transition-all duration-150",
|
|
2069
|
+
"hover:brightness-90 dark:hover:brightness-75",
|
|
2070
|
+
// Placeholder and focus (no ring - matches Select component)
|
|
2071
|
+
"placeholder:text-muted-foreground/60",
|
|
2072
|
+
"focus:outline-none",
|
|
2073
|
+
// Disabled state
|
|
2074
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
2075
|
+
// Invalid state
|
|
2076
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
|
2077
|
+
],
|
|
2078
|
+
{
|
|
2079
|
+
variants: {
|
|
2080
|
+
palette: {
|
|
2081
|
+
default: [
|
|
2082
|
+
"[--text-entry-background:var(--color-background)]",
|
|
2083
|
+
"[--text-entry-foreground:var(--color-foreground)]",
|
|
2084
|
+
"[--text-entry-complement:var(--color-accent)]"
|
|
2085
|
+
],
|
|
2086
|
+
betta: [
|
|
2087
|
+
"[--text-entry-background:var(--color-background)]",
|
|
2088
|
+
"[--text-entry-foreground:var(--color-betta)]",
|
|
2089
|
+
"[--text-entry-complement:var(--color-butterfly)]"
|
|
2090
|
+
],
|
|
2091
|
+
cardinal: [
|
|
2092
|
+
"[--text-entry-background:hsl(var(--cardinal)/0.15)]",
|
|
2093
|
+
"[--text-entry-foreground:var(--color-cardinal)]",
|
|
2094
|
+
"[--text-entry-complement:var(--color-fire-ant)]"
|
|
2095
|
+
],
|
|
2096
|
+
bee: [
|
|
2097
|
+
"[--text-entry-background:var(--color-background)]",
|
|
2098
|
+
"[--text-entry-foreground:var(--color-bee)]",
|
|
2099
|
+
"[--text-entry-complement:var(--color-lion)]"
|
|
2100
|
+
],
|
|
2101
|
+
owl: [
|
|
2102
|
+
"[--text-entry-background:hsl(var(--owl)/0.15)]",
|
|
2103
|
+
"[--text-entry-foreground:var(--color-owl)]",
|
|
2104
|
+
"[--text-entry-complement:var(--color-tree-frog)]"
|
|
2105
|
+
],
|
|
2106
|
+
macaw: [
|
|
2107
|
+
"[--text-entry-background:var(--color-background)]",
|
|
2108
|
+
"[--text-entry-foreground:var(--color-macaw)]",
|
|
2109
|
+
"[--text-entry-complement:var(--color-whale)]"
|
|
2110
|
+
]
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
defaultVariants: {
|
|
2114
|
+
palette: "default"
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
);
|
|
2118
|
+
function TextEntryInteraction({
|
|
2119
|
+
interaction,
|
|
2120
|
+
response,
|
|
2121
|
+
onAnswerSelect,
|
|
2122
|
+
disabled = false,
|
|
2123
|
+
hasSubmitted,
|
|
2124
|
+
palette = "macaw",
|
|
2125
|
+
isCorrect
|
|
2126
|
+
}) {
|
|
2127
|
+
const value = response ?? "";
|
|
2128
|
+
const hasValue = value.length > 0;
|
|
2129
|
+
const getActivePalette = () => {
|
|
2130
|
+
if (hasSubmitted && hasValue) {
|
|
2131
|
+
return isCorrect ? "owl" : "cardinal";
|
|
2132
|
+
}
|
|
2133
|
+
if (hasValue) {
|
|
2134
|
+
return palette;
|
|
2135
|
+
}
|
|
2136
|
+
return "default";
|
|
2137
|
+
};
|
|
2138
|
+
const activePalette = getActivePalette();
|
|
2139
|
+
const charWidth = interaction.expectedLength ?? 10;
|
|
2140
|
+
const inputWidth = `${Math.max(charWidth, 3) + 4}ch`;
|
|
2141
|
+
return /* @__PURE__ */ jsx("div", { className: "my-4 inline-block", children: /* @__PURE__ */ jsx(
|
|
2142
|
+
"input",
|
|
2143
|
+
{
|
|
2144
|
+
type: "text",
|
|
2145
|
+
value,
|
|
2146
|
+
onChange: (e) => onAnswerSelect?.(e.target.value),
|
|
2147
|
+
disabled: disabled || hasSubmitted,
|
|
2148
|
+
placeholder: interaction.placeholder ?? "...",
|
|
2149
|
+
pattern: interaction.patternMask,
|
|
2150
|
+
className: cn(textEntryVariants({ palette: activePalette })),
|
|
2151
|
+
style: { width: inputWidth, minWidth: "8ch" },
|
|
2152
|
+
"aria-invalid": hasSubmitted && hasValue && isCorrect === false ? true : void 0
|
|
2153
|
+
}
|
|
2154
|
+
) });
|
|
2155
|
+
}
|
|
2156
|
+
function InlineTextEntry({
|
|
2157
|
+
embed,
|
|
2158
|
+
response,
|
|
2159
|
+
onAnswerSelect,
|
|
2160
|
+
disabled = false,
|
|
2161
|
+
hasSubmitted,
|
|
2162
|
+
palette = "macaw",
|
|
2163
|
+
isCorrect
|
|
2164
|
+
}) {
|
|
2165
|
+
const value = response ?? "";
|
|
2166
|
+
const hasValue = value.length > 0;
|
|
2167
|
+
const getActivePalette = () => {
|
|
2168
|
+
if (hasSubmitted && hasValue) {
|
|
2169
|
+
return isCorrect ? "owl" : "cardinal";
|
|
2170
|
+
}
|
|
2171
|
+
if (hasValue) {
|
|
2172
|
+
return palette;
|
|
2173
|
+
}
|
|
2174
|
+
return "default";
|
|
2175
|
+
};
|
|
2176
|
+
const activePalette = getActivePalette();
|
|
2177
|
+
const charWidth = embed.expectedLength ?? 4;
|
|
2178
|
+
const inputWidth = `${Math.max(charWidth, 2) + 3}ch`;
|
|
2179
|
+
return /* @__PURE__ */ jsx(
|
|
2180
|
+
"input",
|
|
2181
|
+
{
|
|
2182
|
+
type: "text",
|
|
2183
|
+
value,
|
|
2184
|
+
onChange: (e) => onAnswerSelect?.(e.target.value),
|
|
2185
|
+
disabled: disabled || hasSubmitted,
|
|
2186
|
+
placeholder: "...",
|
|
2187
|
+
pattern: embed.patternMask,
|
|
2188
|
+
className: cn(textEntryVariants({ palette: activePalette }), "align-baseline"),
|
|
2189
|
+
style: { width: inputWidth, minWidth: "6ch" },
|
|
2190
|
+
"aria-invalid": hasSubmitted && hasValue && isCorrect === false ? true : void 0
|
|
2191
|
+
}
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
function OrderInteractionBlock({
|
|
2195
|
+
interaction,
|
|
2196
|
+
responses,
|
|
2197
|
+
onAnswerSelect,
|
|
2198
|
+
disabled,
|
|
2199
|
+
showFeedback,
|
|
2200
|
+
perResponseFeedback
|
|
2201
|
+
}) {
|
|
2202
|
+
const response = responses?.[interaction.responseId];
|
|
2203
|
+
const hasInitialized = React3.useRef(false);
|
|
2204
|
+
React3.useEffect(() => {
|
|
2205
|
+
if (hasInitialized.current) return;
|
|
2206
|
+
if (response) return;
|
|
2207
|
+
if (interaction.choices.length === 0) return;
|
|
2208
|
+
if (!onAnswerSelect) return;
|
|
2209
|
+
hasInitialized.current = true;
|
|
2210
|
+
const initialOrder = interaction.choices.map((c) => c.id);
|
|
2211
|
+
onAnswerSelect(interaction.responseId, initialOrder);
|
|
2212
|
+
}, [response, interaction.choices, interaction.responseId, onAnswerSelect]);
|
|
2213
|
+
let responseArray = [];
|
|
2214
|
+
if (Array.isArray(response)) {
|
|
2215
|
+
responseArray = response;
|
|
2216
|
+
} else if (typeof response === "string") {
|
|
2217
|
+
responseArray = [response];
|
|
2218
|
+
}
|
|
2219
|
+
const feedback = perResponseFeedback?.[interaction.responseId];
|
|
2220
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2221
|
+
/* @__PURE__ */ jsx(
|
|
2222
|
+
OrderInteraction,
|
|
2223
|
+
{
|
|
2224
|
+
interaction,
|
|
2225
|
+
response: responseArray,
|
|
2226
|
+
onAnswerSelect: (val) => onAnswerSelect?.(interaction.responseId, val),
|
|
2227
|
+
disabled,
|
|
2228
|
+
hasSubmitted: showFeedback,
|
|
2229
|
+
isCorrect: feedback?.isCorrect
|
|
2230
|
+
}
|
|
2231
|
+
),
|
|
2232
|
+
/* @__PURE__ */ jsx(
|
|
2233
|
+
FeedbackMessage,
|
|
2234
|
+
{
|
|
2235
|
+
responseId: interaction.responseId,
|
|
2236
|
+
showFeedback,
|
|
2237
|
+
perResponseFeedback
|
|
2238
|
+
}
|
|
2239
|
+
)
|
|
2240
|
+
] });
|
|
2241
|
+
}
|
|
2242
|
+
function GapMatchInteractionBlock({
|
|
2243
|
+
interaction,
|
|
2244
|
+
responses,
|
|
2245
|
+
onAnswerSelect,
|
|
2246
|
+
disabled,
|
|
2247
|
+
showFeedback,
|
|
2248
|
+
perResponseFeedback,
|
|
2249
|
+
selectedChoicesByResponse
|
|
2250
|
+
}) {
|
|
2251
|
+
const response = responses?.[interaction.responseId];
|
|
2252
|
+
let responseArray = [];
|
|
2253
|
+
if (Array.isArray(response)) {
|
|
2254
|
+
responseArray = response;
|
|
2255
|
+
} else if (typeof response === "string" && response) {
|
|
2256
|
+
responseArray = [response];
|
|
2257
|
+
}
|
|
2258
|
+
const gapCorrectness = React3.useMemo(() => {
|
|
2259
|
+
const entries = selectedChoicesByResponse?.[interaction.responseId];
|
|
2260
|
+
if (!entries || !showFeedback) return void 0;
|
|
2261
|
+
const result = {};
|
|
2262
|
+
for (const entry of entries) {
|
|
2263
|
+
const parts = entry.id.split(" ");
|
|
2264
|
+
const gapId = parts[1];
|
|
2265
|
+
if (gapId) {
|
|
2266
|
+
result[gapId] = entry.isCorrect;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return result;
|
|
2270
|
+
}, [selectedChoicesByResponse, interaction.responseId, showFeedback]);
|
|
2271
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2272
|
+
/* @__PURE__ */ jsx(
|
|
2273
|
+
GapMatchInteraction,
|
|
2274
|
+
{
|
|
2275
|
+
interaction,
|
|
2276
|
+
response: responseArray,
|
|
2277
|
+
onAnswerSelect: (val) => onAnswerSelect?.(interaction.responseId, val),
|
|
2278
|
+
disabled,
|
|
2279
|
+
hasSubmitted: showFeedback,
|
|
2280
|
+
gapCorrectness
|
|
2281
|
+
}
|
|
2282
|
+
),
|
|
2283
|
+
/* @__PURE__ */ jsx(
|
|
2284
|
+
FeedbackMessage,
|
|
2285
|
+
{
|
|
2286
|
+
responseId: interaction.responseId,
|
|
2287
|
+
showFeedback,
|
|
2288
|
+
perResponseFeedback
|
|
2289
|
+
}
|
|
2290
|
+
)
|
|
2291
|
+
] });
|
|
2292
|
+
}
|
|
2293
|
+
function MatchInteractionBlock({
|
|
2294
|
+
interaction,
|
|
2295
|
+
responses,
|
|
2296
|
+
onAnswerSelect,
|
|
2297
|
+
disabled,
|
|
2298
|
+
showFeedback,
|
|
2299
|
+
perResponseFeedback,
|
|
2300
|
+
selectedChoicesByResponse
|
|
2301
|
+
}) {
|
|
2302
|
+
const response = responses?.[interaction.responseId];
|
|
2303
|
+
let responseArray = [];
|
|
2304
|
+
if (Array.isArray(response)) {
|
|
2305
|
+
responseArray = response;
|
|
2306
|
+
} else if (typeof response === "string" && response) {
|
|
2307
|
+
responseArray = [response];
|
|
2308
|
+
}
|
|
2309
|
+
const pairCorrectness = React3.useMemo(() => {
|
|
2310
|
+
const entries = selectedChoicesByResponse?.[interaction.responseId];
|
|
2311
|
+
if (!entries || !showFeedback) return void 0;
|
|
2312
|
+
const result = {};
|
|
2313
|
+
for (const entry of entries) {
|
|
2314
|
+
result[entry.id] = entry.isCorrect;
|
|
2315
|
+
}
|
|
2316
|
+
return result;
|
|
2317
|
+
}, [selectedChoicesByResponse, interaction.responseId, showFeedback]);
|
|
2318
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2319
|
+
/* @__PURE__ */ jsx(
|
|
2320
|
+
MatchInteraction,
|
|
2321
|
+
{
|
|
2322
|
+
interaction,
|
|
2323
|
+
response: responseArray,
|
|
2324
|
+
onAnswerSelect: (val) => onAnswerSelect?.(interaction.responseId, val),
|
|
2325
|
+
disabled,
|
|
2326
|
+
hasSubmitted: showFeedback,
|
|
2327
|
+
pairCorrectness
|
|
2328
|
+
}
|
|
2329
|
+
),
|
|
2330
|
+
/* @__PURE__ */ jsx(
|
|
2331
|
+
FeedbackMessage,
|
|
2332
|
+
{
|
|
2333
|
+
responseId: interaction.responseId,
|
|
2334
|
+
showFeedback,
|
|
2335
|
+
perResponseFeedback
|
|
2336
|
+
}
|
|
2337
|
+
)
|
|
2338
|
+
] });
|
|
2339
|
+
}
|
|
2340
|
+
function ContentBlockRenderer({
|
|
2341
|
+
block,
|
|
2342
|
+
responses,
|
|
2343
|
+
onAnswerSelect,
|
|
2344
|
+
showFeedback,
|
|
2345
|
+
disabled,
|
|
2346
|
+
selectedChoicesByResponse,
|
|
2347
|
+
perResponseFeedback
|
|
2348
|
+
}) {
|
|
2349
|
+
if (block.type === "stimulus") {
|
|
2350
|
+
return /* @__PURE__ */ jsx(HTMLContent, { html: block.html, className: "mb-6" });
|
|
2351
|
+
}
|
|
2352
|
+
if (block.type === "richStimulus") {
|
|
2353
|
+
const allResponseIds = [
|
|
2354
|
+
...Object.values(block.inlineEmbeds).map((e) => e.responseId),
|
|
2355
|
+
...Object.values(block.textEmbeds).map((e) => e.responseId)
|
|
2356
|
+
];
|
|
2357
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2358
|
+
/* @__PURE__ */ jsx(
|
|
2359
|
+
HTMLContent,
|
|
2360
|
+
{
|
|
2361
|
+
html: block.html,
|
|
2362
|
+
className: "mb-6",
|
|
2363
|
+
inlineEmbeds: block.inlineEmbeds,
|
|
2364
|
+
textEmbeds: block.textEmbeds,
|
|
2365
|
+
renderInline: (embed) => {
|
|
2366
|
+
const currentValue = (() => {
|
|
2367
|
+
const v = responses?.[embed.responseId];
|
|
2368
|
+
return typeof v === "string" ? v : "";
|
|
2369
|
+
})();
|
|
2370
|
+
const perSelectionEntries = selectedChoicesByResponse?.[embed.responseId];
|
|
2371
|
+
const selectedIsCorrect = perSelectionEntries && currentValue ? perSelectionEntries.find((e) => e.id === currentValue)?.isCorrect : void 0;
|
|
2372
|
+
return /* @__PURE__ */ jsx(
|
|
2373
|
+
InlineInteraction,
|
|
2374
|
+
{
|
|
2375
|
+
embed,
|
|
2376
|
+
response: currentValue,
|
|
2377
|
+
onAnswerSelect: (v) => {
|
|
2378
|
+
if (onAnswerSelect) onAnswerSelect(embed.responseId, v);
|
|
2379
|
+
},
|
|
2380
|
+
disabled: disabled || showFeedback,
|
|
2381
|
+
hasSubmitted: showFeedback,
|
|
2382
|
+
isCorrect: selectedIsCorrect
|
|
2383
|
+
}
|
|
2384
|
+
);
|
|
2385
|
+
},
|
|
2386
|
+
renderTextEntry: (embed) => {
|
|
2387
|
+
const currentValue = (() => {
|
|
2388
|
+
const v = responses?.[embed.responseId];
|
|
2389
|
+
return typeof v === "string" ? v : "";
|
|
2390
|
+
})();
|
|
2391
|
+
const feedback = perResponseFeedback?.[embed.responseId];
|
|
2392
|
+
return /* @__PURE__ */ jsx(
|
|
2393
|
+
InlineTextEntry,
|
|
2394
|
+
{
|
|
2395
|
+
embed,
|
|
2396
|
+
response: currentValue,
|
|
2397
|
+
onAnswerSelect: (v) => {
|
|
2398
|
+
if (onAnswerSelect) onAnswerSelect(embed.responseId, v);
|
|
2399
|
+
},
|
|
2400
|
+
disabled: disabled || showFeedback,
|
|
2401
|
+
hasSubmitted: showFeedback,
|
|
2402
|
+
isCorrect: feedback?.isCorrect
|
|
2403
|
+
},
|
|
2404
|
+
embed.responseId
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
),
|
|
2409
|
+
allResponseIds.map((responseId) => /* @__PURE__ */ jsx(
|
|
2410
|
+
FeedbackMessage,
|
|
2411
|
+
{
|
|
2412
|
+
responseId,
|
|
2413
|
+
showFeedback,
|
|
2414
|
+
perResponseFeedback
|
|
2415
|
+
},
|
|
2416
|
+
`fb-${responseId}`
|
|
2417
|
+
))
|
|
2418
|
+
] });
|
|
2419
|
+
}
|
|
2420
|
+
if (block.type === "interaction" && block.interaction) {
|
|
2421
|
+
const interaction = block.interaction;
|
|
2422
|
+
if (interaction.type === "text") {
|
|
2423
|
+
const response = responses?.[interaction.responseId];
|
|
2424
|
+
const currentValue = typeof response === "string" ? response : "";
|
|
2425
|
+
const feedback = perResponseFeedback?.[interaction.responseId];
|
|
2426
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2427
|
+
/* @__PURE__ */ jsx(
|
|
2428
|
+
TextEntryInteraction,
|
|
2429
|
+
{
|
|
2430
|
+
interaction,
|
|
2431
|
+
response: currentValue,
|
|
2432
|
+
onAnswerSelect: (value) => {
|
|
2433
|
+
if (onAnswerSelect) {
|
|
2434
|
+
onAnswerSelect(interaction.responseId, value);
|
|
2435
|
+
}
|
|
2436
|
+
},
|
|
2437
|
+
disabled,
|
|
2438
|
+
hasSubmitted: showFeedback,
|
|
2439
|
+
isCorrect: feedback?.isCorrect
|
|
2440
|
+
}
|
|
2441
|
+
),
|
|
2442
|
+
/* @__PURE__ */ jsx(
|
|
2443
|
+
FeedbackMessage,
|
|
2444
|
+
{
|
|
2445
|
+
responseId: interaction.responseId,
|
|
2446
|
+
showFeedback,
|
|
2447
|
+
perResponseFeedback
|
|
2448
|
+
}
|
|
2449
|
+
)
|
|
2450
|
+
] });
|
|
2451
|
+
}
|
|
2452
|
+
if (interaction.type === "choice") {
|
|
2453
|
+
const response = responses?.[interaction.responseId];
|
|
2454
|
+
let selected = [];
|
|
2455
|
+
if (Array.isArray(response)) {
|
|
2456
|
+
selected = response;
|
|
2457
|
+
} else if (typeof response === "string") {
|
|
2458
|
+
selected = [response];
|
|
2459
|
+
}
|
|
2460
|
+
const perSelectionEntries = selectedChoicesByResponse?.[interaction.responseId];
|
|
2461
|
+
const perSelectionTable = perSelectionEntries ? new Map(perSelectionEntries.map((e) => [e.id, e.isCorrect])) : void 0;
|
|
2462
|
+
let singleIsCorrect;
|
|
2463
|
+
if (interaction.cardinality === "single" && selected.length === 1 && perSelectionTable) {
|
|
2464
|
+
const firstCandidate = selected[0];
|
|
2465
|
+
if (typeof firstCandidate === "string") {
|
|
2466
|
+
singleIsCorrect = perSelectionTable.get(firstCandidate) === true;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
const toChoiceInteraction = (d) => {
|
|
2470
|
+
return {
|
|
2471
|
+
type: "choiceInteraction",
|
|
2472
|
+
responseIdentifier: d.responseId,
|
|
2473
|
+
shuffle: false,
|
|
2474
|
+
minChoices: d.minChoices,
|
|
2475
|
+
maxChoices: d.maxChoices,
|
|
2476
|
+
promptHtml: d.promptHtml,
|
|
2477
|
+
choices: d.choices.map((c) => ({
|
|
2478
|
+
identifier: c.id,
|
|
2479
|
+
contentHtml: c.contentHtml,
|
|
2480
|
+
inlineFeedbackHtml: c.inlineFeedbackHtml
|
|
2481
|
+
}))
|
|
2482
|
+
};
|
|
2483
|
+
};
|
|
2484
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2485
|
+
/* @__PURE__ */ jsx(
|
|
2486
|
+
ChoiceInteractionRenderer,
|
|
2487
|
+
{
|
|
2488
|
+
interaction: toChoiceInteraction(interaction),
|
|
2489
|
+
response,
|
|
2490
|
+
onAnswerSelect: (value) => {
|
|
2491
|
+
if (onAnswerSelect) {
|
|
2492
|
+
onAnswerSelect(interaction.responseId, value);
|
|
2493
|
+
}
|
|
2494
|
+
},
|
|
2495
|
+
disabled,
|
|
2496
|
+
hasSubmitted: showFeedback,
|
|
2497
|
+
isCorrect: singleIsCorrect,
|
|
2498
|
+
selectedChoicesCorrectness: (() => {
|
|
2499
|
+
const entries = selectedChoicesByResponse?.[interaction.responseId];
|
|
2500
|
+
if (!entries) return void 0;
|
|
2501
|
+
const table = new Map(entries.map((e) => [e.id, e.isCorrect]));
|
|
2502
|
+
return selected.map((id) => ({ id, isCorrect: table.get(id) === true }));
|
|
2503
|
+
})()
|
|
2504
|
+
}
|
|
2505
|
+
),
|
|
2506
|
+
/* @__PURE__ */ jsx(
|
|
2507
|
+
FeedbackMessage,
|
|
2508
|
+
{
|
|
2509
|
+
responseId: interaction.responseId,
|
|
2510
|
+
showFeedback,
|
|
2511
|
+
perResponseFeedback
|
|
2512
|
+
}
|
|
2513
|
+
)
|
|
2514
|
+
] });
|
|
2515
|
+
}
|
|
2516
|
+
if (interaction.type === "order") {
|
|
2517
|
+
return /* @__PURE__ */ jsx(
|
|
2518
|
+
OrderInteractionBlock,
|
|
2519
|
+
{
|
|
2520
|
+
interaction,
|
|
2521
|
+
responses,
|
|
2522
|
+
onAnswerSelect,
|
|
2523
|
+
disabled,
|
|
2524
|
+
showFeedback,
|
|
2525
|
+
perResponseFeedback
|
|
2526
|
+
}
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
if (interaction.type === "gapMatch") {
|
|
2530
|
+
return /* @__PURE__ */ jsx(
|
|
2531
|
+
GapMatchInteractionBlock,
|
|
2532
|
+
{
|
|
2533
|
+
interaction,
|
|
2534
|
+
responses,
|
|
2535
|
+
onAnswerSelect,
|
|
2536
|
+
disabled,
|
|
2537
|
+
showFeedback,
|
|
2538
|
+
perResponseFeedback,
|
|
2539
|
+
selectedChoicesByResponse
|
|
2540
|
+
}
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
if (interaction.type === "match") {
|
|
2544
|
+
return /* @__PURE__ */ jsx(
|
|
2545
|
+
MatchInteractionBlock,
|
|
2546
|
+
{
|
|
2547
|
+
interaction,
|
|
2548
|
+
responses,
|
|
2549
|
+
onAnswerSelect,
|
|
2550
|
+
disabled,
|
|
2551
|
+
showFeedback,
|
|
2552
|
+
perResponseFeedback,
|
|
2553
|
+
selectedChoicesByResponse
|
|
2554
|
+
}
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return null;
|
|
2559
|
+
}
|
|
2560
|
+
function QTIRenderer({
|
|
2561
|
+
item,
|
|
2562
|
+
responses,
|
|
2563
|
+
onResponseChange,
|
|
2564
|
+
theme = "duolingo",
|
|
2565
|
+
showFeedback = false,
|
|
2566
|
+
disabled = false,
|
|
2567
|
+
choiceCorrectness,
|
|
2568
|
+
responseFeedback,
|
|
2569
|
+
overallFeedback,
|
|
2570
|
+
className
|
|
2571
|
+
}) {
|
|
2572
|
+
const themeAttr = theme === "duolingo" ? void 0 : theme;
|
|
2573
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("qti-container", className), "data-qti-theme": themeAttr, children: [
|
|
2574
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-6", children: item.contentBlocks.map((block, index) => /* @__PURE__ */ jsx(
|
|
2575
|
+
ContentBlockRenderer,
|
|
2576
|
+
{
|
|
2577
|
+
block,
|
|
2578
|
+
responses,
|
|
2579
|
+
onAnswerSelect: onResponseChange,
|
|
2580
|
+
showFeedback,
|
|
2581
|
+
disabled,
|
|
2582
|
+
selectedChoicesByResponse: choiceCorrectness,
|
|
2583
|
+
perResponseFeedback: responseFeedback
|
|
2584
|
+
},
|
|
2585
|
+
index
|
|
2586
|
+
)) }),
|
|
2587
|
+
showFeedback && overallFeedback?.messageHtml && /* @__PURE__ */ jsx("div", { className: "qti-feedback mt-6 p-6", "data-correct": overallFeedback.isCorrect, children: /* @__PURE__ */ jsx(HTMLContent, { html: overallFeedback.messageHtml }) })
|
|
2588
|
+
] });
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// ../../node_modules/@superbuilders/errors/dist/index.js
|
|
2592
|
+
function z() {
|
|
2593
|
+
let k2 = [], j2 = this;
|
|
2594
|
+
while (j2 != null) if (k2.push(j2.message), j2.cause instanceof Error) j2 = j2.cause;
|
|
2595
|
+
else break;
|
|
2596
|
+
return k2.join(": ");
|
|
2597
|
+
}
|
|
2598
|
+
function A(k2) {
|
|
2599
|
+
let j2 = new Error(k2);
|
|
2600
|
+
if (Error.captureStackTrace) Error.captureStackTrace(j2, A);
|
|
2601
|
+
return j2.toString = z, Object.freeze(j2);
|
|
2602
|
+
}
|
|
2603
|
+
function B(k2, j2) {
|
|
2604
|
+
let x = new Error(j2, { cause: k2 });
|
|
2605
|
+
if (Error.captureStackTrace) Error.captureStackTrace(x, B);
|
|
2606
|
+
return x.toString = z, Object.freeze(x);
|
|
2607
|
+
}
|
|
2608
|
+
function I(k2) {
|
|
2609
|
+
try {
|
|
2610
|
+
return { data: k2(), error: void 0 };
|
|
2611
|
+
} catch (j2) {
|
|
2612
|
+
return { data: void 0, error: j2 instanceof Error ? j2 : new Error(String(j2)) };
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
var QtiCardinalitySchema = z$1.enum(["single", "multiple", "ordered"]);
|
|
2616
|
+
var QtiBaseTypeSchema = z$1.enum(["identifier", "string", "float", "integer", "boolean", "directedPair", "pair"]);
|
|
2617
|
+
var SimpleChoiceSchema = z$1.object({
|
|
2618
|
+
identifier: z$1.string().min(1),
|
|
2619
|
+
contentHtml: z$1.string(),
|
|
2620
|
+
// Allows "" for empty or image-only content
|
|
2621
|
+
inlineFeedbackHtml: z$1.string().optional()
|
|
2622
|
+
// Optional per-choice feedback (qti-feedback-inline)
|
|
2623
|
+
});
|
|
2624
|
+
var InlineChoiceSchema = z$1.object({
|
|
2625
|
+
identifier: z$1.string().min(1),
|
|
2626
|
+
contentHtml: z$1.string()
|
|
2627
|
+
});
|
|
2628
|
+
var ChoiceInteractionCoreSchema = z$1.object({
|
|
2629
|
+
responseIdentifier: z$1.string().min(1),
|
|
2630
|
+
shuffle: z$1.boolean(),
|
|
2631
|
+
minChoices: z$1.number().int().min(0),
|
|
2632
|
+
maxChoices: z$1.number().int().min(1),
|
|
2633
|
+
promptHtml: z$1.string(),
|
|
2634
|
+
choices: z$1.array(SimpleChoiceSchema).min(1)
|
|
2635
|
+
});
|
|
2636
|
+
var ChoiceInteractionSchema = ChoiceInteractionCoreSchema.extend({ type: z$1.literal("choiceInteraction") });
|
|
2637
|
+
var InlineChoiceInteractionCoreSchema = z$1.object({
|
|
2638
|
+
responseIdentifier: z$1.string().min(1),
|
|
2639
|
+
shuffle: z$1.boolean(),
|
|
2640
|
+
choices: z$1.array(InlineChoiceSchema).min(1)
|
|
2641
|
+
});
|
|
2642
|
+
var InlineChoiceInteractionSchema = InlineChoiceInteractionCoreSchema.extend({
|
|
2643
|
+
type: z$1.literal("inlineChoiceInteraction")
|
|
2644
|
+
});
|
|
2645
|
+
var TextEntryInteractionCoreSchema = z$1.object({
|
|
2646
|
+
responseIdentifier: z$1.string().min(1),
|
|
2647
|
+
expectedLength: z$1.number().int().min(0).optional(),
|
|
2648
|
+
placeholderText: z$1.string().optional(),
|
|
2649
|
+
patternMask: z$1.string().optional()
|
|
2650
|
+
});
|
|
2651
|
+
var TextEntryInteractionSchema = TextEntryInteractionCoreSchema.extend({
|
|
2652
|
+
type: z$1.literal("textEntryInteraction")
|
|
2653
|
+
});
|
|
2654
|
+
var OrderInteractionCoreSchema = z$1.object({
|
|
2655
|
+
responseIdentifier: z$1.string().min(1),
|
|
2656
|
+
shuffle: z$1.boolean(),
|
|
2657
|
+
minChoices: z$1.number().int().min(0),
|
|
2658
|
+
maxChoices: z$1.number().int().min(0).optional(),
|
|
2659
|
+
// 0 or undefined usually means "all"
|
|
2660
|
+
orientation: z$1.enum(["vertical", "horizontal"]),
|
|
2661
|
+
promptHtml: z$1.string(),
|
|
2662
|
+
choices: z$1.array(SimpleChoiceSchema).min(1)
|
|
2663
|
+
});
|
|
2664
|
+
var OrderInteractionSchema = OrderInteractionCoreSchema.extend({
|
|
2665
|
+
type: z$1.literal("orderInteraction")
|
|
2666
|
+
});
|
|
2667
|
+
var GapTextSchema = z$1.object({
|
|
2668
|
+
identifier: z$1.string().min(1),
|
|
2669
|
+
contentHtml: z$1.string(),
|
|
2670
|
+
matchMax: z$1.number().int().min(0)
|
|
2671
|
+
// 0 = unlimited
|
|
2672
|
+
});
|
|
2673
|
+
var GapSchema = z$1.object({
|
|
2674
|
+
identifier: z$1.string().min(1)
|
|
2675
|
+
});
|
|
2676
|
+
var GapMatchInteractionCoreSchema = z$1.object({
|
|
2677
|
+
responseIdentifier: z$1.string().min(1),
|
|
2678
|
+
shuffle: z$1.boolean(),
|
|
2679
|
+
gapTexts: z$1.array(GapTextSchema).min(1),
|
|
2680
|
+
// Draggable source tokens
|
|
2681
|
+
gaps: z$1.array(GapSchema).min(1),
|
|
2682
|
+
// Drop target placeholders
|
|
2683
|
+
contentHtml: z$1.string()
|
|
2684
|
+
// HTML content with gap placeholders
|
|
2685
|
+
});
|
|
2686
|
+
var GapMatchInteractionSchema = GapMatchInteractionCoreSchema.extend({
|
|
2687
|
+
type: z$1.literal("gapMatchInteraction")
|
|
2688
|
+
});
|
|
2689
|
+
var AssociableChoiceSchema = z$1.object({
|
|
2690
|
+
identifier: z$1.string().min(1),
|
|
2691
|
+
matchMax: z$1.number().int().min(0),
|
|
2692
|
+
// 0 = unlimited uses
|
|
2693
|
+
contentHtml: z$1.string()
|
|
2694
|
+
});
|
|
2695
|
+
var MatchInteractionCoreSchema = z$1.object({
|
|
2696
|
+
responseIdentifier: z$1.string().min(1),
|
|
2697
|
+
shuffle: z$1.boolean(),
|
|
2698
|
+
maxAssociations: z$1.number().int().min(0),
|
|
2699
|
+
// 0 = unlimited total associations
|
|
2700
|
+
sourceChoices: z$1.array(AssociableChoiceSchema).min(1),
|
|
2701
|
+
// First <qti-simple-match-set>
|
|
2702
|
+
targetChoices: z$1.array(AssociableChoiceSchema).min(1),
|
|
2703
|
+
// Second <qti-simple-match-set>
|
|
2704
|
+
promptHtml: z$1.string()
|
|
2705
|
+
});
|
|
2706
|
+
var MatchInteractionSchema = MatchInteractionCoreSchema.extend({
|
|
2707
|
+
type: z$1.literal("matchInteraction")
|
|
2708
|
+
});
|
|
2709
|
+
var AnyInteractionSchema = z$1.discriminatedUnion("type", [
|
|
2710
|
+
ChoiceInteractionSchema,
|
|
2711
|
+
InlineChoiceInteractionSchema,
|
|
2712
|
+
TextEntryInteractionSchema,
|
|
2713
|
+
OrderInteractionSchema,
|
|
2714
|
+
GapMatchInteractionSchema,
|
|
2715
|
+
MatchInteractionSchema
|
|
2716
|
+
]);
|
|
2717
|
+
var CorrectResponseSchema = z$1.object({
|
|
2718
|
+
values: z$1.array(z$1.string().min(1)).min(1)
|
|
2719
|
+
});
|
|
2720
|
+
var ResponseDeclarationSchema = z$1.object({
|
|
2721
|
+
identifier: z$1.string().min(1),
|
|
2722
|
+
cardinality: QtiCardinalitySchema,
|
|
2723
|
+
baseType: QtiBaseTypeSchema,
|
|
2724
|
+
correctResponse: CorrectResponseSchema,
|
|
2725
|
+
// Optional response mapping for map-response processing (used for summed feedback/score)
|
|
2726
|
+
mapping: z$1.object({
|
|
2727
|
+
defaultValue: z$1.number().default(0),
|
|
2728
|
+
lowerBound: z$1.number().optional(),
|
|
2729
|
+
upperBound: z$1.number().optional(),
|
|
2730
|
+
entries: z$1.array(
|
|
2731
|
+
z$1.object({
|
|
2732
|
+
key: z$1.string().min(1),
|
|
2733
|
+
value: z$1.number()
|
|
2734
|
+
})
|
|
2735
|
+
)
|
|
2736
|
+
}).optional()
|
|
2737
|
+
});
|
|
2738
|
+
var OutcomeDefaultValueSchema = z$1.object({
|
|
2739
|
+
value: z$1.string()
|
|
2740
|
+
});
|
|
2741
|
+
var OutcomeDeclarationSchema = z$1.object({
|
|
2742
|
+
identifier: z$1.string().min(1),
|
|
2743
|
+
cardinality: QtiCardinalitySchema,
|
|
2744
|
+
baseType: QtiBaseTypeSchema,
|
|
2745
|
+
defaultValue: OutcomeDefaultValueSchema.optional()
|
|
2746
|
+
});
|
|
2747
|
+
var FeedbackBlockSchema = z$1.object({
|
|
2748
|
+
outcomeIdentifier: z$1.string().min(1),
|
|
2749
|
+
identifier: z$1.string().min(1),
|
|
2750
|
+
showHide: z$1.enum(["show", "hide"]).default("show"),
|
|
2751
|
+
contentHtml: z$1.string()
|
|
2752
|
+
});
|
|
2753
|
+
var StimulusBlockSchema = z$1.object({
|
|
2754
|
+
type: z$1.literal("stimulus"),
|
|
2755
|
+
html: z$1.string()
|
|
2756
|
+
});
|
|
2757
|
+
var RichStimulusBlockSchema = z$1.object({
|
|
2758
|
+
type: z$1.literal("richStimulus"),
|
|
2759
|
+
html: z$1.string(),
|
|
2760
|
+
inlineEmbeds: z$1.record(z$1.string(), InlineChoiceInteractionSchema),
|
|
2761
|
+
textEmbeds: z$1.record(z$1.string(), TextEntryInteractionSchema)
|
|
2762
|
+
});
|
|
2763
|
+
var InteractionBlockSchema = z$1.object({
|
|
2764
|
+
type: z$1.literal("interaction"),
|
|
2765
|
+
interaction: AnyInteractionSchema
|
|
2766
|
+
});
|
|
2767
|
+
var ItemBodySchema = z$1.object({
|
|
2768
|
+
contentBlocks: z$1.array(z$1.union([StimulusBlockSchema, RichStimulusBlockSchema, InteractionBlockSchema])).min(1),
|
|
2769
|
+
feedbackBlocks: z$1.array(FeedbackBlockSchema)
|
|
2770
|
+
});
|
|
2771
|
+
var BaseRuleSchema = z$1.object({
|
|
2772
|
+
type: z$1.literal("setOutcomeValue"),
|
|
2773
|
+
identifier: z$1.string(),
|
|
2774
|
+
value: z$1.string()
|
|
2775
|
+
});
|
|
2776
|
+
var MatchConditionSchema = z$1.object({
|
|
2777
|
+
type: z$1.literal("match"),
|
|
2778
|
+
variable: z$1.string(),
|
|
2779
|
+
correct: z$1.literal(true)
|
|
2780
|
+
// Match against correct response
|
|
2781
|
+
});
|
|
2782
|
+
var MatchValueConditionSchema = z$1.object({
|
|
2783
|
+
type: z$1.literal("matchValue"),
|
|
2784
|
+
variable: z$1.string(),
|
|
2785
|
+
// The response identifier from <qti-variable>
|
|
2786
|
+
value: z$1.string()
|
|
2787
|
+
// The target value from <qti-base-value>
|
|
2788
|
+
});
|
|
2789
|
+
var StringMatchConditionSchema = z$1.object({
|
|
2790
|
+
type: z$1.literal("stringMatch"),
|
|
2791
|
+
variable: z$1.string(),
|
|
2792
|
+
value: z$1.string(),
|
|
2793
|
+
caseSensitive: z$1.boolean()
|
|
2794
|
+
});
|
|
2795
|
+
var MemberConditionSchema = z$1.object({
|
|
2796
|
+
type: z$1.literal("member"),
|
|
2797
|
+
variable: z$1.string(),
|
|
2798
|
+
value: z$1.string()
|
|
2799
|
+
});
|
|
2800
|
+
var EqualMapResponseConditionSchema = z$1.object({
|
|
2801
|
+
type: z$1.literal("equalMapResponse"),
|
|
2802
|
+
variable: z$1.string(),
|
|
2803
|
+
// The response identifier from <qti-map-response>
|
|
2804
|
+
value: z$1.string()
|
|
2805
|
+
// The target sum value from <qti-base-value>
|
|
2806
|
+
});
|
|
2807
|
+
var AndConditionSchema = z$1.object({
|
|
2808
|
+
type: z$1.literal("and"),
|
|
2809
|
+
conditions: z$1.array(z$1.lazy(() => AnyConditionSchema))
|
|
2810
|
+
});
|
|
2811
|
+
var OrConditionSchema = z$1.object({
|
|
2812
|
+
type: z$1.literal("or"),
|
|
2813
|
+
conditions: z$1.array(z$1.lazy(() => AnyConditionSchema))
|
|
2814
|
+
});
|
|
2815
|
+
var NotConditionSchema = z$1.object({
|
|
2816
|
+
type: z$1.literal("not"),
|
|
2817
|
+
condition: z$1.lazy(() => AnyConditionSchema)
|
|
2818
|
+
});
|
|
2819
|
+
var AnyConditionSchema = z$1.union([
|
|
2820
|
+
MatchConditionSchema,
|
|
2821
|
+
MatchValueConditionSchema,
|
|
2822
|
+
StringMatchConditionSchema,
|
|
2823
|
+
MemberConditionSchema,
|
|
2824
|
+
z$1.lazy(() => AndConditionSchema),
|
|
2825
|
+
z$1.lazy(() => OrConditionSchema),
|
|
2826
|
+
z$1.lazy(() => NotConditionSchema),
|
|
2827
|
+
EqualMapResponseConditionSchema
|
|
2828
|
+
]);
|
|
2829
|
+
var ConditionBranchSchema = z$1.object({
|
|
2830
|
+
condition: AnyConditionSchema.optional(),
|
|
2831
|
+
actions: z$1.array(BaseRuleSchema),
|
|
2832
|
+
nestedRules: z$1.array(z$1.lazy(() => ResponseRuleSchema)).optional()
|
|
2833
|
+
});
|
|
2834
|
+
var ResponseRuleSchema = z$1.union([
|
|
2835
|
+
z$1.object({
|
|
2836
|
+
type: z$1.literal("condition"),
|
|
2837
|
+
branches: z$1.array(ConditionBranchSchema)
|
|
2838
|
+
}),
|
|
2839
|
+
z$1.object({
|
|
2840
|
+
type: z$1.literal("action"),
|
|
2841
|
+
action: BaseRuleSchema
|
|
2842
|
+
})
|
|
2843
|
+
]);
|
|
2844
|
+
var ScoringRuleSchema = z$1.object({
|
|
2845
|
+
responseIdentifier: z$1.string().min(1),
|
|
2846
|
+
correctScore: z$1.number(),
|
|
2847
|
+
incorrectScore: z$1.number()
|
|
2848
|
+
});
|
|
2849
|
+
var ResponseProcessingSchema = z$1.object({
|
|
2850
|
+
rules: z$1.array(ResponseRuleSchema),
|
|
2851
|
+
scoring: ScoringRuleSchema
|
|
2852
|
+
});
|
|
2853
|
+
var AssessmentItemSchema = z$1.object({
|
|
2854
|
+
identifier: z$1.string().min(1),
|
|
2855
|
+
title: z$1.string().min(1),
|
|
2856
|
+
timeDependent: z$1.boolean(),
|
|
2857
|
+
xmlLang: z$1.string().min(1),
|
|
2858
|
+
responseDeclarations: z$1.array(ResponseDeclarationSchema).min(1),
|
|
2859
|
+
outcomeDeclarations: z$1.array(OutcomeDeclarationSchema).min(1),
|
|
2860
|
+
itemBody: ItemBodySchema,
|
|
2861
|
+
responseProcessing: ResponseProcessingSchema
|
|
2862
|
+
});
|
|
2863
|
+
|
|
2864
|
+
// src/parser.ts
|
|
2865
|
+
function createXmlParser() {
|
|
2866
|
+
return new XMLParser({
|
|
2867
|
+
ignoreAttributes: false,
|
|
2868
|
+
attributeNamePrefix: "",
|
|
2869
|
+
allowBooleanAttributes: true,
|
|
2870
|
+
parseAttributeValue: true,
|
|
2871
|
+
parseTagValue: false,
|
|
2872
|
+
// CRITICAL: Preserve "000" as string, not convert to number 0
|
|
2873
|
+
trimValues: false,
|
|
2874
|
+
preserveOrder: true
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
function coerceString(value) {
|
|
2878
|
+
if (value == null) return "";
|
|
2879
|
+
return String(value);
|
|
2880
|
+
}
|
|
2881
|
+
function isRecord(value) {
|
|
2882
|
+
return typeof value === "object" && value !== null;
|
|
2883
|
+
}
|
|
2884
|
+
function normalizeNode(rawNode) {
|
|
2885
|
+
if (rawNode == null) return "";
|
|
2886
|
+
if (!isRecord(rawNode)) return coerceString(rawNode);
|
|
2887
|
+
const textContent = rawNode["#text"];
|
|
2888
|
+
if (textContent != null) {
|
|
2889
|
+
return coerceString(textContent);
|
|
2890
|
+
}
|
|
2891
|
+
const tagName = Object.keys(rawNode).find((k2) => k2 !== ":@");
|
|
2892
|
+
if (!tagName) return "";
|
|
2893
|
+
const attrsValue = rawNode[":@"];
|
|
2894
|
+
const attrs = isRecord(attrsValue) ? attrsValue : {};
|
|
2895
|
+
const rawChildren = Array.isArray(rawNode[tagName]) ? rawNode[tagName] : [rawNode[tagName]];
|
|
2896
|
+
return {
|
|
2897
|
+
tagName,
|
|
2898
|
+
attrs,
|
|
2899
|
+
children: rawChildren.map(normalizeNode).filter((n) => n !== "")
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
function nodeToXml(node) {
|
|
2903
|
+
if (typeof node === "string") return node;
|
|
2904
|
+
return serializeNode(node);
|
|
2905
|
+
}
|
|
2906
|
+
function getInnerHtml(node) {
|
|
2907
|
+
return serializeInner(node);
|
|
2908
|
+
}
|
|
2909
|
+
function findChild(node, tagName) {
|
|
2910
|
+
const child = node.children.find((c) => typeof c !== "string" && c.tagName === tagName);
|
|
2911
|
+
return typeof child === "string" ? void 0 : child;
|
|
2912
|
+
}
|
|
2913
|
+
function findChildren(node, tagName) {
|
|
2914
|
+
return node.children.filter((c) => typeof c !== "string" && c.tagName === tagName);
|
|
2915
|
+
}
|
|
2916
|
+
function getTextContent(node) {
|
|
2917
|
+
if (!node) return "";
|
|
2918
|
+
const firstChild = node.children[0];
|
|
2919
|
+
if (typeof firstChild === "string") return firstChild.trim();
|
|
2920
|
+
return "";
|
|
2921
|
+
}
|
|
2922
|
+
function findInteractionNodes(node, out = []) {
|
|
2923
|
+
if (node.tagName.includes("-interaction")) {
|
|
2924
|
+
out.push(node);
|
|
2925
|
+
}
|
|
2926
|
+
for (const child of node.children) {
|
|
2927
|
+
if (typeof child !== "string") {
|
|
2928
|
+
findInteractionNodes(child, out);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
return out;
|
|
2932
|
+
}
|
|
2933
|
+
function escapeAttr(value) {
|
|
2934
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
2935
|
+
}
|
|
2936
|
+
function attrsToString(attrs) {
|
|
2937
|
+
const parts = [];
|
|
2938
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
2939
|
+
const str = coerceString(val);
|
|
2940
|
+
if (str === "") continue;
|
|
2941
|
+
parts.push(`${key}="${escapeAttr(str)}"`);
|
|
2942
|
+
}
|
|
2943
|
+
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
2944
|
+
}
|
|
2945
|
+
function serializeWithInlinePlaceholders(node, inlineEmbeds, textEmbeds) {
|
|
2946
|
+
if (typeof node === "string") return node;
|
|
2947
|
+
if (node.tagName === "qti-inline-choice-interaction") {
|
|
2948
|
+
const interaction = extractInlineChoiceInteraction(node);
|
|
2949
|
+
inlineEmbeds[interaction.responseIdentifier] = interaction;
|
|
2950
|
+
return `<span data-qti-inline="${interaction.responseIdentifier}"></span>`;
|
|
2951
|
+
}
|
|
2952
|
+
if (node.tagName === "qti-text-entry-interaction") {
|
|
2953
|
+
const interaction = extractTextEntryInteraction(node);
|
|
2954
|
+
textEmbeds[interaction.responseIdentifier] = interaction;
|
|
2955
|
+
return `<span data-qti-text-entry="${interaction.responseIdentifier}"></span>`;
|
|
2956
|
+
}
|
|
2957
|
+
const open = `<${node.tagName}${attrsToString(node.attrs)}>`;
|
|
2958
|
+
const childrenHtml = node.children.map((c) => serializeWithInlinePlaceholders(c, inlineEmbeds, textEmbeds)).join("");
|
|
2959
|
+
const close = `</${node.tagName}>`;
|
|
2960
|
+
return `${open}${childrenHtml}${close}`;
|
|
2961
|
+
}
|
|
2962
|
+
function extractChoiceInteraction(node) {
|
|
2963
|
+
const promptNode = findChild(node, "qti-prompt");
|
|
2964
|
+
const promptHtml = promptNode ? getInnerHtml(promptNode) : "";
|
|
2965
|
+
const choiceNodes = findChildren(node, "qti-simple-choice");
|
|
2966
|
+
const choices = choiceNodes.map((choice) => {
|
|
2967
|
+
const inlineFeedbackNode = findChild(choice, "qti-feedback-inline");
|
|
2968
|
+
const inlineFeedbackHtml = inlineFeedbackNode ? getInnerHtml(inlineFeedbackNode) : void 0;
|
|
2969
|
+
const contentChildren = choice.children.filter(
|
|
2970
|
+
(c) => !(typeof c !== "string" && c.tagName === "qti-feedback-inline")
|
|
2971
|
+
);
|
|
2972
|
+
const contentHtml = serializeNodes(contentChildren);
|
|
2973
|
+
return {
|
|
2974
|
+
identifier: coerceString(choice.attrs.identifier),
|
|
2975
|
+
contentHtml,
|
|
2976
|
+
inlineFeedbackHtml
|
|
2977
|
+
};
|
|
2978
|
+
});
|
|
2979
|
+
return {
|
|
2980
|
+
type: "choiceInteraction",
|
|
2981
|
+
responseIdentifier: coerceString(node.attrs["response-identifier"]),
|
|
2982
|
+
shuffle: Boolean(node.attrs.shuffle),
|
|
2983
|
+
minChoices: Number(node.attrs["min-choices"] ?? 0),
|
|
2984
|
+
maxChoices: Number(node.attrs["max-choices"] ?? 1),
|
|
2985
|
+
promptHtml,
|
|
2986
|
+
choices
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
function extractInlineChoiceInteraction(node) {
|
|
2990
|
+
const choiceNodes = findChildren(node, "qti-inline-choice");
|
|
2991
|
+
const choices = choiceNodes.map((choice) => {
|
|
2992
|
+
const contentHtml = serializeInner(choice);
|
|
2993
|
+
return {
|
|
2994
|
+
identifier: coerceString(choice.attrs.identifier),
|
|
2995
|
+
contentHtml
|
|
2996
|
+
};
|
|
2997
|
+
});
|
|
2998
|
+
return {
|
|
2999
|
+
type: "inlineChoiceInteraction",
|
|
3000
|
+
responseIdentifier: coerceString(node.attrs["response-identifier"]),
|
|
3001
|
+
shuffle: Boolean(node.attrs.shuffle),
|
|
3002
|
+
choices
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
function extractTextEntryInteraction(node) {
|
|
3006
|
+
const expectedLengthAttr = node.attrs["expected-length"];
|
|
3007
|
+
const expectedLength = expectedLengthAttr ? Number(expectedLengthAttr) : void 0;
|
|
3008
|
+
return {
|
|
3009
|
+
type: "textEntryInteraction",
|
|
3010
|
+
responseIdentifier: coerceString(node.attrs["response-identifier"]),
|
|
3011
|
+
expectedLength: expectedLength !== void 0 && !Number.isNaN(expectedLength) ? expectedLength : void 0,
|
|
3012
|
+
placeholderText: coerceString(node.attrs["placeholder-text"]) || void 0,
|
|
3013
|
+
patternMask: coerceString(node.attrs["pattern-mask"]) || void 0
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
function extractOrderInteraction(node) {
|
|
3017
|
+
const promptNode = findChild(node, "qti-prompt");
|
|
3018
|
+
const promptHtml = promptNode ? getInnerHtml(promptNode) : "";
|
|
3019
|
+
const choiceNodes = findChildren(node, "qti-simple-choice");
|
|
3020
|
+
const choices = choiceNodes.map((choice) => {
|
|
3021
|
+
const contentChildren = choice.children.filter(
|
|
3022
|
+
(c) => !(typeof c !== "string" && c.tagName === "qti-feedback-inline")
|
|
3023
|
+
);
|
|
3024
|
+
const contentHtml = serializeNodes(contentChildren);
|
|
3025
|
+
return {
|
|
3026
|
+
identifier: coerceString(choice.attrs.identifier),
|
|
3027
|
+
contentHtml,
|
|
3028
|
+
// Order interaction rarely displays inline feedback per-item
|
|
3029
|
+
inlineFeedbackHtml: void 0
|
|
3030
|
+
};
|
|
3031
|
+
});
|
|
3032
|
+
let orientation = "vertical";
|
|
3033
|
+
const attrOrientation = coerceString(node.attrs.orientation);
|
|
3034
|
+
const classAttr = coerceString(node.attrs.class);
|
|
3035
|
+
if (attrOrientation === "horizontal" || classAttr.includes("qti-orientation-horizontal")) {
|
|
3036
|
+
orientation = "horizontal";
|
|
3037
|
+
}
|
|
3038
|
+
return {
|
|
3039
|
+
type: "orderInteraction",
|
|
3040
|
+
responseIdentifier: coerceString(node.attrs["response-identifier"]),
|
|
3041
|
+
shuffle: Boolean(node.attrs.shuffle),
|
|
3042
|
+
minChoices: Number(node.attrs["min-choices"] ?? 0),
|
|
3043
|
+
maxChoices: Number(node.attrs["max-choices"] ?? 0) || void 0,
|
|
3044
|
+
orientation,
|
|
3045
|
+
promptHtml,
|
|
3046
|
+
choices
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
function extractGapMatchInteraction(node) {
|
|
3050
|
+
const gapTextNodes = findChildren(node, "qti-gap-text");
|
|
3051
|
+
const gapTexts = gapTextNodes.map((gt) => ({
|
|
3052
|
+
identifier: coerceString(gt.attrs.identifier),
|
|
3053
|
+
contentHtml: serializeInner(gt),
|
|
3054
|
+
matchMax: Number(gt.attrs["match-max"] ?? 0)
|
|
3055
|
+
// 0 = unlimited
|
|
3056
|
+
}));
|
|
3057
|
+
const gaps = [];
|
|
3058
|
+
function serializeWithGapPlaceholders(child) {
|
|
3059
|
+
if (typeof child === "string") return child;
|
|
3060
|
+
if (child.tagName === "qti-gap") {
|
|
3061
|
+
const gapId = coerceString(child.attrs.identifier);
|
|
3062
|
+
gaps.push({ identifier: gapId });
|
|
3063
|
+
return `<span data-qti-gap="${gapId}"></span>`;
|
|
3064
|
+
}
|
|
3065
|
+
if (child.tagName === "qti-gap-text") {
|
|
3066
|
+
return "";
|
|
3067
|
+
}
|
|
3068
|
+
const open = `<${child.tagName}${attrsToString(child.attrs)}>`;
|
|
3069
|
+
const childrenHtml = child.children.map((c) => serializeWithGapPlaceholders(c)).join("");
|
|
3070
|
+
const close = `</${child.tagName}>`;
|
|
3071
|
+
return `${open}${childrenHtml}${close}`;
|
|
3072
|
+
}
|
|
3073
|
+
const contentHtml = node.children.filter((c) => typeof c === "string" || c.tagName !== "qti-gap-text").map((c) => serializeWithGapPlaceholders(c)).join("");
|
|
3074
|
+
return {
|
|
3075
|
+
type: "gapMatchInteraction",
|
|
3076
|
+
responseIdentifier: coerceString(node.attrs["response-identifier"]),
|
|
3077
|
+
shuffle: Boolean(node.attrs.shuffle),
|
|
3078
|
+
gapTexts,
|
|
3079
|
+
gaps,
|
|
3080
|
+
contentHtml
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
function extractMatchInteraction(node) {
|
|
3084
|
+
const promptNode = findChild(node, "qti-prompt");
|
|
3085
|
+
const promptHtml = promptNode ? getInnerHtml(promptNode) : "";
|
|
3086
|
+
const matchSets = findChildren(node, "qti-simple-match-set");
|
|
3087
|
+
const extractChoices = (setNode) => {
|
|
3088
|
+
const choiceNodes = findChildren(setNode, "qti-simple-associable-choice");
|
|
3089
|
+
return choiceNodes.map((c) => ({
|
|
3090
|
+
identifier: coerceString(c.attrs.identifier),
|
|
3091
|
+
matchMax: Number(c.attrs["match-max"]) || 1,
|
|
3092
|
+
contentHtml: serializeInner(c)
|
|
3093
|
+
}));
|
|
3094
|
+
};
|
|
3095
|
+
const firstSet = matchSets[0];
|
|
3096
|
+
const secondSet = matchSets[1];
|
|
3097
|
+
const sourceChoices = firstSet ? extractChoices(firstSet) : [];
|
|
3098
|
+
const targetChoices = secondSet ? extractChoices(secondSet) : [];
|
|
3099
|
+
return {
|
|
3100
|
+
type: "matchInteraction",
|
|
3101
|
+
responseIdentifier: coerceString(node.attrs["response-identifier"]),
|
|
3102
|
+
shuffle: node.attrs.shuffle === "true",
|
|
3103
|
+
maxAssociations: Number(node.attrs["max-associations"]) || 0,
|
|
3104
|
+
sourceChoices,
|
|
3105
|
+
targetChoices,
|
|
3106
|
+
promptHtml
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
function extractFeedbackBlock(node) {
|
|
3110
|
+
const contentBodyNode = findChild(node, "qti-content-body");
|
|
3111
|
+
const contentHtml = contentBodyNode ? getInnerHtml(contentBodyNode) : "";
|
|
3112
|
+
const showHide = coerceString(node.attrs["show-hide"]) === "hide" ? "hide" : "show";
|
|
3113
|
+
return {
|
|
3114
|
+
outcomeIdentifier: coerceString(node.attrs["outcome-identifier"]),
|
|
3115
|
+
identifier: coerceString(node.attrs.identifier),
|
|
3116
|
+
showHide,
|
|
3117
|
+
contentHtml
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
function extractResponseDeclarations(rootChildren) {
|
|
3121
|
+
const declNodes = rootChildren.filter((n) => n.tagName === "qti-response-declaration");
|
|
3122
|
+
return declNodes.map((node) => {
|
|
3123
|
+
const correctResponseNode = findChild(node, "qti-correct-response");
|
|
3124
|
+
const valueNodes = correctResponseNode ? findChildren(correctResponseNode, "qti-value") : [];
|
|
3125
|
+
const values = valueNodes.map(getTextContent);
|
|
3126
|
+
const mappingNode = findChild(node, "qti-mapping");
|
|
3127
|
+
let mapping;
|
|
3128
|
+
if (mappingNode) {
|
|
3129
|
+
const defaultValueRaw = coerceString(mappingNode.attrs["default-value"]);
|
|
3130
|
+
const lowerBoundRaw = coerceString(mappingNode.attrs["lower-bound"]);
|
|
3131
|
+
const upperBoundRaw = coerceString(mappingNode.attrs["upper-bound"]);
|
|
3132
|
+
const entryNodes = findChildren(mappingNode, "qti-map-entry");
|
|
3133
|
+
const entries = entryNodes.map((en) => {
|
|
3134
|
+
return {
|
|
3135
|
+
key: coerceString(en.attrs["map-key"]),
|
|
3136
|
+
value: Number(coerceString(en.attrs["mapped-value"]) || 0)
|
|
3137
|
+
};
|
|
3138
|
+
});
|
|
3139
|
+
mapping = {
|
|
3140
|
+
defaultValue: Number(defaultValueRaw || 0),
|
|
3141
|
+
lowerBound: lowerBoundRaw !== "" ? Number(lowerBoundRaw) : void 0,
|
|
3142
|
+
upperBound: upperBoundRaw !== "" ? Number(upperBoundRaw) : void 0,
|
|
3143
|
+
entries
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
const cardinalityRaw = coerceString(node.attrs.cardinality);
|
|
3147
|
+
const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
|
|
3148
|
+
if (!cardinalityResult.success) {
|
|
3149
|
+
throw A(`invalid cardinality '${cardinalityRaw}' in response declaration`);
|
|
3150
|
+
}
|
|
3151
|
+
const baseTypeRaw = coerceString(node.attrs["base-type"]);
|
|
3152
|
+
const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
|
|
3153
|
+
if (!baseTypeResult.success) {
|
|
3154
|
+
throw A(`invalid base-type '${baseTypeRaw}' in response declaration`);
|
|
3155
|
+
}
|
|
3156
|
+
return {
|
|
3157
|
+
identifier: coerceString(node.attrs.identifier),
|
|
3158
|
+
cardinality: cardinalityResult.data,
|
|
3159
|
+
baseType: baseTypeResult.data,
|
|
3160
|
+
correctResponse: { values },
|
|
3161
|
+
mapping
|
|
3162
|
+
};
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
function extractOutcomeDeclarations(rootChildren) {
|
|
3166
|
+
const declNodes = rootChildren.filter((n) => n.tagName === "qti-outcome-declaration");
|
|
3167
|
+
return declNodes.map((node) => {
|
|
3168
|
+
const defaultValueNode = findChild(node, "qti-default-value");
|
|
3169
|
+
const valueNode = defaultValueNode ? findChild(defaultValueNode, "qti-value") : void 0;
|
|
3170
|
+
const defaultValue = valueNode ? { value: getTextContent(valueNode) } : void 0;
|
|
3171
|
+
const cardinalityRaw = coerceString(node.attrs.cardinality);
|
|
3172
|
+
const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
|
|
3173
|
+
if (!cardinalityResult.success) {
|
|
3174
|
+
throw A(`invalid cardinality '${cardinalityRaw}' in outcome declaration`);
|
|
3175
|
+
}
|
|
3176
|
+
const baseTypeRaw = coerceString(node.attrs["base-type"]);
|
|
3177
|
+
const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
|
|
3178
|
+
if (!baseTypeResult.success) {
|
|
3179
|
+
throw A(`invalid base-type '${baseTypeRaw}' in outcome declaration`);
|
|
3180
|
+
}
|
|
3181
|
+
return {
|
|
3182
|
+
identifier: coerceString(node.attrs.identifier),
|
|
3183
|
+
cardinality: cardinalityResult.data,
|
|
3184
|
+
baseType: baseTypeResult.data,
|
|
3185
|
+
defaultValue
|
|
3186
|
+
};
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
function parseCondition(node) {
|
|
3190
|
+
if (node.tagName === "qti-and") {
|
|
3191
|
+
const conditions = [];
|
|
3192
|
+
for (const child of node.children) {
|
|
3193
|
+
if (typeof child !== "string") {
|
|
3194
|
+
const parsed = parseCondition(child);
|
|
3195
|
+
if (parsed) conditions.push(parsed);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
if (conditions.length === 0) return null;
|
|
3199
|
+
return { type: "and", conditions };
|
|
3200
|
+
}
|
|
3201
|
+
if (node.tagName === "qti-or") {
|
|
3202
|
+
const conditions = [];
|
|
3203
|
+
for (const child of node.children) {
|
|
3204
|
+
if (typeof child !== "string") {
|
|
3205
|
+
const parsed = parseCondition(child);
|
|
3206
|
+
if (parsed) conditions.push(parsed);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
if (conditions.length === 0) return null;
|
|
3210
|
+
return { type: "or", conditions };
|
|
3211
|
+
}
|
|
3212
|
+
if (node.tagName === "qti-not") {
|
|
3213
|
+
for (const child of node.children) {
|
|
3214
|
+
if (typeof child !== "string") {
|
|
3215
|
+
const parsed = parseCondition(child);
|
|
3216
|
+
if (parsed) return { type: "not", condition: parsed };
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
return null;
|
|
3220
|
+
}
|
|
3221
|
+
const variableNode = findChild(node, "qti-variable");
|
|
3222
|
+
const baseValueNode = findChild(node, "qti-base-value");
|
|
3223
|
+
const identifier = variableNode ? coerceString(variableNode.attrs.identifier) : void 0;
|
|
3224
|
+
const value = baseValueNode ? getTextContent(baseValueNode) : void 0;
|
|
3225
|
+
if (node.tagName === "qti-match") {
|
|
3226
|
+
if (findChild(node, "qti-correct") && identifier) {
|
|
3227
|
+
return { type: "match", variable: identifier, correct: true };
|
|
3228
|
+
}
|
|
3229
|
+
if (identifier && value !== void 0) {
|
|
3230
|
+
return { type: "matchValue", variable: identifier, value };
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
if (node.tagName === "qti-string-match") {
|
|
3234
|
+
if (identifier && value !== void 0) {
|
|
3235
|
+
return {
|
|
3236
|
+
type: "stringMatch",
|
|
3237
|
+
variable: identifier,
|
|
3238
|
+
value,
|
|
3239
|
+
// QTI spec: case-sensitive defaults to true
|
|
3240
|
+
caseSensitive: node.attrs["case-sensitive"] !== "false"
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
if (node.tagName === "qti-member") {
|
|
3245
|
+
if (identifier && value !== void 0) {
|
|
3246
|
+
return { type: "member", variable: identifier, value };
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (node.tagName === "qti-equal") {
|
|
3250
|
+
const mapResponseNode = findChild(node, "qti-map-response");
|
|
3251
|
+
const equalBaseValueNode = findChild(node, "qti-base-value");
|
|
3252
|
+
if (mapResponseNode && equalBaseValueNode) {
|
|
3253
|
+
return {
|
|
3254
|
+
type: "equalMapResponse",
|
|
3255
|
+
variable: coerceString(mapResponseNode.attrs.identifier),
|
|
3256
|
+
value: getTextContent(equalBaseValueNode)
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
if (findChild(node, "qti-correct") && identifier) {
|
|
3260
|
+
return { type: "match", variable: identifier, correct: true };
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
return null;
|
|
3264
|
+
}
|
|
3265
|
+
function parseActions(node) {
|
|
3266
|
+
const rules = [];
|
|
3267
|
+
const setNodes = findChildren(node, "qti-set-outcome-value");
|
|
3268
|
+
for (const setNode of setNodes) {
|
|
3269
|
+
const valueNode = findChild(setNode, "qti-base-value");
|
|
3270
|
+
if (valueNode) {
|
|
3271
|
+
rules.push({
|
|
3272
|
+
type: "setOutcomeValue",
|
|
3273
|
+
identifier: coerceString(setNode.attrs.identifier),
|
|
3274
|
+
value: getTextContent(valueNode)
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
return rules;
|
|
3279
|
+
}
|
|
3280
|
+
function parseResponseRule(node) {
|
|
3281
|
+
if (node.tagName === "qti-set-outcome-value") {
|
|
3282
|
+
const valueNode = findChild(node, "qti-base-value");
|
|
3283
|
+
if (valueNode) {
|
|
3284
|
+
return {
|
|
3285
|
+
type: "action",
|
|
3286
|
+
action: {
|
|
3287
|
+
type: "setOutcomeValue",
|
|
3288
|
+
identifier: coerceString(node.attrs.identifier),
|
|
3289
|
+
value: getTextContent(valueNode)
|
|
3290
|
+
}
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
return null;
|
|
3294
|
+
}
|
|
3295
|
+
if (node.tagName === "qti-response-condition") {
|
|
3296
|
+
const branches = [];
|
|
3297
|
+
const ifNode = findChild(node, "qti-response-if");
|
|
3298
|
+
if (ifNode) {
|
|
3299
|
+
const condition = findConditionInBranch(ifNode);
|
|
3300
|
+
if (condition) {
|
|
3301
|
+
branches.push({
|
|
3302
|
+
condition,
|
|
3303
|
+
actions: parseActions(ifNode),
|
|
3304
|
+
nestedRules: findNestedRules(ifNode)
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
const elseIfNodes = findChildren(node, "qti-response-else-if");
|
|
3309
|
+
for (const elseIfNode of elseIfNodes) {
|
|
3310
|
+
const condition = findConditionInBranch(elseIfNode);
|
|
3311
|
+
if (condition) {
|
|
3312
|
+
branches.push({
|
|
3313
|
+
condition,
|
|
3314
|
+
actions: parseActions(elseIfNode),
|
|
3315
|
+
nestedRules: findNestedRules(elseIfNode)
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
const elseNode = findChild(node, "qti-response-else");
|
|
3320
|
+
if (elseNode) {
|
|
3321
|
+
branches.push({
|
|
3322
|
+
condition: void 0,
|
|
3323
|
+
// Else has no condition
|
|
3324
|
+
actions: parseActions(elseNode),
|
|
3325
|
+
nestedRules: findNestedRules(elseNode)
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
if (branches.length > 0) {
|
|
3329
|
+
return {
|
|
3330
|
+
type: "condition",
|
|
3331
|
+
branches
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return null;
|
|
3336
|
+
}
|
|
3337
|
+
function findConditionInBranch(node) {
|
|
3338
|
+
const conditionTagNames = ["qti-match", "qti-and", "qti-or", "qti-not", "qti-equal", "qti-string-match", "qti-member"];
|
|
3339
|
+
for (const child of node.children) {
|
|
3340
|
+
if (typeof child !== "string" && conditionTagNames.includes(child.tagName)) {
|
|
3341
|
+
return parseCondition(child);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
return null;
|
|
3345
|
+
}
|
|
3346
|
+
function findNestedRules(node) {
|
|
3347
|
+
const nested = [];
|
|
3348
|
+
for (const child of node.children) {
|
|
3349
|
+
if (typeof child !== "string" && child.tagName === "qti-response-condition") {
|
|
3350
|
+
const rule = parseResponseRule(child);
|
|
3351
|
+
if (rule) nested.push(rule);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
return nested;
|
|
3355
|
+
}
|
|
3356
|
+
function extractResponseProcessing(rootChildren) {
|
|
3357
|
+
const processingNode = rootChildren.find((n) => n.tagName === "qti-response-processing");
|
|
3358
|
+
if (!processingNode) {
|
|
3359
|
+
return { rules: [], scoring: { responseIdentifier: "", correctScore: 1, incorrectScore: 0 } };
|
|
3360
|
+
}
|
|
3361
|
+
const rules = [];
|
|
3362
|
+
for (const child of processingNode.children) {
|
|
3363
|
+
if (typeof child !== "string") {
|
|
3364
|
+
const rule = parseResponseRule(child);
|
|
3365
|
+
if (rule) rules.push(rule);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
let scoring = { responseIdentifier: "", correctScore: 1, incorrectScore: 0 };
|
|
3369
|
+
const conditionNodes = findChildren(processingNode, "qti-response-condition");
|
|
3370
|
+
for (const conditionNode of conditionNodes) {
|
|
3371
|
+
const responseIfNode = findChild(conditionNode, "qti-response-if");
|
|
3372
|
+
const responseElseNode = findChild(conditionNode, "qti-response-else");
|
|
3373
|
+
if (responseIfNode && responseElseNode) {
|
|
3374
|
+
const andNode = findChild(responseIfNode, "qti-and");
|
|
3375
|
+
if (andNode) {
|
|
3376
|
+
const matchNode = findChild(andNode, "qti-match");
|
|
3377
|
+
const variableNode = matchNode ? findChild(matchNode, "qti-variable") : void 0;
|
|
3378
|
+
if (variableNode) {
|
|
3379
|
+
const ifSetOutcomeNode = findChild(responseIfNode, "qti-set-outcome-value");
|
|
3380
|
+
const elseSetOutcomeNode = findChild(responseElseNode, "qti-set-outcome-value");
|
|
3381
|
+
const correctValueNode = ifSetOutcomeNode ? findChild(ifSetOutcomeNode, "qti-base-value") : void 0;
|
|
3382
|
+
const incorrectValueNode = elseSetOutcomeNode ? findChild(elseSetOutcomeNode, "qti-base-value") : void 0;
|
|
3383
|
+
scoring = {
|
|
3384
|
+
responseIdentifier: coerceString(variableNode.attrs.identifier),
|
|
3385
|
+
correctScore: Number(getTextContent(correctValueNode) || 1),
|
|
3386
|
+
incorrectScore: Number(getTextContent(incorrectValueNode) || 0)
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
return { rules, scoring };
|
|
3393
|
+
}
|
|
3394
|
+
function parseAssessmentItemXml(xml) {
|
|
3395
|
+
if (!xml || typeof xml !== "string") {
|
|
3396
|
+
throw A("xml input must be a non-empty string");
|
|
3397
|
+
}
|
|
3398
|
+
const parser = createXmlParser();
|
|
3399
|
+
const parseResult = I(() => {
|
|
3400
|
+
return parser.parse(xml, true);
|
|
3401
|
+
});
|
|
3402
|
+
if (parseResult.error) {
|
|
3403
|
+
throw B(parseResult.error, "xml parse");
|
|
3404
|
+
}
|
|
3405
|
+
const raw = parseResult.data;
|
|
3406
|
+
if (!Array.isArray(raw)) {
|
|
3407
|
+
throw A("expected xml parser to output an array for preserveOrder");
|
|
3408
|
+
}
|
|
3409
|
+
const normalizedTree = raw.map(normalizeNode).filter((n) => typeof n !== "string");
|
|
3410
|
+
const rootNode = normalizedTree.find((n) => n.tagName.endsWith("assessment-item"));
|
|
3411
|
+
if (!rootNode) {
|
|
3412
|
+
throw A("qti assessment item not found in xml document");
|
|
3413
|
+
}
|
|
3414
|
+
const rootChildren = rootNode.children.filter((c) => typeof c !== "string");
|
|
3415
|
+
const itemBodyNode = rootChildren.find((n) => n.tagName === "qti-item-body");
|
|
3416
|
+
const contentBlocks = [];
|
|
3417
|
+
const feedbackBlocks = [];
|
|
3418
|
+
if (itemBodyNode) {
|
|
3419
|
+
for (const child of itemBodyNode.children) {
|
|
3420
|
+
if (typeof child === "string") {
|
|
3421
|
+
if (child.trim()) contentBlocks.push({ type: "stimulus", html: child });
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
if (child.tagName === "qti-feedback-block") {
|
|
3425
|
+
feedbackBlocks.push(extractFeedbackBlock(child));
|
|
3426
|
+
} else {
|
|
3427
|
+
const interactionNodes = findInteractionNodes(child);
|
|
3428
|
+
const hasInline = interactionNodes.some(
|
|
3429
|
+
(n) => n.tagName === "qti-inline-choice-interaction" || n.tagName === "qti-text-entry-interaction"
|
|
3430
|
+
);
|
|
3431
|
+
if (hasInline) {
|
|
3432
|
+
const inlineEmbeds = {};
|
|
3433
|
+
const textEmbeds = {};
|
|
3434
|
+
const html = serializeWithInlinePlaceholders(child, inlineEmbeds, textEmbeds);
|
|
3435
|
+
contentBlocks.push({ type: "richStimulus", html, inlineEmbeds, textEmbeds });
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
if (interactionNodes.length > 0) {
|
|
3439
|
+
for (const inode of interactionNodes) {
|
|
3440
|
+
if (inode.tagName === "qti-choice-interaction") {
|
|
3441
|
+
const interaction = extractChoiceInteraction(inode);
|
|
3442
|
+
contentBlocks.push({ type: "interaction", interaction });
|
|
3443
|
+
} else if (inode.tagName === "qti-text-entry-interaction") {
|
|
3444
|
+
const interaction = extractTextEntryInteraction(inode);
|
|
3445
|
+
contentBlocks.push({ type: "interaction", interaction });
|
|
3446
|
+
} else if (inode.tagName === "qti-order-interaction") {
|
|
3447
|
+
const interaction = extractOrderInteraction(inode);
|
|
3448
|
+
contentBlocks.push({ type: "interaction", interaction });
|
|
3449
|
+
} else if (inode.tagName === "qti-gap-match-interaction") {
|
|
3450
|
+
const interaction = extractGapMatchInteraction(inode);
|
|
3451
|
+
contentBlocks.push({ type: "interaction", interaction });
|
|
3452
|
+
} else if (inode.tagName === "qti-match-interaction") {
|
|
3453
|
+
const interaction = extractMatchInteraction(inode);
|
|
3454
|
+
contentBlocks.push({ type: "interaction", interaction });
|
|
3455
|
+
} else {
|
|
3456
|
+
contentBlocks.push({ type: "stimulus", html: nodeToXml(inode) });
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
contentBlocks.push({ type: "stimulus", html: nodeToXml(child) });
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
let normalizedItem = {
|
|
3466
|
+
identifier: coerceString(rootNode.attrs.identifier),
|
|
3467
|
+
title: coerceString(rootNode.attrs.title),
|
|
3468
|
+
timeDependent: Boolean(rootNode.attrs["time-dependent"]),
|
|
3469
|
+
xmlLang: coerceString(rootNode.attrs["xml:lang"]) || coerceString(rootNode.attrs["xml-lang"]) || "en",
|
|
3470
|
+
responseDeclarations: extractResponseDeclarations(rootChildren),
|
|
3471
|
+
outcomeDeclarations: extractOutcomeDeclarations(rootChildren),
|
|
3472
|
+
itemBody: { contentBlocks, feedbackBlocks },
|
|
3473
|
+
responseProcessing: extractResponseProcessing(rootChildren)
|
|
3474
|
+
};
|
|
3475
|
+
if (normalizedItem.responseProcessing.scoring.responseIdentifier === "" && normalizedItem.responseDeclarations.length > 0) {
|
|
3476
|
+
const firstDecl = normalizedItem.responseDeclarations[0];
|
|
3477
|
+
if (firstDecl) {
|
|
3478
|
+
normalizedItem.responseProcessing.scoring.responseIdentifier = firstDecl.identifier;
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
const validation = AssessmentItemSchema.safeParse(normalizedItem);
|
|
3482
|
+
if (!validation.success) {
|
|
3483
|
+
const errorDetails = validation.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
|
|
3484
|
+
throw A(`qti item validation: ${errorDetails}`);
|
|
3485
|
+
}
|
|
3486
|
+
return validation.data;
|
|
3487
|
+
}
|
|
3488
|
+
var Q = new TextEncoder();
|
|
3489
|
+
({ DEBUG: Q.encode(" DEBUG "), INFO: Q.encode(" INFO "), WARN: Q.encode(" WARN "), ERROR: Q.encode(" ERROR "), NEWLINE: Q.encode(`
|
|
3490
|
+
`), SPACE: Q.encode(" "), EQUALS: Q.encode("="), SLASH: Q.encode("/"), COLON: Q.encode(":"), NULL: Q.encode("null"), UNDEFINED: Q.encode("undefined"), TRUE: Q.encode("true"), FALSE: Q.encode("false"), QUOTE: Q.encode('"'), BRACKET_OPEN: Q.encode("["), BRACKET_CLOSE: Q.encode("]"), BRACE_OPEN: Q.encode("{"), BRACE_CLOSE: Q.encode("}"), COMMA: Q.encode(","), ZERO: Q.encode("0"), MINUS: Q.encode("-"), DOT: Q.encode(".") });
|
|
3491
|
+
function y(x, G) {
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
// src/evaluator.ts
|
|
3496
|
+
function normalizeString(str, caseSensitive) {
|
|
3497
|
+
const s = (str ?? "").trim();
|
|
3498
|
+
return caseSensitive ? s : s.toLowerCase();
|
|
3499
|
+
}
|
|
3500
|
+
function checkCondition(condition, item, responses, responseResults) {
|
|
3501
|
+
if (condition.type === "and") {
|
|
3502
|
+
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
3503
|
+
const allTrue = results.every((r) => r === true);
|
|
3504
|
+
y("qti evaluator: checking AND condition", {
|
|
3505
|
+
numConditions: condition.conditions.length});
|
|
3506
|
+
return allTrue;
|
|
3507
|
+
}
|
|
3508
|
+
if (condition.type === "or") {
|
|
3509
|
+
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
3510
|
+
const anyTrue = results.some((r) => r === true);
|
|
3511
|
+
y("qti evaluator: checking OR condition", {
|
|
3512
|
+
numConditions: condition.conditions.length});
|
|
3513
|
+
return anyTrue;
|
|
3514
|
+
}
|
|
3515
|
+
if (condition.type === "not") {
|
|
3516
|
+
const result = checkCondition(condition.condition, item, responses, responseResults);
|
|
3517
|
+
return !result;
|
|
3518
|
+
}
|
|
3519
|
+
if (condition.type === "match") {
|
|
3520
|
+
const result = responseResults[condition.variable];
|
|
3521
|
+
y("qti evaluator: checking match condition", {
|
|
3522
|
+
variable: condition.variable});
|
|
3523
|
+
return result === true;
|
|
3524
|
+
}
|
|
3525
|
+
if (condition.type === "matchValue") {
|
|
3526
|
+
const userResponse = responses[condition.variable];
|
|
3527
|
+
let userValues;
|
|
3528
|
+
if (Array.isArray(userResponse)) {
|
|
3529
|
+
userValues = userResponse;
|
|
3530
|
+
} else if (userResponse) {
|
|
3531
|
+
userValues = [userResponse];
|
|
3532
|
+
} else {
|
|
3533
|
+
userValues = [];
|
|
3534
|
+
}
|
|
3535
|
+
const matches = userValues.includes(condition.value);
|
|
3536
|
+
y("qti evaluator: checking matchValue condition", {
|
|
3537
|
+
variable: condition.variable,
|
|
3538
|
+
targetValue: condition.value});
|
|
3539
|
+
return matches;
|
|
3540
|
+
}
|
|
3541
|
+
if (condition.type === "stringMatch") {
|
|
3542
|
+
const userResponse = responses[condition.variable];
|
|
3543
|
+
const val = Array.isArray(userResponse) ? userResponse[0] : userResponse;
|
|
3544
|
+
if (!val) return false;
|
|
3545
|
+
const u = normalizeString(String(val), condition.caseSensitive);
|
|
3546
|
+
const t = normalizeString(condition.value, condition.caseSensitive);
|
|
3547
|
+
const matches = u === t;
|
|
3548
|
+
y("qti evaluator: checking stringMatch", {
|
|
3549
|
+
variable: condition.variable,
|
|
3550
|
+
caseSensitive: condition.caseSensitive});
|
|
3551
|
+
return matches;
|
|
3552
|
+
}
|
|
3553
|
+
if (condition.type === "member") {
|
|
3554
|
+
const userResponse = responses[condition.variable];
|
|
3555
|
+
let userValues;
|
|
3556
|
+
if (Array.isArray(userResponse)) {
|
|
3557
|
+
userValues = userResponse;
|
|
3558
|
+
} else if (userResponse) {
|
|
3559
|
+
userValues = [userResponse];
|
|
3560
|
+
} else {
|
|
3561
|
+
userValues = [];
|
|
3562
|
+
}
|
|
3563
|
+
const matches = userValues.includes(condition.value);
|
|
3564
|
+
y("qti evaluator: checking member", {
|
|
3565
|
+
variable: condition.variable,
|
|
3566
|
+
target: condition.value});
|
|
3567
|
+
return matches;
|
|
3568
|
+
}
|
|
3569
|
+
if (condition.type === "equalMapResponse") {
|
|
3570
|
+
const responseId = condition.variable;
|
|
3571
|
+
const userResponse = responses[responseId];
|
|
3572
|
+
const responseDecl = item.responseDeclarations.find((rd) => rd.identifier === responseId);
|
|
3573
|
+
if (!userResponse || !responseDecl?.mapping) return false;
|
|
3574
|
+
const userValues = Array.isArray(userResponse) ? userResponse : [userResponse];
|
|
3575
|
+
const valueMap = new Map(responseDecl.mapping.entries.map((e) => [e.key, e.value]));
|
|
3576
|
+
let sum = 0;
|
|
3577
|
+
for (const val of userValues) {
|
|
3578
|
+
sum += valueMap.get(val) ?? responseDecl.mapping.defaultValue;
|
|
3579
|
+
}
|
|
3580
|
+
const target = Number(condition.value);
|
|
3581
|
+
const diff = Math.abs(sum - target);
|
|
3582
|
+
const matches = diff < 1e-4;
|
|
3583
|
+
y("qti evaluator: checking equalMapResponse", {
|
|
3584
|
+
target: condition.value});
|
|
3585
|
+
return matches;
|
|
3586
|
+
}
|
|
3587
|
+
return false;
|
|
3588
|
+
}
|
|
3589
|
+
function evaluateRule(rule, item, responses, responseResults) {
|
|
3590
|
+
const feedbackIds = [];
|
|
3591
|
+
if (rule.type === "action") {
|
|
3592
|
+
const action = rule.action;
|
|
3593
|
+
if (action.type === "setOutcomeValue" && (action.identifier === "FEEDBACK__OVERALL" || action.identifier === "FEEDBACK__PEDAGOGY" || action.identifier === "FEEDBACK")) {
|
|
3594
|
+
y("qti evaluator: executing action", { id: action.identifier, value: action.value });
|
|
3595
|
+
feedbackIds.push(action.value);
|
|
3596
|
+
}
|
|
3597
|
+
return feedbackIds;
|
|
3598
|
+
}
|
|
3599
|
+
if (rule.type === "condition") {
|
|
3600
|
+
for (const branch of rule.branches) {
|
|
3601
|
+
const isMatch = branch.condition ? checkCondition(branch.condition, item, responses, responseResults) : true;
|
|
3602
|
+
y("qti evaluator: evaluating branch", {
|
|
3603
|
+
hasCondition: !!branch.condition});
|
|
3604
|
+
if (isMatch) {
|
|
3605
|
+
const feedbackActions = branch.actions.filter(
|
|
3606
|
+
(r) => r.type === "setOutcomeValue" && (r.identifier === "FEEDBACK__OVERALL" || r.identifier === "FEEDBACK__PEDAGOGY" || r.identifier === "FEEDBACK")
|
|
3607
|
+
);
|
|
3608
|
+
for (const action of feedbackActions) {
|
|
3609
|
+
feedbackIds.push(action.value);
|
|
3610
|
+
}
|
|
3611
|
+
if (branch.nestedRules) {
|
|
3612
|
+
for (const nested of branch.nestedRules) {
|
|
3613
|
+
const nestedIds = evaluateRule(nested, item, responses, responseResults);
|
|
3614
|
+
feedbackIds.push(...nestedIds);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
break;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
return feedbackIds;
|
|
3622
|
+
}
|
|
3623
|
+
function evaluateFeedbackIdentifiers(item, responses, responseResults) {
|
|
3624
|
+
const processing = item.responseProcessing;
|
|
3625
|
+
if (!processing) {
|
|
3626
|
+
return [];
|
|
3627
|
+
}
|
|
3628
|
+
y("qti evaluator: starting evaluation", {
|
|
3629
|
+
numRules: processing.rules.length});
|
|
3630
|
+
const allFeedbackIds = [];
|
|
3631
|
+
for (const rule of processing.rules) {
|
|
3632
|
+
const ids = evaluateRule(rule, item, responses, responseResults);
|
|
3633
|
+
allFeedbackIds.push(...ids);
|
|
3634
|
+
}
|
|
3635
|
+
return allFeedbackIds;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
export { AssessmentItemSchema, ContentBlockRenderer, QTIRenderer, detectContentType, evaluateFeedbackIdentifiers, parseAssessmentItemXml, sanitizeForDisplay, sanitizeHtml, serializeInner, serializeNode, serializeNodes };
|
|
3639
|
+
//# sourceMappingURL=index.js.map
|
|
3640
|
+
//# sourceMappingURL=index.js.map
|