@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/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, "&quot;")}"`;
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
+ "&": "&amp;",
321
+ "<": "&lt;",
322
+ ">": "&gt;",
323
+ '"': "&quot;",
324
+ "'": "&#39;"
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, "&quot;")}"`;
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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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