@superbuilders/incept-renderer 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +632 -88
- package/dist/actions/index.d.ts +1 -1
- package/dist/actions/index.js +362 -552
- package/dist/actions/index.js.map +1 -1
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.js +71 -115
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +415 -461
- package/dist/index.js.map +1 -1
- package/dist/{schema-DxNEXGoq.d.ts → schema-CkAPLwco.d.ts} +5 -5
- package/dist/{types-MOyn9ktl.d.ts → types-DIDj-78l.d.ts} +1 -1
- package/package.json +16 -3
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { clsx } from 'clsx';
|
|
|
3
3
|
import { twMerge } from 'tailwind-merge';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
|
7
6
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
7
|
+
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
|
8
8
|
import { cva } from 'class-variance-authority';
|
|
9
9
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
10
10
|
import '@radix-ui/react-separator';
|
|
@@ -13,8 +13,10 @@ import { useSensors, useSensor, PointerSensor, KeyboardSensor, defaultDropAnimat
|
|
|
13
13
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
14
14
|
import { sortableKeyboardCoordinates, SortableContext, horizontalListSortingStrategy, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
|
|
15
15
|
import { CSS } from '@dnd-kit/utilities';
|
|
16
|
+
import * as logger from '@superbuilders/slog';
|
|
17
|
+
import * as errors from '@superbuilders/errors';
|
|
16
18
|
import { XMLParser } from 'fast-xml-parser';
|
|
17
|
-
import { z
|
|
19
|
+
import { z } from 'zod';
|
|
18
20
|
|
|
19
21
|
// src/components/qti-renderer.tsx
|
|
20
22
|
function cn(...inputs) {
|
|
@@ -496,26 +498,11 @@ var choiceIndicatorVariants = cva(
|
|
|
496
498
|
"[--choice-foreground:var(--color-foreground)]",
|
|
497
499
|
"[--choice-complement:var(--color-muted-foreground)]"
|
|
498
500
|
],
|
|
499
|
-
betta: [
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
],
|
|
503
|
-
|
|
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
|
-
]
|
|
501
|
+
betta: ["[--choice-foreground:var(--color-betta)]", "[--choice-complement:var(--color-butterfly)]"],
|
|
502
|
+
cardinal: ["[--choice-foreground:var(--color-cardinal)]", "[--choice-complement:var(--color-fire-ant)]"],
|
|
503
|
+
bee: ["[--choice-foreground:var(--color-bee)]", "[--choice-complement:var(--color-lion)]"],
|
|
504
|
+
owl: ["[--choice-foreground:var(--color-owl)]", "[--choice-complement:var(--color-tree-frog)]"],
|
|
505
|
+
macaw: ["[--choice-foreground:var(--color-macaw)]", "[--choice-complement:var(--color-whale)]"]
|
|
519
506
|
}
|
|
520
507
|
},
|
|
521
508
|
defaultVariants: {
|
|
@@ -558,10 +545,7 @@ function ChoiceIndicator({
|
|
|
558
545
|
}
|
|
559
546
|
);
|
|
560
547
|
}
|
|
561
|
-
function Label({
|
|
562
|
-
className,
|
|
563
|
-
...props
|
|
564
|
-
}) {
|
|
548
|
+
function Label({ className, ...props }) {
|
|
565
549
|
return /* @__PURE__ */ jsx(
|
|
566
550
|
LabelPrimitive.Root,
|
|
567
551
|
{
|
|
@@ -601,29 +585,26 @@ function FieldGroup({ className, ...props }) {
|
|
|
601
585
|
}
|
|
602
586
|
);
|
|
603
587
|
}
|
|
604
|
-
var fieldVariants = cva(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
],
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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"
|
|
588
|
+
var fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", {
|
|
589
|
+
variants: {
|
|
590
|
+
orientation: {
|
|
591
|
+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
592
|
+
horizontal: [
|
|
593
|
+
"flex-row items-center",
|
|
594
|
+
"[&>[data-slot=field-label]]:flex-auto",
|
|
595
|
+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px"
|
|
596
|
+
],
|
|
597
|
+
responsive: [
|
|
598
|
+
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
|
599
|
+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
600
|
+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px"
|
|
601
|
+
]
|
|
624
602
|
}
|
|
603
|
+
},
|
|
604
|
+
defaultVariants: {
|
|
605
|
+
orientation: "vertical"
|
|
625
606
|
}
|
|
626
|
-
);
|
|
607
|
+
});
|
|
627
608
|
function Field({
|
|
628
609
|
className,
|
|
629
610
|
orientation = "vertical",
|
|
@@ -645,10 +626,7 @@ function FieldContent({ className, ...props }) {
|
|
|
645
626
|
"div",
|
|
646
627
|
{
|
|
647
628
|
"data-slot": "field-content",
|
|
648
|
-
className: cn(
|
|
649
|
-
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
650
|
-
className
|
|
651
|
-
),
|
|
629
|
+
className: cn("group/field-content flex flex-1 flex-col gap-1.5 leading-snug", className),
|
|
652
630
|
...props
|
|
653
631
|
}
|
|
654
632
|
);
|
|
@@ -751,18 +729,8 @@ cva(
|
|
|
751
729
|
}
|
|
752
730
|
}
|
|
753
731
|
);
|
|
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
|
-
);
|
|
732
|
+
function RadioGroup({ className, ...props }) {
|
|
733
|
+
return /* @__PURE__ */ jsx(RadioGroupPrimitive.Root, { "data-slot": "radio-group", className: cn("grid gap-3", className), ...props });
|
|
766
734
|
}
|
|
767
735
|
function ChoiceInteractionRenderer({
|
|
768
736
|
interaction,
|
|
@@ -1239,6 +1207,46 @@ function GapMatchInteraction({
|
|
|
1239
1207
|
}
|
|
1240
1208
|
);
|
|
1241
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
// src/shared/shuffle.ts
|
|
1212
|
+
function xmur3(str) {
|
|
1213
|
+
let h = 1779033703 ^ str.length;
|
|
1214
|
+
for (let i = 0; i < str.length; i++) {
|
|
1215
|
+
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
|
1216
|
+
h = h << 13 | h >>> 19;
|
|
1217
|
+
}
|
|
1218
|
+
return () => {
|
|
1219
|
+
h = Math.imul(h ^ h >>> 16, 2246822507);
|
|
1220
|
+
h = Math.imul(h ^ h >>> 13, 3266489909);
|
|
1221
|
+
const shifted = h >>> 16;
|
|
1222
|
+
h = h ^ shifted;
|
|
1223
|
+
return h >>> 0;
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function mulberry32(seed) {
|
|
1227
|
+
let state = seed;
|
|
1228
|
+
return () => {
|
|
1229
|
+
state = state + 1831565813;
|
|
1230
|
+
let t = state;
|
|
1231
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1232
|
+
t = t ^ t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1233
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function shuffleWithSeed(items, seed) {
|
|
1237
|
+
const out = items.slice();
|
|
1238
|
+
const seedFn = xmur3(seed);
|
|
1239
|
+
const rng = mulberry32(seedFn());
|
|
1240
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
1241
|
+
const j = Math.floor(rng() * (i + 1));
|
|
1242
|
+
const vi = out[i];
|
|
1243
|
+
const vj = out[j];
|
|
1244
|
+
if (vi === void 0 || vj === void 0) continue;
|
|
1245
|
+
out[i] = vj;
|
|
1246
|
+
out[j] = vi;
|
|
1247
|
+
}
|
|
1248
|
+
return out;
|
|
1249
|
+
}
|
|
1242
1250
|
var selectTriggerVariants = cva(
|
|
1243
1251
|
[
|
|
1244
1252
|
"cursor-pointer",
|
|
@@ -1297,10 +1305,7 @@ var selectTriggerVariants = cva(
|
|
|
1297
1305
|
function Select({ ...props }) {
|
|
1298
1306
|
return /* @__PURE__ */ jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
|
|
1299
1307
|
}
|
|
1300
|
-
function SelectValue({
|
|
1301
|
-
className,
|
|
1302
|
-
...props
|
|
1303
|
-
}) {
|
|
1308
|
+
function SelectValue({ className, ...props }) {
|
|
1304
1309
|
return /* @__PURE__ */ jsx(
|
|
1305
1310
|
SelectPrimitive.Value,
|
|
1306
1311
|
{
|
|
@@ -1371,11 +1376,7 @@ function SelectContent({
|
|
|
1371
1376
|
}
|
|
1372
1377
|
) });
|
|
1373
1378
|
}
|
|
1374
|
-
function SelectItem({
|
|
1375
|
-
className,
|
|
1376
|
-
children,
|
|
1377
|
-
...props
|
|
1378
|
-
}) {
|
|
1379
|
+
function SelectItem({ className, children, ...props }) {
|
|
1379
1380
|
return /* @__PURE__ */ jsxs(
|
|
1380
1381
|
SelectPrimitive.Item,
|
|
1381
1382
|
{
|
|
@@ -1392,10 +1393,7 @@ function SelectItem({
|
|
|
1392
1393
|
}
|
|
1393
1394
|
);
|
|
1394
1395
|
}
|
|
1395
|
-
function SelectScrollUpButton({
|
|
1396
|
-
className,
|
|
1397
|
-
...props
|
|
1398
|
-
}) {
|
|
1396
|
+
function SelectScrollUpButton({ className, ...props }) {
|
|
1399
1397
|
return /* @__PURE__ */ jsx(
|
|
1400
1398
|
SelectPrimitive.ScrollUpButton,
|
|
1401
1399
|
{
|
|
@@ -1420,46 +1418,6 @@ function SelectScrollDownButton({
|
|
|
1420
1418
|
}
|
|
1421
1419
|
);
|
|
1422
1420
|
}
|
|
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
1421
|
function SafeInlineHTML2({ html, className, style }) {
|
|
1464
1422
|
const ref = React3.useRef(null);
|
|
1465
1423
|
React3.useEffect(() => {
|
|
@@ -1768,7 +1726,7 @@ function MatchInteraction({
|
|
|
1768
1726
|
const newState = { ...currentState };
|
|
1769
1727
|
const oldSources = newState[fromTargetId];
|
|
1770
1728
|
if (oldSources) {
|
|
1771
|
-
newState[fromTargetId] = oldSources.filter((
|
|
1729
|
+
newState[fromTargetId] = oldSources.filter((_, i) => i !== fromIndex);
|
|
1772
1730
|
if (newState[fromTargetId].length === 0) {
|
|
1773
1731
|
delete newState[fromTargetId];
|
|
1774
1732
|
}
|
|
@@ -1806,7 +1764,7 @@ function MatchInteraction({
|
|
|
1806
1764
|
const newState = { ...currentState };
|
|
1807
1765
|
const oldSources = newState[fromTargetId];
|
|
1808
1766
|
if (oldSources) {
|
|
1809
|
-
newState[fromTargetId] = oldSources.filter((
|
|
1767
|
+
newState[fromTargetId] = oldSources.filter((_, i) => i !== fromIndex);
|
|
1810
1768
|
if (newState[fromTargetId].length === 0) {
|
|
1811
1769
|
delete newState[fromTargetId];
|
|
1812
1770
|
}
|
|
@@ -1823,7 +1781,7 @@ function MatchInteraction({
|
|
|
1823
1781
|
const newState = { ...currentState };
|
|
1824
1782
|
const sources = newState[targetId];
|
|
1825
1783
|
if (sources) {
|
|
1826
|
-
newState[targetId] = sources.filter((
|
|
1784
|
+
newState[targetId] = sources.filter((_, i) => i !== index);
|
|
1827
1785
|
if (newState[targetId].length === 0) {
|
|
1828
1786
|
delete newState[targetId];
|
|
1829
1787
|
}
|
|
@@ -2587,126 +2545,271 @@ function QTIRenderer({
|
|
|
2587
2545
|
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
2546
|
] });
|
|
2589
2547
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
if (
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2548
|
+
function normalizeString(str, caseSensitive) {
|
|
2549
|
+
const s = (str ?? "").trim();
|
|
2550
|
+
return caseSensitive ? s : s.toLowerCase();
|
|
2551
|
+
}
|
|
2552
|
+
function checkCondition(condition, item, responses, responseResults) {
|
|
2553
|
+
if (condition.type === "and") {
|
|
2554
|
+
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
2555
|
+
const allTrue = results.every((r) => r === true);
|
|
2556
|
+
logger.debug("qti evaluator: checking AND condition", {
|
|
2557
|
+
numConditions: condition.conditions.length,
|
|
2558
|
+
results,
|
|
2559
|
+
allTrue
|
|
2560
|
+
});
|
|
2561
|
+
return allTrue;
|
|
2562
|
+
}
|
|
2563
|
+
if (condition.type === "or") {
|
|
2564
|
+
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
2565
|
+
const anyTrue = results.some((r) => r === true);
|
|
2566
|
+
logger.debug("qti evaluator: checking OR condition", {
|
|
2567
|
+
numConditions: condition.conditions.length,
|
|
2568
|
+
results,
|
|
2569
|
+
anyTrue
|
|
2570
|
+
});
|
|
2571
|
+
return anyTrue;
|
|
2572
|
+
}
|
|
2573
|
+
if (condition.type === "not") {
|
|
2574
|
+
const result = checkCondition(condition.condition, item, responses, responseResults);
|
|
2575
|
+
logger.debug("qti evaluator: checking NOT condition", { result, negated: !result });
|
|
2576
|
+
return !result;
|
|
2577
|
+
}
|
|
2578
|
+
if (condition.type === "match") {
|
|
2579
|
+
const result = responseResults[condition.variable];
|
|
2580
|
+
logger.debug("qti evaluator: checking match condition", {
|
|
2581
|
+
variable: condition.variable,
|
|
2582
|
+
isCorrect: result,
|
|
2583
|
+
matches: result === true
|
|
2584
|
+
});
|
|
2585
|
+
return result === true;
|
|
2586
|
+
}
|
|
2587
|
+
if (condition.type === "matchValue") {
|
|
2588
|
+
const userResponse = responses[condition.variable];
|
|
2589
|
+
let userValues;
|
|
2590
|
+
if (Array.isArray(userResponse)) {
|
|
2591
|
+
userValues = userResponse;
|
|
2592
|
+
} else if (userResponse) {
|
|
2593
|
+
userValues = [userResponse];
|
|
2594
|
+
} else {
|
|
2595
|
+
userValues = [];
|
|
2596
|
+
}
|
|
2597
|
+
const matches = userValues.includes(condition.value);
|
|
2598
|
+
logger.debug("qti evaluator: checking matchValue condition", {
|
|
2599
|
+
variable: condition.variable,
|
|
2600
|
+
targetValue: condition.value,
|
|
2601
|
+
userValues,
|
|
2602
|
+
matches
|
|
2603
|
+
});
|
|
2604
|
+
return matches;
|
|
2605
|
+
}
|
|
2606
|
+
if (condition.type === "stringMatch") {
|
|
2607
|
+
const userResponse = responses[condition.variable];
|
|
2608
|
+
const val = Array.isArray(userResponse) ? userResponse[0] : userResponse;
|
|
2609
|
+
if (!val) return false;
|
|
2610
|
+
const u = normalizeString(String(val), condition.caseSensitive);
|
|
2611
|
+
const t = normalizeString(condition.value, condition.caseSensitive);
|
|
2612
|
+
const matches = u === t;
|
|
2613
|
+
logger.debug("qti evaluator: checking stringMatch", {
|
|
2614
|
+
variable: condition.variable,
|
|
2615
|
+
caseSensitive: condition.caseSensitive,
|
|
2616
|
+
matches
|
|
2617
|
+
});
|
|
2618
|
+
return matches;
|
|
2619
|
+
}
|
|
2620
|
+
if (condition.type === "member") {
|
|
2621
|
+
const userResponse = responses[condition.variable];
|
|
2622
|
+
let userValues;
|
|
2623
|
+
if (Array.isArray(userResponse)) {
|
|
2624
|
+
userValues = userResponse;
|
|
2625
|
+
} else if (userResponse) {
|
|
2626
|
+
userValues = [userResponse];
|
|
2627
|
+
} else {
|
|
2628
|
+
userValues = [];
|
|
2629
|
+
}
|
|
2630
|
+
const matches = userValues.includes(condition.value);
|
|
2631
|
+
logger.debug("qti evaluator: checking member", {
|
|
2632
|
+
variable: condition.variable,
|
|
2633
|
+
target: condition.value,
|
|
2634
|
+
userValues,
|
|
2635
|
+
matches
|
|
2636
|
+
});
|
|
2637
|
+
return matches;
|
|
2638
|
+
}
|
|
2639
|
+
if (condition.type === "equalMapResponse") {
|
|
2640
|
+
const responseId = condition.variable;
|
|
2641
|
+
const userResponse = responses[responseId];
|
|
2642
|
+
const responseDecl = item.responseDeclarations.find((rd) => rd.identifier === responseId);
|
|
2643
|
+
if (!userResponse || !responseDecl?.mapping) return false;
|
|
2644
|
+
const userValues = Array.isArray(userResponse) ? userResponse : [userResponse];
|
|
2645
|
+
const valueMap = new Map(responseDecl.mapping.entries.map((e) => [e.key, e.value]));
|
|
2646
|
+
let sum = 0;
|
|
2647
|
+
for (const val of userValues) {
|
|
2648
|
+
sum += valueMap.get(val) ?? responseDecl.mapping.defaultValue;
|
|
2649
|
+
}
|
|
2650
|
+
const target = Number(condition.value);
|
|
2651
|
+
const diff = Math.abs(sum - target);
|
|
2652
|
+
const matches = diff < 1e-4;
|
|
2653
|
+
logger.debug("qti evaluator: checking equalMapResponse", {
|
|
2654
|
+
responseId,
|
|
2655
|
+
userValues,
|
|
2656
|
+
sum,
|
|
2657
|
+
target: condition.value,
|
|
2658
|
+
matches
|
|
2659
|
+
});
|
|
2660
|
+
return matches;
|
|
2661
|
+
}
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
function evaluateRule(rule, item, responses, responseResults) {
|
|
2665
|
+
const feedbackIds = [];
|
|
2666
|
+
if (rule.type === "action") {
|
|
2667
|
+
const action = rule.action;
|
|
2668
|
+
if (action.type === "setOutcomeValue" && (action.identifier === "FEEDBACK__OVERALL" || action.identifier === "FEEDBACK__PEDAGOGY" || action.identifier === "FEEDBACK")) {
|
|
2669
|
+
logger.debug("qti evaluator: executing action", { id: action.identifier, value: action.value });
|
|
2670
|
+
feedbackIds.push(action.value);
|
|
2671
|
+
}
|
|
2672
|
+
return feedbackIds;
|
|
2673
|
+
}
|
|
2674
|
+
if (rule.type === "condition") {
|
|
2675
|
+
for (const branch of rule.branches) {
|
|
2676
|
+
const isMatch = branch.condition ? checkCondition(branch.condition, item, responses, responseResults) : true;
|
|
2677
|
+
logger.debug("qti evaluator: evaluating branch", {
|
|
2678
|
+
hasCondition: !!branch.condition,
|
|
2679
|
+
isMatch
|
|
2680
|
+
});
|
|
2681
|
+
if (isMatch) {
|
|
2682
|
+
const feedbackActions = branch.actions.filter(
|
|
2683
|
+
(r) => r.type === "setOutcomeValue" && (r.identifier === "FEEDBACK__OVERALL" || r.identifier === "FEEDBACK__PEDAGOGY" || r.identifier === "FEEDBACK")
|
|
2684
|
+
);
|
|
2685
|
+
for (const action of feedbackActions) {
|
|
2686
|
+
feedbackIds.push(action.value);
|
|
2687
|
+
}
|
|
2688
|
+
if (branch.nestedRules) {
|
|
2689
|
+
for (const nested of branch.nestedRules) {
|
|
2690
|
+
const nestedIds = evaluateRule(nested, item, responses, responseResults);
|
|
2691
|
+
feedbackIds.push(...nestedIds);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
break;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return feedbackIds;
|
|
2699
|
+
}
|
|
2700
|
+
function evaluateFeedbackIdentifiers(item, responses, responseResults) {
|
|
2701
|
+
const processing = item.responseProcessing;
|
|
2702
|
+
if (!processing) {
|
|
2703
|
+
logger.debug("qti evaluator: no response processing found");
|
|
2704
|
+
return [];
|
|
2705
|
+
}
|
|
2706
|
+
logger.debug("qti evaluator: starting evaluation", {
|
|
2707
|
+
numRules: processing.rules.length,
|
|
2708
|
+
responseResults
|
|
2709
|
+
});
|
|
2710
|
+
const allFeedbackIds = [];
|
|
2711
|
+
for (const rule of processing.rules) {
|
|
2712
|
+
const ids = evaluateRule(rule, item, responses, responseResults);
|
|
2713
|
+
allFeedbackIds.push(...ids);
|
|
2714
|
+
}
|
|
2715
|
+
logger.debug("qti evaluator: selected feedback identifiers", { feedbackIds: allFeedbackIds });
|
|
2716
|
+
return allFeedbackIds;
|
|
2717
|
+
}
|
|
2718
|
+
var QtiCardinalitySchema = z.enum(["single", "multiple", "ordered"]);
|
|
2719
|
+
var QtiBaseTypeSchema = z.enum(["identifier", "string", "float", "integer", "boolean", "directedPair", "pair"]);
|
|
2720
|
+
var SimpleChoiceSchema = z.object({
|
|
2721
|
+
identifier: z.string().min(1),
|
|
2722
|
+
contentHtml: z.string(),
|
|
2620
2723
|
// Allows "" for empty or image-only content
|
|
2621
|
-
inlineFeedbackHtml: z
|
|
2724
|
+
inlineFeedbackHtml: z.string().optional()
|
|
2622
2725
|
// Optional per-choice feedback (qti-feedback-inline)
|
|
2623
2726
|
});
|
|
2624
|
-
var InlineChoiceSchema = z
|
|
2625
|
-
identifier: z
|
|
2626
|
-
contentHtml: z
|
|
2727
|
+
var InlineChoiceSchema = z.object({
|
|
2728
|
+
identifier: z.string().min(1),
|
|
2729
|
+
contentHtml: z.string()
|
|
2627
2730
|
});
|
|
2628
|
-
var ChoiceInteractionCoreSchema = z
|
|
2629
|
-
responseIdentifier: z
|
|
2630
|
-
shuffle: z
|
|
2631
|
-
minChoices: z
|
|
2632
|
-
maxChoices: z
|
|
2633
|
-
promptHtml: z
|
|
2634
|
-
choices: z
|
|
2731
|
+
var ChoiceInteractionCoreSchema = z.object({
|
|
2732
|
+
responseIdentifier: z.string().min(1),
|
|
2733
|
+
shuffle: z.boolean(),
|
|
2734
|
+
minChoices: z.number().int().min(0),
|
|
2735
|
+
maxChoices: z.number().int().min(1),
|
|
2736
|
+
promptHtml: z.string(),
|
|
2737
|
+
choices: z.array(SimpleChoiceSchema).min(1)
|
|
2635
2738
|
});
|
|
2636
|
-
var ChoiceInteractionSchema = ChoiceInteractionCoreSchema.extend({ type: z
|
|
2637
|
-
var InlineChoiceInteractionCoreSchema = z
|
|
2638
|
-
responseIdentifier: z
|
|
2639
|
-
shuffle: z
|
|
2640
|
-
choices: z
|
|
2739
|
+
var ChoiceInteractionSchema = ChoiceInteractionCoreSchema.extend({ type: z.literal("choiceInteraction") });
|
|
2740
|
+
var InlineChoiceInteractionCoreSchema = z.object({
|
|
2741
|
+
responseIdentifier: z.string().min(1),
|
|
2742
|
+
shuffle: z.boolean(),
|
|
2743
|
+
choices: z.array(InlineChoiceSchema).min(1)
|
|
2641
2744
|
});
|
|
2642
2745
|
var InlineChoiceInteractionSchema = InlineChoiceInteractionCoreSchema.extend({
|
|
2643
|
-
type: z
|
|
2746
|
+
type: z.literal("inlineChoiceInteraction")
|
|
2644
2747
|
});
|
|
2645
|
-
var TextEntryInteractionCoreSchema = z
|
|
2646
|
-
responseIdentifier: z
|
|
2647
|
-
expectedLength: z
|
|
2648
|
-
placeholderText: z
|
|
2649
|
-
patternMask: z
|
|
2748
|
+
var TextEntryInteractionCoreSchema = z.object({
|
|
2749
|
+
responseIdentifier: z.string().min(1),
|
|
2750
|
+
expectedLength: z.number().int().min(0).optional(),
|
|
2751
|
+
placeholderText: z.string().optional(),
|
|
2752
|
+
patternMask: z.string().optional()
|
|
2650
2753
|
});
|
|
2651
2754
|
var TextEntryInteractionSchema = TextEntryInteractionCoreSchema.extend({
|
|
2652
|
-
type: z
|
|
2755
|
+
type: z.literal("textEntryInteraction")
|
|
2653
2756
|
});
|
|
2654
|
-
var OrderInteractionCoreSchema = z
|
|
2655
|
-
responseIdentifier: z
|
|
2656
|
-
shuffle: z
|
|
2657
|
-
minChoices: z
|
|
2658
|
-
maxChoices: z
|
|
2757
|
+
var OrderInteractionCoreSchema = z.object({
|
|
2758
|
+
responseIdentifier: z.string().min(1),
|
|
2759
|
+
shuffle: z.boolean(),
|
|
2760
|
+
minChoices: z.number().int().min(0),
|
|
2761
|
+
maxChoices: z.number().int().min(0).optional(),
|
|
2659
2762
|
// 0 or undefined usually means "all"
|
|
2660
|
-
orientation: z
|
|
2661
|
-
promptHtml: z
|
|
2662
|
-
choices: z
|
|
2763
|
+
orientation: z.enum(["vertical", "horizontal"]),
|
|
2764
|
+
promptHtml: z.string(),
|
|
2765
|
+
choices: z.array(SimpleChoiceSchema).min(1)
|
|
2663
2766
|
});
|
|
2664
2767
|
var OrderInteractionSchema = OrderInteractionCoreSchema.extend({
|
|
2665
|
-
type: z
|
|
2768
|
+
type: z.literal("orderInteraction")
|
|
2666
2769
|
});
|
|
2667
|
-
var GapTextSchema = z
|
|
2668
|
-
identifier: z
|
|
2669
|
-
contentHtml: z
|
|
2670
|
-
matchMax: z
|
|
2770
|
+
var GapTextSchema = z.object({
|
|
2771
|
+
identifier: z.string().min(1),
|
|
2772
|
+
contentHtml: z.string(),
|
|
2773
|
+
matchMax: z.number().int().min(0)
|
|
2671
2774
|
// 0 = unlimited
|
|
2672
2775
|
});
|
|
2673
|
-
var GapSchema = z
|
|
2674
|
-
identifier: z
|
|
2776
|
+
var GapSchema = z.object({
|
|
2777
|
+
identifier: z.string().min(1)
|
|
2675
2778
|
});
|
|
2676
|
-
var GapMatchInteractionCoreSchema = z
|
|
2677
|
-
responseIdentifier: z
|
|
2678
|
-
shuffle: z
|
|
2679
|
-
gapTexts: z
|
|
2779
|
+
var GapMatchInteractionCoreSchema = z.object({
|
|
2780
|
+
responseIdentifier: z.string().min(1),
|
|
2781
|
+
shuffle: z.boolean(),
|
|
2782
|
+
gapTexts: z.array(GapTextSchema).min(1),
|
|
2680
2783
|
// Draggable source tokens
|
|
2681
|
-
gaps: z
|
|
2784
|
+
gaps: z.array(GapSchema).min(1),
|
|
2682
2785
|
// Drop target placeholders
|
|
2683
|
-
contentHtml: z
|
|
2786
|
+
contentHtml: z.string()
|
|
2684
2787
|
// HTML content with gap placeholders
|
|
2685
2788
|
});
|
|
2686
2789
|
var GapMatchInteractionSchema = GapMatchInteractionCoreSchema.extend({
|
|
2687
|
-
type: z
|
|
2790
|
+
type: z.literal("gapMatchInteraction")
|
|
2688
2791
|
});
|
|
2689
|
-
var AssociableChoiceSchema = z
|
|
2690
|
-
identifier: z
|
|
2691
|
-
matchMax: z
|
|
2792
|
+
var AssociableChoiceSchema = z.object({
|
|
2793
|
+
identifier: z.string().min(1),
|
|
2794
|
+
matchMax: z.number().int().min(0),
|
|
2692
2795
|
// 0 = unlimited uses
|
|
2693
|
-
contentHtml: z
|
|
2796
|
+
contentHtml: z.string()
|
|
2694
2797
|
});
|
|
2695
|
-
var MatchInteractionCoreSchema = z
|
|
2696
|
-
responseIdentifier: z
|
|
2697
|
-
shuffle: z
|
|
2698
|
-
maxAssociations: z
|
|
2798
|
+
var MatchInteractionCoreSchema = z.object({
|
|
2799
|
+
responseIdentifier: z.string().min(1),
|
|
2800
|
+
shuffle: z.boolean(),
|
|
2801
|
+
maxAssociations: z.number().int().min(0),
|
|
2699
2802
|
// 0 = unlimited total associations
|
|
2700
|
-
sourceChoices: z
|
|
2803
|
+
sourceChoices: z.array(AssociableChoiceSchema).min(1),
|
|
2701
2804
|
// First <qti-simple-match-set>
|
|
2702
|
-
targetChoices: z
|
|
2805
|
+
targetChoices: z.array(AssociableChoiceSchema).min(1),
|
|
2703
2806
|
// Second <qti-simple-match-set>
|
|
2704
|
-
promptHtml: z
|
|
2807
|
+
promptHtml: z.string()
|
|
2705
2808
|
});
|
|
2706
2809
|
var MatchInteractionSchema = MatchInteractionCoreSchema.extend({
|
|
2707
|
-
type: z
|
|
2810
|
+
type: z.literal("matchInteraction")
|
|
2708
2811
|
});
|
|
2709
|
-
var AnyInteractionSchema = z
|
|
2812
|
+
var AnyInteractionSchema = z.discriminatedUnion("type", [
|
|
2710
2813
|
ChoiceInteractionSchema,
|
|
2711
2814
|
InlineChoiceInteractionSchema,
|
|
2712
2815
|
TextEntryInteractionSchema,
|
|
@@ -2714,149 +2817,149 @@ var AnyInteractionSchema = z$1.discriminatedUnion("type", [
|
|
|
2714
2817
|
GapMatchInteractionSchema,
|
|
2715
2818
|
MatchInteractionSchema
|
|
2716
2819
|
]);
|
|
2717
|
-
var CorrectResponseSchema = z
|
|
2718
|
-
values: z
|
|
2820
|
+
var CorrectResponseSchema = z.object({
|
|
2821
|
+
values: z.array(z.string().min(1)).min(1)
|
|
2719
2822
|
});
|
|
2720
|
-
var ResponseDeclarationSchema = z
|
|
2721
|
-
identifier: z
|
|
2823
|
+
var ResponseDeclarationSchema = z.object({
|
|
2824
|
+
identifier: z.string().min(1),
|
|
2722
2825
|
cardinality: QtiCardinalitySchema,
|
|
2723
2826
|
baseType: QtiBaseTypeSchema,
|
|
2724
2827
|
correctResponse: CorrectResponseSchema,
|
|
2725
2828
|
// Optional response mapping for map-response processing (used for summed feedback/score)
|
|
2726
|
-
mapping: z
|
|
2727
|
-
defaultValue: z
|
|
2728
|
-
lowerBound: z
|
|
2729
|
-
upperBound: z
|
|
2730
|
-
entries: z
|
|
2731
|
-
z
|
|
2732
|
-
key: z
|
|
2733
|
-
value: z
|
|
2829
|
+
mapping: z.object({
|
|
2830
|
+
defaultValue: z.number().default(0),
|
|
2831
|
+
lowerBound: z.number().optional(),
|
|
2832
|
+
upperBound: z.number().optional(),
|
|
2833
|
+
entries: z.array(
|
|
2834
|
+
z.object({
|
|
2835
|
+
key: z.string().min(1),
|
|
2836
|
+
value: z.number()
|
|
2734
2837
|
})
|
|
2735
2838
|
)
|
|
2736
2839
|
}).optional()
|
|
2737
2840
|
});
|
|
2738
|
-
var OutcomeDefaultValueSchema = z
|
|
2739
|
-
value: z
|
|
2841
|
+
var OutcomeDefaultValueSchema = z.object({
|
|
2842
|
+
value: z.string()
|
|
2740
2843
|
});
|
|
2741
|
-
var OutcomeDeclarationSchema = z
|
|
2742
|
-
identifier: z
|
|
2844
|
+
var OutcomeDeclarationSchema = z.object({
|
|
2845
|
+
identifier: z.string().min(1),
|
|
2743
2846
|
cardinality: QtiCardinalitySchema,
|
|
2744
2847
|
baseType: QtiBaseTypeSchema,
|
|
2745
2848
|
defaultValue: OutcomeDefaultValueSchema.optional()
|
|
2746
2849
|
});
|
|
2747
|
-
var FeedbackBlockSchema = z
|
|
2748
|
-
outcomeIdentifier: z
|
|
2749
|
-
identifier: z
|
|
2750
|
-
showHide: z
|
|
2751
|
-
contentHtml: z
|
|
2850
|
+
var FeedbackBlockSchema = z.object({
|
|
2851
|
+
outcomeIdentifier: z.string().min(1),
|
|
2852
|
+
identifier: z.string().min(1),
|
|
2853
|
+
showHide: z.enum(["show", "hide"]).default("show"),
|
|
2854
|
+
contentHtml: z.string()
|
|
2752
2855
|
});
|
|
2753
|
-
var StimulusBlockSchema = z
|
|
2754
|
-
type: z
|
|
2755
|
-
html: z
|
|
2856
|
+
var StimulusBlockSchema = z.object({
|
|
2857
|
+
type: z.literal("stimulus"),
|
|
2858
|
+
html: z.string()
|
|
2756
2859
|
});
|
|
2757
|
-
var RichStimulusBlockSchema = z
|
|
2758
|
-
type: z
|
|
2759
|
-
html: z
|
|
2760
|
-
inlineEmbeds: z
|
|
2761
|
-
textEmbeds: z
|
|
2860
|
+
var RichStimulusBlockSchema = z.object({
|
|
2861
|
+
type: z.literal("richStimulus"),
|
|
2862
|
+
html: z.string(),
|
|
2863
|
+
inlineEmbeds: z.record(z.string(), InlineChoiceInteractionSchema),
|
|
2864
|
+
textEmbeds: z.record(z.string(), TextEntryInteractionSchema)
|
|
2762
2865
|
});
|
|
2763
|
-
var InteractionBlockSchema = z
|
|
2764
|
-
type: z
|
|
2866
|
+
var InteractionBlockSchema = z.object({
|
|
2867
|
+
type: z.literal("interaction"),
|
|
2765
2868
|
interaction: AnyInteractionSchema
|
|
2766
2869
|
});
|
|
2767
|
-
var ItemBodySchema = z
|
|
2768
|
-
contentBlocks: z
|
|
2769
|
-
feedbackBlocks: z
|
|
2870
|
+
var ItemBodySchema = z.object({
|
|
2871
|
+
contentBlocks: z.array(z.union([StimulusBlockSchema, RichStimulusBlockSchema, InteractionBlockSchema])).min(1),
|
|
2872
|
+
feedbackBlocks: z.array(FeedbackBlockSchema)
|
|
2770
2873
|
});
|
|
2771
|
-
var BaseRuleSchema = z
|
|
2772
|
-
type: z
|
|
2773
|
-
identifier: z
|
|
2774
|
-
value: z
|
|
2874
|
+
var BaseRuleSchema = z.object({
|
|
2875
|
+
type: z.literal("setOutcomeValue"),
|
|
2876
|
+
identifier: z.string(),
|
|
2877
|
+
value: z.string()
|
|
2775
2878
|
});
|
|
2776
|
-
var MatchConditionSchema = z
|
|
2777
|
-
type: z
|
|
2778
|
-
variable: z
|
|
2779
|
-
correct: z
|
|
2879
|
+
var MatchConditionSchema = z.object({
|
|
2880
|
+
type: z.literal("match"),
|
|
2881
|
+
variable: z.string(),
|
|
2882
|
+
correct: z.literal(true)
|
|
2780
2883
|
// Match against correct response
|
|
2781
2884
|
});
|
|
2782
|
-
var MatchValueConditionSchema = z
|
|
2783
|
-
type: z
|
|
2784
|
-
variable: z
|
|
2885
|
+
var MatchValueConditionSchema = z.object({
|
|
2886
|
+
type: z.literal("matchValue"),
|
|
2887
|
+
variable: z.string(),
|
|
2785
2888
|
// The response identifier from <qti-variable>
|
|
2786
|
-
value: z
|
|
2889
|
+
value: z.string()
|
|
2787
2890
|
// The target value from <qti-base-value>
|
|
2788
2891
|
});
|
|
2789
|
-
var StringMatchConditionSchema = z
|
|
2790
|
-
type: z
|
|
2791
|
-
variable: z
|
|
2792
|
-
value: z
|
|
2793
|
-
caseSensitive: z
|
|
2892
|
+
var StringMatchConditionSchema = z.object({
|
|
2893
|
+
type: z.literal("stringMatch"),
|
|
2894
|
+
variable: z.string(),
|
|
2895
|
+
value: z.string(),
|
|
2896
|
+
caseSensitive: z.boolean()
|
|
2794
2897
|
});
|
|
2795
|
-
var MemberConditionSchema = z
|
|
2796
|
-
type: z
|
|
2797
|
-
variable: z
|
|
2798
|
-
value: z
|
|
2898
|
+
var MemberConditionSchema = z.object({
|
|
2899
|
+
type: z.literal("member"),
|
|
2900
|
+
variable: z.string(),
|
|
2901
|
+
value: z.string()
|
|
2799
2902
|
});
|
|
2800
|
-
var EqualMapResponseConditionSchema = z
|
|
2801
|
-
type: z
|
|
2802
|
-
variable: z
|
|
2903
|
+
var EqualMapResponseConditionSchema = z.object({
|
|
2904
|
+
type: z.literal("equalMapResponse"),
|
|
2905
|
+
variable: z.string(),
|
|
2803
2906
|
// The response identifier from <qti-map-response>
|
|
2804
|
-
value: z
|
|
2907
|
+
value: z.string()
|
|
2805
2908
|
// The target sum value from <qti-base-value>
|
|
2806
2909
|
});
|
|
2807
|
-
var AndConditionSchema = z
|
|
2808
|
-
type: z
|
|
2809
|
-
conditions: z
|
|
2910
|
+
var AndConditionSchema = z.object({
|
|
2911
|
+
type: z.literal("and"),
|
|
2912
|
+
conditions: z.array(z.lazy(() => AnyConditionSchema))
|
|
2810
2913
|
});
|
|
2811
|
-
var OrConditionSchema = z
|
|
2812
|
-
type: z
|
|
2813
|
-
conditions: z
|
|
2914
|
+
var OrConditionSchema = z.object({
|
|
2915
|
+
type: z.literal("or"),
|
|
2916
|
+
conditions: z.array(z.lazy(() => AnyConditionSchema))
|
|
2814
2917
|
});
|
|
2815
|
-
var NotConditionSchema = z
|
|
2816
|
-
type: z
|
|
2817
|
-
condition: z
|
|
2918
|
+
var NotConditionSchema = z.object({
|
|
2919
|
+
type: z.literal("not"),
|
|
2920
|
+
condition: z.lazy(() => AnyConditionSchema)
|
|
2818
2921
|
});
|
|
2819
|
-
var AnyConditionSchema = z
|
|
2922
|
+
var AnyConditionSchema = z.union([
|
|
2820
2923
|
MatchConditionSchema,
|
|
2821
2924
|
MatchValueConditionSchema,
|
|
2822
2925
|
StringMatchConditionSchema,
|
|
2823
2926
|
MemberConditionSchema,
|
|
2824
|
-
z
|
|
2825
|
-
z
|
|
2826
|
-
z
|
|
2927
|
+
z.lazy(() => AndConditionSchema),
|
|
2928
|
+
z.lazy(() => OrConditionSchema),
|
|
2929
|
+
z.lazy(() => NotConditionSchema),
|
|
2827
2930
|
EqualMapResponseConditionSchema
|
|
2828
2931
|
]);
|
|
2829
|
-
var ConditionBranchSchema = z
|
|
2932
|
+
var ConditionBranchSchema = z.object({
|
|
2830
2933
|
condition: AnyConditionSchema.optional(),
|
|
2831
|
-
actions: z
|
|
2832
|
-
nestedRules: z
|
|
2934
|
+
actions: z.array(BaseRuleSchema),
|
|
2935
|
+
nestedRules: z.array(z.lazy(() => ResponseRuleSchema)).optional()
|
|
2833
2936
|
});
|
|
2834
|
-
var ResponseRuleSchema = z
|
|
2835
|
-
z
|
|
2836
|
-
type: z
|
|
2837
|
-
branches: z
|
|
2937
|
+
var ResponseRuleSchema = z.union([
|
|
2938
|
+
z.object({
|
|
2939
|
+
type: z.literal("condition"),
|
|
2940
|
+
branches: z.array(ConditionBranchSchema)
|
|
2838
2941
|
}),
|
|
2839
|
-
z
|
|
2840
|
-
type: z
|
|
2942
|
+
z.object({
|
|
2943
|
+
type: z.literal("action"),
|
|
2841
2944
|
action: BaseRuleSchema
|
|
2842
2945
|
})
|
|
2843
2946
|
]);
|
|
2844
|
-
var ScoringRuleSchema = z
|
|
2845
|
-
responseIdentifier: z
|
|
2846
|
-
correctScore: z
|
|
2847
|
-
incorrectScore: z
|
|
2947
|
+
var ScoringRuleSchema = z.object({
|
|
2948
|
+
responseIdentifier: z.string().min(1),
|
|
2949
|
+
correctScore: z.number(),
|
|
2950
|
+
incorrectScore: z.number()
|
|
2848
2951
|
});
|
|
2849
|
-
var ResponseProcessingSchema = z
|
|
2850
|
-
rules: z
|
|
2952
|
+
var ResponseProcessingSchema = z.object({
|
|
2953
|
+
rules: z.array(ResponseRuleSchema),
|
|
2851
2954
|
scoring: ScoringRuleSchema
|
|
2852
2955
|
});
|
|
2853
|
-
var AssessmentItemSchema = z
|
|
2854
|
-
identifier: z
|
|
2855
|
-
title: z
|
|
2856
|
-
timeDependent: z
|
|
2857
|
-
xmlLang: z
|
|
2858
|
-
responseDeclarations: z
|
|
2859
|
-
outcomeDeclarations: z
|
|
2956
|
+
var AssessmentItemSchema = z.object({
|
|
2957
|
+
identifier: z.string().min(1),
|
|
2958
|
+
title: z.string().min(1),
|
|
2959
|
+
timeDependent: z.boolean(),
|
|
2960
|
+
xmlLang: z.string().min(1),
|
|
2961
|
+
responseDeclarations: z.array(ResponseDeclarationSchema).min(1),
|
|
2962
|
+
outcomeDeclarations: z.array(OutcomeDeclarationSchema).min(1),
|
|
2860
2963
|
itemBody: ItemBodySchema,
|
|
2861
2964
|
responseProcessing: ResponseProcessingSchema
|
|
2862
2965
|
});
|
|
@@ -2888,7 +2991,7 @@ function normalizeNode(rawNode) {
|
|
|
2888
2991
|
if (textContent != null) {
|
|
2889
2992
|
return coerceString(textContent);
|
|
2890
2993
|
}
|
|
2891
|
-
const tagName = Object.keys(rawNode).find((
|
|
2994
|
+
const tagName = Object.keys(rawNode).find((k) => k !== ":@");
|
|
2892
2995
|
if (!tagName) return "";
|
|
2893
2996
|
const attrsValue = rawNode[":@"];
|
|
2894
2997
|
const attrs = isRecord(attrsValue) ? attrsValue : {};
|
|
@@ -3146,12 +3249,12 @@ function extractResponseDeclarations(rootChildren) {
|
|
|
3146
3249
|
const cardinalityRaw = coerceString(node.attrs.cardinality);
|
|
3147
3250
|
const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
|
|
3148
3251
|
if (!cardinalityResult.success) {
|
|
3149
|
-
throw
|
|
3252
|
+
throw errors.new(`invalid cardinality '${cardinalityRaw}' in response declaration`);
|
|
3150
3253
|
}
|
|
3151
3254
|
const baseTypeRaw = coerceString(node.attrs["base-type"]);
|
|
3152
3255
|
const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
|
|
3153
3256
|
if (!baseTypeResult.success) {
|
|
3154
|
-
throw
|
|
3257
|
+
throw errors.new(`invalid base-type '${baseTypeRaw}' in response declaration`);
|
|
3155
3258
|
}
|
|
3156
3259
|
return {
|
|
3157
3260
|
identifier: coerceString(node.attrs.identifier),
|
|
@@ -3171,12 +3274,12 @@ function extractOutcomeDeclarations(rootChildren) {
|
|
|
3171
3274
|
const cardinalityRaw = coerceString(node.attrs.cardinality);
|
|
3172
3275
|
const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
|
|
3173
3276
|
if (!cardinalityResult.success) {
|
|
3174
|
-
throw
|
|
3277
|
+
throw errors.new(`invalid cardinality '${cardinalityRaw}' in outcome declaration`);
|
|
3175
3278
|
}
|
|
3176
3279
|
const baseTypeRaw = coerceString(node.attrs["base-type"]);
|
|
3177
3280
|
const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
|
|
3178
3281
|
if (!baseTypeResult.success) {
|
|
3179
|
-
throw
|
|
3282
|
+
throw errors.new(`invalid base-type '${baseTypeRaw}' in outcome declaration`);
|
|
3180
3283
|
}
|
|
3181
3284
|
return {
|
|
3182
3285
|
identifier: coerceString(node.attrs.identifier),
|
|
@@ -3393,23 +3496,23 @@ function extractResponseProcessing(rootChildren) {
|
|
|
3393
3496
|
}
|
|
3394
3497
|
function parseAssessmentItemXml(xml) {
|
|
3395
3498
|
if (!xml || typeof xml !== "string") {
|
|
3396
|
-
throw
|
|
3499
|
+
throw errors.new("xml input must be a non-empty string");
|
|
3397
3500
|
}
|
|
3398
3501
|
const parser = createXmlParser();
|
|
3399
|
-
const parseResult =
|
|
3502
|
+
const parseResult = errors.trySync(() => {
|
|
3400
3503
|
return parser.parse(xml, true);
|
|
3401
3504
|
});
|
|
3402
3505
|
if (parseResult.error) {
|
|
3403
|
-
throw
|
|
3506
|
+
throw errors.wrap(parseResult.error, "xml parse");
|
|
3404
3507
|
}
|
|
3405
3508
|
const raw = parseResult.data;
|
|
3406
3509
|
if (!Array.isArray(raw)) {
|
|
3407
|
-
throw
|
|
3510
|
+
throw errors.new("expected xml parser to output an array for preserveOrder");
|
|
3408
3511
|
}
|
|
3409
3512
|
const normalizedTree = raw.map(normalizeNode).filter((n) => typeof n !== "string");
|
|
3410
3513
|
const rootNode = normalizedTree.find((n) => n.tagName.endsWith("assessment-item"));
|
|
3411
3514
|
if (!rootNode) {
|
|
3412
|
-
throw
|
|
3515
|
+
throw errors.new("qti assessment item not found in xml document");
|
|
3413
3516
|
}
|
|
3414
3517
|
const rootChildren = rootNode.children.filter((c) => typeof c !== "string");
|
|
3415
3518
|
const itemBodyNode = rootChildren.find((n) => n.tagName === "qti-item-body");
|
|
@@ -3481,159 +3584,10 @@ function parseAssessmentItemXml(xml) {
|
|
|
3481
3584
|
const validation = AssessmentItemSchema.safeParse(normalizedItem);
|
|
3482
3585
|
if (!validation.success) {
|
|
3483
3586
|
const errorDetails = validation.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
|
|
3484
|
-
throw
|
|
3587
|
+
throw errors.new(`qti item validation: ${errorDetails}`);
|
|
3485
3588
|
}
|
|
3486
3589
|
return validation.data;
|
|
3487
3590
|
}
|
|
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
3591
|
|
|
3638
3592
|
export { AssessmentItemSchema, ContentBlockRenderer, QTIRenderer, detectContentType, evaluateFeedbackIdentifiers, parseAssessmentItemXml, sanitizeForDisplay, sanitizeHtml, serializeInner, serializeNode, serializeNodes };
|
|
3639
3593
|
//# sourceMappingURL=index.js.map
|