@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 CHANGED
@@ -1,20 +1,128 @@
1
1
  # @pie-element/mc-populated-blank
2
2
 
3
- Svelte 5 PIE element: the learner picks a **multiple-choice** option and that choice **fills a blank** inside an HTML **template**.
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`** (optional)
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEvC,wBAkCE"}
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,QAAsC,CAAC;AAEzF,eAAO,MAAM,gBAAgB,GAAI,GAAG,GAAG,QAAe,CAAC;AAmEvD,eAAO,MAAM,KAAK,GAAU,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,EAAE,gBAAgB,GAAG,iBA0CrF,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAI,UAAU,GAAG,EAAE,KAAK,GAAG,qBAYnE,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,QAAO,GAAQ,EAAE,UAAS,GAAQ,QAuE1D,CAAC"}
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"}
@@ -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
- const hasAudioUrl = !!model.audioUrl?.trim();
229
- const hasTranscript = !!model.audioTranscript?.trim();
230
- if (!hasAudioUrl && !hasTranscript) errors.audioTranscript = "Audio transcript or audio URL is required when audio is enabled";
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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"}