@pie-element/mc-populated-blank 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -2
- package/dist/controller/defaults.d.ts +85 -0
- package/dist/controller/defaults.d.ts.map +1 -1
- package/dist/controller/index.d.ts.map +1 -1
- package/dist/controller/index.js +109 -4
- package/dist/controller/index.js.map +1 -1
- package/dist/delivery/index.js +334 -398
- package/dist/delivery/index.js.map +1 -1
- package/dist/index.iife.js +10 -18
- package/dist/index.js +334 -398
- package/dist/index.js.map +1 -1
- package/package.json +1 -4
- package/src/controller/defaults.ts +51 -0
- package/src/controller/index.ts +84 -5
- package/src/delivery/McPopulatedBlank.svelte +317 -425
package/README.md
CHANGED
|
@@ -1,20 +1,128 @@
|
|
|
1
1
|
# @pie-element/mc-populated-blank
|
|
2
2
|
|
|
3
|
-
Svelte 5 PIE element
|
|
3
|
+
Svelte 5 PIE element where the learner picks a **multiple-choice** option and that choice **fills a blank** in a sentence/template.
|
|
4
|
+
|
|
5
|
+
This element is the PIE counterpart for Star Learnosity CQT "populated blank" interactions. The CQT family had several published `custom_type` flavors, but they are all variants of the same base interaction:
|
|
6
|
+
|
|
7
|
+
- one selected choice
|
|
8
|
+
- one correctness key
|
|
9
|
+
- selected choice rendered into a blank target
|
|
10
|
+
- optional audio/transcript support
|
|
11
|
+
|
|
12
|
+
## What the original CQT flavors mean
|
|
13
|
+
|
|
14
|
+
The historical Learnosity CQT names mostly describe layout/stimulus differences, not different scoring logic:
|
|
15
|
+
|
|
16
|
+
- `sel_vic`: sentence-style vocabulary in context, with audio/transcript support
|
|
17
|
+
- `sr-vic`: sentence-style vocabulary in context, typically no audio button
|
|
18
|
+
- `sel_r1-g_plusggg`: lead token before blank (`before_cloze_1`) plus choice set
|
|
19
|
+
- `sel_r1-_gplusggg`: blank with a shorter/token-focused stem arrangement
|
|
20
|
+
- `sel_r1-_plusggg`: audio + choices only (no blank; modeled as audio-only mode)
|
|
21
|
+
- `sel_r1-gg_plusggg`: token-sequence variant with larger glyph-like tokens
|
|
22
|
+
- `sel_r1-_ggplusggg`: blank appears before trailing tokens (`after_cloze_*`)
|
|
23
|
+
- `sel_r1-s3_plusggg`: stimulus-heavy variant (often image/sentence block + blank + choices)
|
|
24
|
+
|
|
25
|
+
The shared behavior across these flavors is now represented by one element with configuration, instead of one element per flavor.
|
|
26
|
+
|
|
27
|
+
## Flavor to model mapping (quick defaults)
|
|
28
|
+
|
|
29
|
+
Use this as a practical starting point when mapping Learnosity CQT payloads into `mc-populated-blank` models.
|
|
30
|
+
|
|
31
|
+
| Learnosity `custom_type` | Typical layout meaning | Suggested `interactionMode` | Suggested `choiceMode` | Template hint |
|
|
32
|
+
| --- | --- | --- | --- | --- |
|
|
33
|
+
| `sel_vic` | sentence cloze with audio/transcript | `populate_blank` | `text` | `<p>{before} {{blank}} {after}</p>` |
|
|
34
|
+
| `sr-vic` | sentence cloze without listen button | `populate_blank` | `text` | `<p>{before} {{blank}} {after}</p>` |
|
|
35
|
+
| `sel_r1-g_plusggg` | lead token before blank | `populate_blank` | `text` | `<p>{before_cloze_1} {{blank}}</p>` |
|
|
36
|
+
| `sel_r1-_gplusggg` | short stem + blank | `populate_blank` | `text` | `<p>{before/after segments around {{blank}}}</p>` |
|
|
37
|
+
| `sel_r1-_plusggg` | audio + choices only (no blank) | `audio_mc_only` | `text` | no blank token in template |
|
|
38
|
+
| `sel_r1-gg_plusggg` | token-sequence style before blank | `populate_blank` | `text` | `<p>{before_1}{before_2} {{blank}}</p>` |
|
|
39
|
+
| `sel_r1-_ggplusggg` | blank before trailing tokens | `populate_blank` | `text` | `<p>{{blank}} {after_1}{after_2}</p>` |
|
|
40
|
+
| `sel_r1-s3_plusggg` | stimulus-heavy/image-first variant | `populate_blank` | `text` or `image` | include stimulus in `prompt`/`sentenceHtml`; keep single blank in `template` |
|
|
41
|
+
|
|
42
|
+
Notes:
|
|
43
|
+
|
|
44
|
+
- `choiceMode` should be `image` only when distractors are image choices; otherwise use `text`.
|
|
45
|
+
- Current model contract is single-blank for `populate_blank`; dual-cloze source shapes are normalized to one `{{blank}}`.
|
|
46
|
+
- `correctChoiceId` should always map from Learnosity `valid_response.name` (`distractor_n` -> `cN`).
|
|
47
|
+
- Layout defaults are tuned from original CQT visual baselines, but they are not fixed: override through `model.layoutLimits` when host/theme requirements differ.
|
|
4
48
|
|
|
5
49
|
## Authoring model
|
|
6
50
|
|
|
7
51
|
- **`prompt`** (optional) and **`promptEnabled`**
|
|
8
52
|
- **`template`**: HTML string containing exactly one literal `{{blank}}` token
|
|
53
|
+
- **`interactionMode`**: `populate_blank` | `audio_mc_only`
|
|
9
54
|
- **`choiceMode`**: `text` | `image`
|
|
10
55
|
- **`choices`**: `{ id, labelHtml? }` or `{ id, imageUrl, imageAlt }` per mode
|
|
11
56
|
- **`correctChoiceId`**
|
|
12
|
-
- **`hasAudio`**, **`audioUrl`**, **`audioTranscript`** (
|
|
57
|
+
- **`hasAudio`**, **`audioUrl`**, **`audioTranscript`** (`audioUrl` required when `hasAudio=true`)
|
|
58
|
+
- **`autoplayAudioEnabled`**, **`completeAudioEnabled`** (optional integration flags)
|
|
59
|
+
- **`layoutLimits`** (optional): numeric visual constraints; defaults are based on current CQT parity behavior and can be overridden per item
|
|
60
|
+
- **`layoutProfilePresets`** (optional): named preset map by `layoutProfile`; use profile as a template and override with `layoutLimits`
|
|
61
|
+
- **`audioButtonSkin`** / **`audioButtonSkinsByLocale`** (optional): override listen-button skin URLs
|
|
62
|
+
- **`uiText`** (optional): override labels/messages (show/hide correct, answer choices, autoplay prompt, transcript label, missing-audio message)
|
|
63
|
+
|
|
64
|
+
### `layoutLimits` keys (all optional, positive numbers)
|
|
65
|
+
|
|
66
|
+
- `blankStandaloneWidthRem`
|
|
67
|
+
- `blankWideWidthRem`
|
|
68
|
+
- `blankUnderlineWidthPx`
|
|
69
|
+
- `blankUnderlineWideWidthPx`
|
|
70
|
+
- `horizontalChoiceWidthPx`
|
|
71
|
+
- `horizontalChoiceWidthVw`
|
|
72
|
+
- `horizontalChoiceTileMinHeightRem`
|
|
73
|
+
- `horizontalChoiceContentMinHeightRem`
|
|
74
|
+
- `selectedImageMaxHeightRem`
|
|
75
|
+
- `choiceImageMaxHeightRem`
|
|
76
|
+
- `listenButtonSizePx`
|
|
77
|
+
- `stimulusMinColumnPx`
|
|
78
|
+
- `textMinColumnPx`
|
|
79
|
+
- `legendMaxChars`
|
|
80
|
+
- `choiceGroupGapRem`
|
|
81
|
+
- `choiceRowGapRem`
|
|
82
|
+
- `toggleButtonGapRem`
|
|
83
|
+
- `horizontalChoiceRadioTopMarginRem`
|
|
84
|
+
- `audioBlankTemplateMarginTopRem`
|
|
85
|
+
- `audioBlankTemplateMarginBottomRem`
|
|
86
|
+
- `stimulusGridColumnGapRem`
|
|
87
|
+
- `stimulusGridRowGapRem`
|
|
88
|
+
- `stimulusSentenceMarginTopRem`
|
|
89
|
+
- `stimulusChoicesMarginTopRem`
|
|
90
|
+
- `tokenGridColumnGapRem`
|
|
91
|
+
- `tokenGridRowGapRem`
|
|
92
|
+
- `tokenTemplateMarginTopRem`
|
|
93
|
+
- `tokenInlineTokenGapRem`
|
|
94
|
+
- `tokenChoicesMarginTopRem`
|
|
95
|
+
- `inlineGridColumnGapRem`
|
|
96
|
+
- `inlineGridRowGapRem`
|
|
97
|
+
- `inlineTemplateMarginTopRem`
|
|
98
|
+
- `inlineChoicesMarginTopRem`
|
|
13
99
|
|
|
14
100
|
## Session
|
|
15
101
|
|
|
16
102
|
- **`choiceId`**: selected choice id
|
|
17
103
|
|
|
104
|
+
## Implementation hints
|
|
105
|
+
|
|
106
|
+
- **Controller invariants:** in `populate_blank`, template must contain exactly one `{{blank}}`; in `audio_mc_only`, template must not contain `{{blank}}`.
|
|
107
|
+
- **Choice normalization:** choices are polymorphic by `choiceMode` (text via `labelHtml`, image via `imageUrl`/`imageAlt`).
|
|
108
|
+
- **Delivery rendering:** template is split around `{{blank}}`; selected choice content is rendered into the blank slot.
|
|
109
|
+
- **Layout limits are model-driven:** delivery reads `model.layoutLimits` (blank widths/underline widths, choice tile sizing, image max heights, listen button size, layout column minimums); defaults are CQT-informed but overrideable.
|
|
110
|
+
- **Evaluate mode behavior:** when evaluate/correct-answer mode is enabled, delivery can render `correctChoiceId` in the blank/choice state.
|
|
111
|
+
- **Audio error behavior:** no TTS fallback is used; when `hasAudio=true` and no playable `audioUrl` is provided, delivery shows an explicit error message (configurable via `uiText.audioResourceUnavailable`).
|
|
112
|
+
- **Print parity:** print view mirrors prompt/template/choice presentation with the same blank-token contract.
|
|
113
|
+
|
|
114
|
+
## Theming hooks (`pie-*` classes)
|
|
115
|
+
|
|
116
|
+
Delivery now exposes stable `pie-*` classes so hosts can theme this element with the same class-oriented approach used by other PIE elements.
|
|
117
|
+
|
|
118
|
+
- Root/container: `pie-element`, `pie-element-mc-populated-blank`, `pie-delivery-root`
|
|
119
|
+
- Prompt/audio/template: `pie-prompt`, `pie-audio-container`, `pie-audio-player`, `pie-audio-transcript`, `pie-template-line`, `pie-sentence-line`
|
|
120
|
+
- Blank display: `pie-blank-slot`, `pie-blank-slot-standalone`, `pie-blank-value`, `pie-blank-image`
|
|
121
|
+
- Choice group: `pie-choices-fieldset`, `pie-choices-legend`, `pie-choices`
|
|
122
|
+
- Choice rows/items: `pie-choice`, `pie-choice-horizontal`, `pie-choice-selected`, `pie-choice-label`, `pie-choice-image`
|
|
123
|
+
- Choice controls: `pie-choice-radio`, `pie-choice-radio-inline`, `pie-choice-radio-bottom`
|
|
124
|
+
- Evaluate/toggle feedback: `pie-toggle-correct-answer`, `pie-result-feedback`, `pie-choice-feedback-correct`, `pie-choice-feedback-incorrect`
|
|
125
|
+
|
|
18
126
|
## Builds
|
|
19
127
|
|
|
20
128
|
Same layout as `@pie-element/simple-cloze`: `delivery`, `controller`, `author`, `print`, plus IIFE bundle for script-tag loading.
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
/** Token authors must include exactly once in `template` (HTML). */
|
|
2
2
|
export declare const BLANK_TOKEN = "{{blank}}";
|
|
3
|
+
/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */
|
|
4
|
+
export declare const DEFAULT_LAYOUT_LIMITS: {
|
|
5
|
+
readonly blankStandaloneWidthRem: 7;
|
|
6
|
+
readonly blankWideWidthRem: 10;
|
|
7
|
+
readonly blankUnderlineWidthPx: 2;
|
|
8
|
+
readonly blankUnderlineWideWidthPx: 4;
|
|
9
|
+
readonly horizontalChoiceWidthPx: 170;
|
|
10
|
+
readonly horizontalChoiceWidthVw: 30;
|
|
11
|
+
readonly horizontalChoiceTileMinHeightRem: 11;
|
|
12
|
+
readonly horizontalChoiceContentMinHeightRem: 7.5;
|
|
13
|
+
readonly selectedImageMaxHeightRem: 4;
|
|
14
|
+
readonly choiceImageMaxHeightRem: 5;
|
|
15
|
+
readonly listenButtonSizePx: 128;
|
|
16
|
+
readonly stimulusMinColumnPx: 210;
|
|
17
|
+
readonly textMinColumnPx: 260;
|
|
18
|
+
readonly legendMaxChars: 120;
|
|
19
|
+
readonly choiceGroupGapRem: 0.5;
|
|
20
|
+
readonly choiceRowGapRem: 0.5;
|
|
21
|
+
readonly toggleButtonGapRem: 0.5;
|
|
22
|
+
readonly horizontalChoiceRadioTopMarginRem: 0.5;
|
|
23
|
+
readonly audioBlankTemplateMarginTopRem: 0.8;
|
|
24
|
+
readonly audioBlankTemplateMarginBottomRem: 1.8;
|
|
25
|
+
readonly stimulusGridColumnGapRem: 2;
|
|
26
|
+
readonly stimulusGridRowGapRem: 0.7;
|
|
27
|
+
readonly stimulusSentenceMarginTopRem: 0.2;
|
|
28
|
+
readonly stimulusChoicesMarginTopRem: 0.9;
|
|
29
|
+
readonly tokenGridColumnGapRem: 1.5;
|
|
30
|
+
readonly tokenGridRowGapRem: 0.8;
|
|
31
|
+
readonly tokenTemplateMarginTopRem: 0.6;
|
|
32
|
+
readonly tokenInlineTokenGapRem: 0.35;
|
|
33
|
+
readonly tokenChoicesMarginTopRem: 0.2;
|
|
34
|
+
readonly inlineGridColumnGapRem: 1.5;
|
|
35
|
+
readonly inlineGridRowGapRem: 0.65;
|
|
36
|
+
readonly inlineTemplateMarginTopRem: 0.45;
|
|
37
|
+
readonly inlineChoicesMarginTopRem: 0.25;
|
|
38
|
+
};
|
|
3
39
|
declare const _default: {
|
|
4
40
|
model: {
|
|
5
41
|
id: string;
|
|
@@ -9,6 +45,55 @@ declare const _default: {
|
|
|
9
45
|
interactionMode: "populate_blank";
|
|
10
46
|
layoutProfile: string;
|
|
11
47
|
choiceLayout: string;
|
|
48
|
+
layoutProfilePresets: {};
|
|
49
|
+
layoutLimits: {
|
|
50
|
+
blankStandaloneWidthRem: 7;
|
|
51
|
+
blankWideWidthRem: 10;
|
|
52
|
+
blankUnderlineWidthPx: 2;
|
|
53
|
+
blankUnderlineWideWidthPx: 4;
|
|
54
|
+
horizontalChoiceWidthPx: 170;
|
|
55
|
+
horizontalChoiceWidthVw: 30;
|
|
56
|
+
horizontalChoiceTileMinHeightRem: 11;
|
|
57
|
+
horizontalChoiceContentMinHeightRem: 7.5;
|
|
58
|
+
selectedImageMaxHeightRem: 4;
|
|
59
|
+
choiceImageMaxHeightRem: 5;
|
|
60
|
+
listenButtonSizePx: 128;
|
|
61
|
+
stimulusMinColumnPx: 210;
|
|
62
|
+
textMinColumnPx: 260;
|
|
63
|
+
legendMaxChars: 120;
|
|
64
|
+
choiceGroupGapRem: 0.5;
|
|
65
|
+
choiceRowGapRem: 0.5;
|
|
66
|
+
toggleButtonGapRem: 0.5;
|
|
67
|
+
horizontalChoiceRadioTopMarginRem: 0.5;
|
|
68
|
+
audioBlankTemplateMarginTopRem: 0.8;
|
|
69
|
+
audioBlankTemplateMarginBottomRem: 1.8;
|
|
70
|
+
stimulusGridColumnGapRem: 2;
|
|
71
|
+
stimulusGridRowGapRem: 0.7;
|
|
72
|
+
stimulusSentenceMarginTopRem: 0.2;
|
|
73
|
+
stimulusChoicesMarginTopRem: 0.9;
|
|
74
|
+
tokenGridColumnGapRem: 1.5;
|
|
75
|
+
tokenGridRowGapRem: 0.8;
|
|
76
|
+
tokenTemplateMarginTopRem: 0.6;
|
|
77
|
+
tokenInlineTokenGapRem: 0.35;
|
|
78
|
+
tokenChoicesMarginTopRem: 0.2;
|
|
79
|
+
inlineGridColumnGapRem: 1.5;
|
|
80
|
+
inlineGridRowGapRem: 0.65;
|
|
81
|
+
inlineTemplateMarginTopRem: 0.45;
|
|
82
|
+
inlineChoicesMarginTopRem: 0.25;
|
|
83
|
+
};
|
|
84
|
+
audioButtonSkin: null;
|
|
85
|
+
audioButtonSkinsByLocale: {};
|
|
86
|
+
uiText: {
|
|
87
|
+
answerChoices: string;
|
|
88
|
+
selectedAnswerInSentence: string;
|
|
89
|
+
showCorrectAnswer: string;
|
|
90
|
+
hideCorrectAnswer: string;
|
|
91
|
+
clickToEnableAutoplay: string;
|
|
92
|
+
audioResourceUnavailable: string;
|
|
93
|
+
transcriptLabel: string;
|
|
94
|
+
listenLabelEn: string;
|
|
95
|
+
listenLabelEs: string;
|
|
96
|
+
};
|
|
12
97
|
sentenceHtml: string;
|
|
13
98
|
template: string;
|
|
14
99
|
choiceMode: "text";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/controller/defaults.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,eAAO,MAAM,WAAW,cAAc,CAAC
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/controller/defaults.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,eAAO,MAAM,WAAW,cAAc,CAAC;AACvC,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCxB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEX,wBAiDE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controller/index.ts"],"names":[],"mappings":"AAGA,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,eAAO,MAAM,cAAc,GAAI,UAAU,GAAG,EAAE,SAAS,GAAG,2CASzD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,WAAW,GAAG,EAAE,SAAS,GAAG,UAK3D,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,uBAAqB,YAY5E,CAAC;AAEF,eAAO,MAAM,OAAO,GAAI,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,qBA+BzD,CAAC;AAEL,eAAO,MAAM,kBAAkB,GAAI,QAAO,GAAQ,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controller/index.ts"],"names":[],"mappings":"AAGA,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,eAAO,MAAM,cAAc,GAAI,UAAU,GAAG,EAAE,SAAS,GAAG,2CASzD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,WAAW,GAAG,EAAE,SAAS,GAAG,UAK3D,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,uBAAqB,YAY5E,CAAC;AAEF,eAAO,MAAM,OAAO,GAAI,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,qBA+BzD,CAAC;AAEL,eAAO,MAAM,kBAAkB,GAAI,QAAO,GAAQ,QAOhD,CAAC;AAEH,eAAO,MAAM,gBAAgB,GAAI,GAAG,GAAG,QAAe,CAAC;AAmEvD,eAAO,MAAM,KAAK,GAAU,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,EAAE,gBAAgB,GAAG,iBAoErF,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAI,UAAU,GAAG,EAAE,KAAK,GAAG,qBAYnE,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,QAAO,GAAQ,EAAE,UAAS,GAAQ,QAqH1D,CAAC"}
|
package/dist/controller/index.js
CHANGED
|
@@ -2,6 +2,42 @@ import { isEmpty } from "lodash-es";
|
|
|
2
2
|
//#region src/controller/defaults.ts
|
|
3
3
|
/** Token authors must include exactly once in `template` (HTML). */
|
|
4
4
|
var BLANK_TOKEN = "{{blank}}";
|
|
5
|
+
/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */
|
|
6
|
+
var DEFAULT_LAYOUT_LIMITS = {
|
|
7
|
+
blankStandaloneWidthRem: 7,
|
|
8
|
+
blankWideWidthRem: 10,
|
|
9
|
+
blankUnderlineWidthPx: 2,
|
|
10
|
+
blankUnderlineWideWidthPx: 4,
|
|
11
|
+
horizontalChoiceWidthPx: 170,
|
|
12
|
+
horizontalChoiceWidthVw: 30,
|
|
13
|
+
horizontalChoiceTileMinHeightRem: 11,
|
|
14
|
+
horizontalChoiceContentMinHeightRem: 7.5,
|
|
15
|
+
selectedImageMaxHeightRem: 4,
|
|
16
|
+
choiceImageMaxHeightRem: 5,
|
|
17
|
+
listenButtonSizePx: 128,
|
|
18
|
+
stimulusMinColumnPx: 210,
|
|
19
|
+
textMinColumnPx: 260,
|
|
20
|
+
legendMaxChars: 120,
|
|
21
|
+
choiceGroupGapRem: .5,
|
|
22
|
+
choiceRowGapRem: .5,
|
|
23
|
+
toggleButtonGapRem: .5,
|
|
24
|
+
horizontalChoiceRadioTopMarginRem: .5,
|
|
25
|
+
audioBlankTemplateMarginTopRem: .8,
|
|
26
|
+
audioBlankTemplateMarginBottomRem: 1.8,
|
|
27
|
+
stimulusGridColumnGapRem: 2,
|
|
28
|
+
stimulusGridRowGapRem: .7,
|
|
29
|
+
stimulusSentenceMarginTopRem: .2,
|
|
30
|
+
stimulusChoicesMarginTopRem: .9,
|
|
31
|
+
tokenGridColumnGapRem: 1.5,
|
|
32
|
+
tokenGridRowGapRem: .8,
|
|
33
|
+
tokenTemplateMarginTopRem: .6,
|
|
34
|
+
tokenInlineTokenGapRem: .35,
|
|
35
|
+
tokenChoicesMarginTopRem: .2,
|
|
36
|
+
inlineGridColumnGapRem: 1.5,
|
|
37
|
+
inlineGridRowGapRem: .65,
|
|
38
|
+
inlineTemplateMarginTopRem: .45,
|
|
39
|
+
inlineChoicesMarginTopRem: .25
|
|
40
|
+
};
|
|
5
41
|
var defaults_default = {
|
|
6
42
|
model: {
|
|
7
43
|
id: "1",
|
|
@@ -11,6 +47,21 @@ var defaults_default = {
|
|
|
11
47
|
interactionMode: "populate_blank",
|
|
12
48
|
layoutProfile: "",
|
|
13
49
|
choiceLayout: "",
|
|
50
|
+
layoutProfilePresets: {},
|
|
51
|
+
layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },
|
|
52
|
+
audioButtonSkin: null,
|
|
53
|
+
audioButtonSkinsByLocale: {},
|
|
54
|
+
uiText: {
|
|
55
|
+
answerChoices: "Answer choices",
|
|
56
|
+
selectedAnswerInSentence: "Selected answer in sentence",
|
|
57
|
+
showCorrectAnswer: "Show correct answer",
|
|
58
|
+
hideCorrectAnswer: "Hide correct answer",
|
|
59
|
+
clickToEnableAutoplay: "Click to enable audio autoplay",
|
|
60
|
+
audioResourceUnavailable: "Audio is enabled but no playable audio URL is configured.",
|
|
61
|
+
transcriptLabel: "Transcript",
|
|
62
|
+
listenLabelEn: "Listen",
|
|
63
|
+
listenLabelEs: "Escuchar"
|
|
64
|
+
},
|
|
14
65
|
sentenceHtml: "",
|
|
15
66
|
template: `<p>The answer is ${BLANK_TOKEN}.</p>`,
|
|
16
67
|
choiceMode: "text",
|
|
@@ -100,7 +151,11 @@ var outcome = (question, session, env) => new Promise((resolve) => {
|
|
|
100
151
|
});
|
|
101
152
|
var createDefaultModel = (model = {}) => ({
|
|
102
153
|
...defaults_default.model,
|
|
103
|
-
...model
|
|
154
|
+
...model,
|
|
155
|
+
layoutLimits: {
|
|
156
|
+
...DEFAULT_LAYOUT_LIMITS,
|
|
157
|
+
...model?.layoutLimits || {}
|
|
158
|
+
}
|
|
104
159
|
});
|
|
105
160
|
var normalizeSession = (s) => ({ ...s });
|
|
106
161
|
var shouldShuffleChoices = (question) => !!question?.shuffle;
|
|
@@ -147,6 +202,14 @@ var model = async (question, session, env, updateSession) => {
|
|
|
147
202
|
interactionMode: normalizedQuestion.interactionMode || "populate_blank",
|
|
148
203
|
layoutProfile: normalizedQuestion.layoutProfile || "",
|
|
149
204
|
choiceLayout: normalizedQuestion.choiceLayout || "",
|
|
205
|
+
layoutProfilePresets: normalizedQuestion.layoutProfilePresets && typeof normalizedQuestion.layoutProfilePresets === "object" ? normalizedQuestion.layoutProfilePresets : {},
|
|
206
|
+
layoutLimits: {
|
|
207
|
+
...DEFAULT_LAYOUT_LIMITS,
|
|
208
|
+
...normalizedQuestion.layoutLimits || {}
|
|
209
|
+
},
|
|
210
|
+
audioButtonSkin: normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === "object" ? normalizedQuestion.audioButtonSkin : null,
|
|
211
|
+
audioButtonSkinsByLocale: normalizedQuestion.audioButtonSkinsByLocale && typeof normalizedQuestion.audioButtonSkinsByLocale === "object" ? normalizedQuestion.audioButtonSkinsByLocale : {},
|
|
212
|
+
uiText: normalizedQuestion.uiText && typeof normalizedQuestion.uiText === "object" ? normalizedQuestion.uiText : {},
|
|
150
213
|
sentenceHtml: normalizedQuestion.sentenceHtml || null,
|
|
151
214
|
template: normalizedQuestion.template,
|
|
152
215
|
choiceMode: normalizedQuestion.choiceMode,
|
|
@@ -157,6 +220,7 @@ var model = async (question, session, env, updateSession) => {
|
|
|
157
220
|
audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,
|
|
158
221
|
audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,
|
|
159
222
|
showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,
|
|
223
|
+
useFeatureButtonAudio: typeof normalizedQuestion.useFeatureButtonAudio === "boolean" ? normalizedQuestion.useFeatureButtonAudio : void 0,
|
|
160
224
|
locale: normalizedQuestion.locale || "",
|
|
161
225
|
disabled: env.mode !== "gather",
|
|
162
226
|
view: env.mode === "view",
|
|
@@ -225,9 +289,50 @@ var validate = (model = {}, _config = {}) => {
|
|
|
225
289
|
const correct = model.correctChoiceId;
|
|
226
290
|
if (!correct || !choices.some((c) => c.id === correct)) errors.correctChoiceId = "Correct choice must match one of the choice ids";
|
|
227
291
|
if (model.hasAudio) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
292
|
+
if (!!!model.audioUrl?.trim()) errors.audioUrl = "A playable audio URL is required when audio is enabled";
|
|
293
|
+
}
|
|
294
|
+
const limits = model?.layoutLimits;
|
|
295
|
+
if (limits && typeof limits === "object") for (const key of [
|
|
296
|
+
"blankStandaloneWidthRem",
|
|
297
|
+
"blankWideWidthRem",
|
|
298
|
+
"blankUnderlineWidthPx",
|
|
299
|
+
"blankUnderlineWideWidthPx",
|
|
300
|
+
"horizontalChoiceWidthPx",
|
|
301
|
+
"horizontalChoiceWidthVw",
|
|
302
|
+
"horizontalChoiceTileMinHeightRem",
|
|
303
|
+
"horizontalChoiceContentMinHeightRem",
|
|
304
|
+
"selectedImageMaxHeightRem",
|
|
305
|
+
"choiceImageMaxHeightRem",
|
|
306
|
+
"listenButtonSizePx",
|
|
307
|
+
"stimulusMinColumnPx",
|
|
308
|
+
"textMinColumnPx",
|
|
309
|
+
"legendMaxChars",
|
|
310
|
+
"choiceGroupGapRem",
|
|
311
|
+
"choiceRowGapRem",
|
|
312
|
+
"toggleButtonGapRem",
|
|
313
|
+
"horizontalChoiceRadioTopMarginRem",
|
|
314
|
+
"audioBlankTemplateMarginTopRem",
|
|
315
|
+
"audioBlankTemplateMarginBottomRem",
|
|
316
|
+
"stimulusGridColumnGapRem",
|
|
317
|
+
"stimulusGridRowGapRem",
|
|
318
|
+
"stimulusSentenceMarginTopRem",
|
|
319
|
+
"stimulusChoicesMarginTopRem",
|
|
320
|
+
"tokenGridColumnGapRem",
|
|
321
|
+
"tokenGridRowGapRem",
|
|
322
|
+
"tokenTemplateMarginTopRem",
|
|
323
|
+
"tokenInlineTokenGapRem",
|
|
324
|
+
"tokenChoicesMarginTopRem",
|
|
325
|
+
"inlineGridColumnGapRem",
|
|
326
|
+
"inlineGridRowGapRem",
|
|
327
|
+
"inlineTemplateMarginTopRem",
|
|
328
|
+
"inlineChoicesMarginTopRem"
|
|
329
|
+
]) {
|
|
330
|
+
if (limits[key] === void 0 || limits[key] === null || limits[key] === "") continue;
|
|
331
|
+
const value = Number(limits[key]);
|
|
332
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
333
|
+
errors.layoutLimits = `${key} must be a positive number when provided`;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
231
336
|
}
|
|
232
337
|
return errors;
|
|
233
338
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/controller/defaults.ts","../../src/controller/index.ts"],"sourcesContent":["/** Token authors must include exactly once in `template` (HTML). */\nexport const BLANK_TOKEN = '{{blank}}';\n\nexport default {\n model: {\n id: '1',\n element: 'mc-populated-blank',\n prompt: '<p><strong>Demo prompt</strong></p>',\n promptEnabled: true,\n interactionMode: 'populate_blank' as const,\n layoutProfile: '',\n choiceLayout: '',\n sentenceHtml: '',\n template: `<p>The answer is ${BLANK_TOKEN}.</p>`,\n choiceMode: 'text' as const,\n choices: [\n { id: 'c1', labelHtml: '<p>Option A</p>' },\n { id: 'c2', labelHtml: '<p>Option B</p>' },\n { id: 'c3', labelHtml: '<p>Option C</p>' },\n ],\n correctChoiceId: 'c2',\n hasAudio: false,\n autoplayAudioEnabled: false,\n completeAudioEnabled: false,\n audioUrl: '',\n audioTranscript: '',\n showVisibleTranscript: false,\n locale: '',\n teacherInstructions: '',\n teacherInstructionsEnabled: false,\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import { isEmpty } from 'lodash-es';\nimport defaults, { BLANK_TOKEN } from './defaults';\n\nexport function countBlankTokens(template: string): number {\n if (!template) return 0;\n const re = /\\{\\{blank\\}\\}/g;\n return (template.match(re) || []).length;\n}\n\nexport const getCorrectness = (question: any, session: any) => {\n if (!session || !session.choiceId) {\n return 'unanswered';\n }\n const correct = question?.correctChoiceId || '';\n if (session.choiceId === correct) {\n return 'correct';\n }\n return 'incorrect';\n};\n\nexport const getPartialScore = (_question: any, session: any) => {\n if (!session || isEmpty(session) || !session.choiceId) {\n return 0;\n }\n return 1;\n};\n\nexport const isComplete = (question: any, session: any, audioComplete = false) => {\n if (!session || !session.choiceId) {\n return false;\n }\n const autoplayAudioEnabled = !!question?.autoplayAudioEnabled;\n const completeAudioEnabled = !!question?.completeAudioEnabled;\n const requiresAudioCompletion =\n autoplayAudioEnabled && completeAudioEnabled && !!question?.hasAudio;\n if (requiresAudioCompletion && !audioComplete) {\n return false;\n }\n return true;\n};\n\nexport const outcome = (question: any, session: any, env: any) =>\n new Promise((resolve) => {\n if (!session || isEmpty(session)) {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n session = normalizeSession(session);\n const correctness = getCorrectness(question, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n const score = correctness === 'correct' ? 1 : 0;\n const traceLog = [\n `Mode: ${env?.mode || 'unknown'}.`,\n `Student selected choice: ${session.choiceId}.`,\n `Correct choice: ${question?.correctChoiceId || 'none'}.`,\n `Final score: ${score}.`,\n ];\n resolve({ score, empty: false, traceLog });\n });\n\nexport const createDefaultModel = (model: any = {}) => ({ ...defaults.model, ...model });\n\nexport const normalizeSession = (s: any) => ({ ...s });\n\nconst shouldShuffleChoices = (question: any) => !!question?.shuffle;\n\nconst shouldLockChoices = (question: any, env: any) => {\n if (question?.lockChoiceOrder) return true;\n if (env?.['@pie-element']?.lockChoiceOrder) return true;\n return env?.role === 'instructor';\n};\n\nconst shuffleArray = <T>(items: T[]): T[] => {\n const out = [...items];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [out[i], out[j]] = [out[j], out[i]];\n }\n return out;\n};\n\nconst getStoredShuffle = (session: any): string[] =>\n Array.isArray(session?.data?.shuffledValues)\n ? session.data.shuffledValues\n : Array.isArray(session?.shuffledValues)\n ? session.shuffledValues\n : [];\n\nconst applyShuffledValues = (choices: any[], shuffledValues: string[], choiceKey: string) => {\n const orderedChoices = shuffledValues\n .map((value) => choices.find((choice) => choice?.[choiceKey] === value))\n .filter(Boolean);\n\n if (orderedChoices.length === choices.length) {\n return orderedChoices;\n }\n\n const orderedValues = new Set(orderedChoices.map((choice: any) => choice[choiceKey]));\n const leftovers = choices.filter((choice) => !orderedValues.has(choice?.[choiceKey]));\n return [...orderedChoices, ...leftovers];\n};\n\nconst getOrderedChoices = async (question: any, session: any, env: any, updateSession?: any) => {\n const choices = Array.isArray(question?.choices) ? [...question.choices] : [];\n if (!choices.length || !shouldShuffleChoices(question)) {\n return choices;\n }\n\n if (shouldLockChoices(question, env || {})) {\n return choices;\n }\n\n const shuffledValues = getStoredShuffle(session);\n if (shuffledValues.length) {\n return applyShuffledValues(choices, shuffledValues, 'id');\n }\n\n const shuffledChoices = shuffleArray(choices);\n\n if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {\n const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);\n if (shuffledIds.length) {\n await updateSession(session.id, session.element, { shuffledValues: shuffledIds });\n }\n }\n\n return shuffledChoices;\n};\n\nexport const model = async (question: any, session: any, env: any, updateSession?: any) => {\n session = session || {};\n const normalizedQuestion = createDefaultModel(question);\n const choices = await getOrderedChoices(normalizedQuestion, session, env, updateSession);\n\n const out: any = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n interactionMode: normalizedQuestion.interactionMode || 'populate_blank',\n layoutProfile: normalizedQuestion.layoutProfile || '',\n choiceLayout: normalizedQuestion.choiceLayout || '',\n sentenceHtml: normalizedQuestion.sentenceHtml || null,\n template: normalizedQuestion.template,\n choiceMode: normalizedQuestion.choiceMode,\n choices,\n hasAudio: normalizedQuestion.hasAudio,\n autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,\n completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,\n audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,\n audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,\n showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,\n locale: normalizedQuestion.locale || '',\n disabled: env.mode !== 'gather',\n view: env.mode === 'view',\n mode: env.mode,\n };\n\n if (env.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, session);\n out.correctness = correctness;\n out.responseCorrect = correctness === 'correct';\n out.correctChoiceId = normalizedQuestion.correctChoiceId;\n }\n\n if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n return out;\n};\n\nexport const createCorrectResponseSession = (question: any, env: any) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n resolve({\n id: '1',\n element: 'mc-populated-blank',\n choiceId: question?.correctChoiceId || '',\n });\n } else {\n resolve(null);\n }\n });\n};\n\nexport const validate = (model: any = {}, _config: any = {}) => {\n const errors: any = {};\n\n if (model.promptEnabled) {\n const p = model.prompt?.trim() || '';\n if (!p || p === '<p></p>') {\n errors.prompt = 'Prompt is required when prompt is enabled';\n }\n }\n\n const interactionMode = model.interactionMode || 'populate_blank';\n const template = model.template || '';\n const n = countBlankTokens(template);\n if (interactionMode === 'audio_mc_only') {\n if (n > 0) {\n errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;\n }\n } else if (interactionMode === 'populate_blank') {\n if (n === 0) {\n errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;\n } else if (n > 1) {\n errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;\n }\n } else {\n errors.interactionMode = 'Unknown interaction mode';\n }\n\n const choices = Array.isArray(model.choices) ? model.choices : [];\n if (choices.length < 2) {\n errors.choices = 'At least two choices are required';\n }\n\n const mode = model.choiceMode || 'text';\n for (let i = 0; i < choices.length; i++) {\n const c = choices[i];\n if (!c?.id) {\n errors.choices = `Choice ${i + 1} is missing an id`;\n break;\n }\n if (mode === 'text') {\n const lbl = (c.labelHtml || '').trim();\n if (!lbl || lbl === '<p></p>') {\n errors.choices = `Choice ${i + 1} needs label text`;\n break;\n }\n } else {\n if (!c.imageUrl?.trim()) {\n errors.choices = `Choice ${i + 1} needs an image URL`;\n break;\n }\n if (!c.imageAlt?.trim()) {\n errors.choices = `Choice ${i + 1} needs image alt text`;\n break;\n }\n }\n }\n\n const correct = model.correctChoiceId;\n if (!correct || !choices.some((c: any) => c.id === correct)) {\n errors.correctChoiceId = 'Correct choice must match one of the choice ids';\n }\n\n if (model.hasAudio) {\n const hasAudioUrl = !!model.audioUrl?.trim();\n const hasTranscript = !!model.audioTranscript?.trim();\n if (!hasAudioUrl && !hasTranscript) {\n errors.audioTranscript = 'Audio transcript or audio URL is required when audio is enabled';\n }\n }\n\n return errors;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/controller/defaults.ts","../../src/controller/index.ts"],"sourcesContent":["/** Token authors must include exactly once in `template` (HTML). */\nexport const BLANK_TOKEN = '{{blank}}';\n/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */\nexport const DEFAULT_LAYOUT_LIMITS = {\n blankStandaloneWidthRem: 7,\n blankWideWidthRem: 10,\n blankUnderlineWidthPx: 2,\n blankUnderlineWideWidthPx: 4,\n horizontalChoiceWidthPx: 170,\n horizontalChoiceWidthVw: 30,\n horizontalChoiceTileMinHeightRem: 11,\n horizontalChoiceContentMinHeightRem: 7.5,\n selectedImageMaxHeightRem: 4,\n choiceImageMaxHeightRem: 5,\n listenButtonSizePx: 128,\n stimulusMinColumnPx: 210,\n textMinColumnPx: 260,\n legendMaxChars: 120,\n choiceGroupGapRem: 0.5,\n choiceRowGapRem: 0.5,\n toggleButtonGapRem: 0.5,\n horizontalChoiceRadioTopMarginRem: 0.5,\n audioBlankTemplateMarginTopRem: 0.8,\n audioBlankTemplateMarginBottomRem: 1.8,\n stimulusGridColumnGapRem: 2,\n stimulusGridRowGapRem: 0.7,\n stimulusSentenceMarginTopRem: 0.2,\n stimulusChoicesMarginTopRem: 0.9,\n tokenGridColumnGapRem: 1.5,\n tokenGridRowGapRem: 0.8,\n tokenTemplateMarginTopRem: 0.6,\n tokenInlineTokenGapRem: 0.35,\n tokenChoicesMarginTopRem: 0.2,\n inlineGridColumnGapRem: 1.5,\n inlineGridRowGapRem: 0.65,\n inlineTemplateMarginTopRem: 0.45,\n inlineChoicesMarginTopRem: 0.25,\n} as const;\n\nexport default {\n model: {\n id: '1',\n element: 'mc-populated-blank',\n prompt: '<p><strong>Demo prompt</strong></p>',\n promptEnabled: true,\n interactionMode: 'populate_blank' as const,\n layoutProfile: '',\n choiceLayout: '',\n layoutProfilePresets: {},\n layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin: null,\n audioButtonSkinsByLocale: {},\n uiText: {\n answerChoices: 'Answer choices',\n selectedAnswerInSentence: 'Selected answer in sentence',\n showCorrectAnswer: 'Show correct answer',\n hideCorrectAnswer: 'Hide correct answer',\n clickToEnableAutoplay: 'Click to enable audio autoplay',\n audioResourceUnavailable: 'Audio is enabled but no playable audio URL is configured.',\n transcriptLabel: 'Transcript',\n listenLabelEn: 'Listen',\n listenLabelEs: 'Escuchar',\n },\n sentenceHtml: '',\n template: `<p>The answer is ${BLANK_TOKEN}.</p>`,\n choiceMode: 'text' as const,\n choices: [\n { id: 'c1', labelHtml: '<p>Option A</p>' },\n { id: 'c2', labelHtml: '<p>Option B</p>' },\n { id: 'c3', labelHtml: '<p>Option C</p>' },\n ],\n correctChoiceId: 'c2',\n hasAudio: false,\n autoplayAudioEnabled: false,\n completeAudioEnabled: false,\n audioUrl: '',\n audioTranscript: '',\n showVisibleTranscript: false,\n locale: '',\n teacherInstructions: '',\n teacherInstructionsEnabled: false,\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import { isEmpty } from 'lodash-es';\nimport defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults';\n\nexport function countBlankTokens(template: string): number {\n if (!template) return 0;\n const re = /\\{\\{blank\\}\\}/g;\n return (template.match(re) || []).length;\n}\n\nexport const getCorrectness = (question: any, session: any) => {\n if (!session || !session.choiceId) {\n return 'unanswered';\n }\n const correct = question?.correctChoiceId || '';\n if (session.choiceId === correct) {\n return 'correct';\n }\n return 'incorrect';\n};\n\nexport const getPartialScore = (_question: any, session: any) => {\n if (!session || isEmpty(session) || !session.choiceId) {\n return 0;\n }\n return 1;\n};\n\nexport const isComplete = (question: any, session: any, audioComplete = false) => {\n if (!session || !session.choiceId) {\n return false;\n }\n const autoplayAudioEnabled = !!question?.autoplayAudioEnabled;\n const completeAudioEnabled = !!question?.completeAudioEnabled;\n const requiresAudioCompletion =\n autoplayAudioEnabled && completeAudioEnabled && !!question?.hasAudio;\n if (requiresAudioCompletion && !audioComplete) {\n return false;\n }\n return true;\n};\n\nexport const outcome = (question: any, session: any, env: any) =>\n new Promise((resolve) => {\n if (!session || isEmpty(session)) {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n session = normalizeSession(session);\n const correctness = getCorrectness(question, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n const score = correctness === 'correct' ? 1 : 0;\n const traceLog = [\n `Mode: ${env?.mode || 'unknown'}.`,\n `Student selected choice: ${session.choiceId}.`,\n `Correct choice: ${question?.correctChoiceId || 'none'}.`,\n `Final score: ${score}.`,\n ];\n resolve({ score, empty: false, traceLog });\n });\n\nexport const createDefaultModel = (model: any = {}) => ({\n ...defaults.model,\n ...model,\n layoutLimits: {\n ...DEFAULT_LAYOUT_LIMITS,\n ...(model?.layoutLimits || {}),\n },\n});\n\nexport const normalizeSession = (s: any) => ({ ...s });\n\nconst shouldShuffleChoices = (question: any) => !!question?.shuffle;\n\nconst shouldLockChoices = (question: any, env: any) => {\n if (question?.lockChoiceOrder) return true;\n if (env?.['@pie-element']?.lockChoiceOrder) return true;\n return env?.role === 'instructor';\n};\n\nconst shuffleArray = <T>(items: T[]): T[] => {\n const out = [...items];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [out[i], out[j]] = [out[j], out[i]];\n }\n return out;\n};\n\nconst getStoredShuffle = (session: any): string[] =>\n Array.isArray(session?.data?.shuffledValues)\n ? session.data.shuffledValues\n : Array.isArray(session?.shuffledValues)\n ? session.shuffledValues\n : [];\n\nconst applyShuffledValues = (choices: any[], shuffledValues: string[], choiceKey: string) => {\n const orderedChoices = shuffledValues\n .map((value) => choices.find((choice) => choice?.[choiceKey] === value))\n .filter(Boolean);\n\n if (orderedChoices.length === choices.length) {\n return orderedChoices;\n }\n\n const orderedValues = new Set(orderedChoices.map((choice: any) => choice[choiceKey]));\n const leftovers = choices.filter((choice) => !orderedValues.has(choice?.[choiceKey]));\n return [...orderedChoices, ...leftovers];\n};\n\nconst getOrderedChoices = async (question: any, session: any, env: any, updateSession?: any) => {\n const choices = Array.isArray(question?.choices) ? [...question.choices] : [];\n if (!choices.length || !shouldShuffleChoices(question)) {\n return choices;\n }\n\n if (shouldLockChoices(question, env || {})) {\n return choices;\n }\n\n const shuffledValues = getStoredShuffle(session);\n if (shuffledValues.length) {\n return applyShuffledValues(choices, shuffledValues, 'id');\n }\n\n const shuffledChoices = shuffleArray(choices);\n\n if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {\n const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);\n if (shuffledIds.length) {\n await updateSession(session.id, session.element, { shuffledValues: shuffledIds });\n }\n }\n\n return shuffledChoices;\n};\n\nexport const model = async (question: any, session: any, env: any, updateSession?: any) => {\n session = session || {};\n const normalizedQuestion = createDefaultModel(question);\n const choices = await getOrderedChoices(normalizedQuestion, session, env, updateSession);\n\n const out: any = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n interactionMode: normalizedQuestion.interactionMode || 'populate_blank',\n layoutProfile: normalizedQuestion.layoutProfile || '',\n choiceLayout: normalizedQuestion.choiceLayout || '',\n layoutProfilePresets:\n normalizedQuestion.layoutProfilePresets &&\n typeof normalizedQuestion.layoutProfilePresets === 'object'\n ? normalizedQuestion.layoutProfilePresets\n : {},\n layoutLimits: {\n ...DEFAULT_LAYOUT_LIMITS,\n ...(normalizedQuestion.layoutLimits || {}),\n },\n audioButtonSkin:\n normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'\n ? normalizedQuestion.audioButtonSkin\n : null,\n audioButtonSkinsByLocale:\n normalizedQuestion.audioButtonSkinsByLocale &&\n typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'\n ? normalizedQuestion.audioButtonSkinsByLocale\n : {},\n uiText:\n normalizedQuestion.uiText && typeof normalizedQuestion.uiText === 'object'\n ? normalizedQuestion.uiText\n : {},\n sentenceHtml: normalizedQuestion.sentenceHtml || null,\n template: normalizedQuestion.template,\n choiceMode: normalizedQuestion.choiceMode,\n choices,\n hasAudio: normalizedQuestion.hasAudio,\n autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,\n completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,\n audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,\n audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,\n showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,\n useFeatureButtonAudio:\n typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'\n ? normalizedQuestion.useFeatureButtonAudio\n : undefined,\n locale: normalizedQuestion.locale || '',\n disabled: env.mode !== 'gather',\n view: env.mode === 'view',\n mode: env.mode,\n };\n\n if (env.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, session);\n out.correctness = correctness;\n out.responseCorrect = correctness === 'correct';\n out.correctChoiceId = normalizedQuestion.correctChoiceId;\n }\n\n if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n return out;\n};\n\nexport const createCorrectResponseSession = (question: any, env: any) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n resolve({\n id: '1',\n element: 'mc-populated-blank',\n choiceId: question?.correctChoiceId || '',\n });\n } else {\n resolve(null);\n }\n });\n};\n\nexport const validate = (model: any = {}, _config: any = {}) => {\n const errors: any = {};\n\n if (model.promptEnabled) {\n const p = model.prompt?.trim() || '';\n if (!p || p === '<p></p>') {\n errors.prompt = 'Prompt is required when prompt is enabled';\n }\n }\n\n const interactionMode = model.interactionMode || 'populate_blank';\n const template = model.template || '';\n const n = countBlankTokens(template);\n if (interactionMode === 'audio_mc_only') {\n if (n > 0) {\n errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;\n }\n } else if (interactionMode === 'populate_blank') {\n if (n === 0) {\n errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;\n } else if (n > 1) {\n errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;\n }\n } else {\n errors.interactionMode = 'Unknown interaction mode';\n }\n\n const choices = Array.isArray(model.choices) ? model.choices : [];\n if (choices.length < 2) {\n errors.choices = 'At least two choices are required';\n }\n\n const mode = model.choiceMode || 'text';\n for (let i = 0; i < choices.length; i++) {\n const c = choices[i];\n if (!c?.id) {\n errors.choices = `Choice ${i + 1} is missing an id`;\n break;\n }\n if (mode === 'text') {\n const lbl = (c.labelHtml || '').trim();\n if (!lbl || lbl === '<p></p>') {\n errors.choices = `Choice ${i + 1} needs label text`;\n break;\n }\n } else {\n if (!c.imageUrl?.trim()) {\n errors.choices = `Choice ${i + 1} needs an image URL`;\n break;\n }\n if (!c.imageAlt?.trim()) {\n errors.choices = `Choice ${i + 1} needs image alt text`;\n break;\n }\n }\n }\n\n const correct = model.correctChoiceId;\n if (!correct || !choices.some((c: any) => c.id === correct)) {\n errors.correctChoiceId = 'Correct choice must match one of the choice ids';\n }\n\n if (model.hasAudio) {\n const hasAudioUrl = !!model.audioUrl?.trim();\n if (!hasAudioUrl) {\n errors.audioUrl = 'A playable audio URL is required when audio is enabled';\n }\n }\n\n const limits = model?.layoutLimits;\n if (limits && typeof limits === 'object') {\n const numericLimitKeys = [\n 'blankStandaloneWidthRem',\n 'blankWideWidthRem',\n 'blankUnderlineWidthPx',\n 'blankUnderlineWideWidthPx',\n 'horizontalChoiceWidthPx',\n 'horizontalChoiceWidthVw',\n 'horizontalChoiceTileMinHeightRem',\n 'horizontalChoiceContentMinHeightRem',\n 'selectedImageMaxHeightRem',\n 'choiceImageMaxHeightRem',\n 'listenButtonSizePx',\n 'stimulusMinColumnPx',\n 'textMinColumnPx',\n 'legendMaxChars',\n 'choiceGroupGapRem',\n 'choiceRowGapRem',\n 'toggleButtonGapRem',\n 'horizontalChoiceRadioTopMarginRem',\n 'audioBlankTemplateMarginTopRem',\n 'audioBlankTemplateMarginBottomRem',\n 'stimulusGridColumnGapRem',\n 'stimulusGridRowGapRem',\n 'stimulusSentenceMarginTopRem',\n 'stimulusChoicesMarginTopRem',\n 'tokenGridColumnGapRem',\n 'tokenGridRowGapRem',\n 'tokenTemplateMarginTopRem',\n 'tokenInlineTokenGapRem',\n 'tokenChoicesMarginTopRem',\n 'inlineGridColumnGapRem',\n 'inlineGridRowGapRem',\n 'inlineTemplateMarginTopRem',\n 'inlineChoicesMarginTopRem',\n ];\n for (const key of numericLimitKeys) {\n if (limits[key] === undefined || limits[key] === null || limits[key] === '') continue;\n const value = Number(limits[key]);\n if (!Number.isFinite(value) || value <= 0) {\n errors.layoutLimits = `${key} must be a positive number when provided`;\n break;\n }\n }\n }\n\n return errors;\n};\n"],"mappings":"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"}
|