@pie-element/mc-populated-blank 0.2.7 → 0.2.9

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
@@ -133,6 +133,16 @@ Delivery now exposes stable `pie-*` classes so hosts can theme this element with
133
133
 
134
134
  Same layout as `@pie-element/simple-cloze`: `delivery`, `controller`, `author`, `print`, plus IIFE bundle for script-tag loading.
135
135
 
136
+ ### Builder Compatibility Note
137
+
138
+ This package intentionally ships a root `controller.js` shim:
139
+
140
+ ```js
141
+ export * from './dist/controller/index.js';
142
+ ```
143
+
144
+ Reason: `pie-api-aws` client/editor bundling may resolve `@pie-element/<name>/controller` as a filesystem path rooted at the element package alias. Keeping this shim in the published package ensures compatibility there while `exports["./controller"]` remains the canonical ESM subpath entry.
145
+
136
146
  ```bash
137
147
  bun run build
138
148
  ```
package/controller.js ADDED
@@ -0,0 +1 @@
1
+ export * from './dist/controller/index.js';
@@ -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,QAOhD,CAAC;AAEH,eAAO,MAAM,SAAS,GAAI,WAAU,GAAQ,QAAiC,CAAC;AAE9E,eAAO,MAAM,gBAAgB,GAAI,GAAG,GAAG,QAAe,CAAC;AAmEvD,eAAO,MAAM,KAAK,GAAU,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,EAAE,gBAAgB,GAAG,iBAiErF,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAI,UAAU,GAAG,EAAE,KAAK,GAAG,qBAYnE,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,QAAO,GAAQ,EAAE,UAAS,GAAQ,QAuH1D,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controller/index.ts"],"names":[],"mappings":"AAQA,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,SAAS,GAAI,WAAU,GAAQ,QAAiC,CAAC;AAE9E,eAAO,MAAM,gBAAgB,GAAI,GAAG,GAAG,QAAe,CAAC;AAmEvD,eAAO,MAAM,KAAK,GAAU,UAAU,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,GAAG,EAAE,gBAAgB,GAAG,iBAiErF,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAI,UAAU,GAAG,EAAE,KAAK,GAAG,qBAYnE,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,QAAO,GAAQ,EAAE,UAAS,GAAQ,QAuH1D,CAAC"}
@@ -1,4 +1,3 @@
1
- import { isEmpty } from "lodash-es";
2
1
  //#region src/controller/defaults.ts
3
2
  /** Token authors must include exactly once in `template` (HTML). */
4
3
  var BLANK_TOKEN = "{{blank}}";
@@ -99,6 +98,7 @@ var defaults_default = {
99
98
  };
100
99
  //#endregion
101
100
  //#region src/controller/index.ts
101
+ var isEmptyObject = (value) => !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
102
102
  function countBlankTokens(template) {
103
103
  if (!template) return 0;
104
104
  return (template.match(/\{\{blank\}\}/g) || []).length;
@@ -110,7 +110,7 @@ var getCorrectness = (question, session) => {
110
110
  return "incorrect";
111
111
  };
112
112
  var getPartialScore = (_question, session) => {
113
- if (!session || isEmpty(session) || !session.choiceId) return 0;
113
+ if (!session || isEmptyObject(session) || !session.choiceId) return 0;
114
114
  return 1;
115
115
  };
116
116
  var isComplete = (question, session, audioComplete = false) => {
@@ -121,7 +121,7 @@ var isComplete = (question, session, audioComplete = false) => {
121
121
  return true;
122
122
  };
123
123
  var outcome = (question, session, env) => new Promise((resolve) => {
124
- if (!session || isEmpty(session)) {
124
+ if (!session || isEmptyObject(session)) {
125
125
  resolve({
126
126
  score: 0,
127
127
  empty: true,
@@ -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/** 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 audioInstructionsMaxWidthPx: 875,\n narrowHorizontalChoiceMaxWidthPx: 230,\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 normalize = (question: any = {}) => createDefaultModel(question);\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\nfunction 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 safeEnv = env || {};\n const normalizedQuestion = normalize(question);\n const choices = await getOrderedChoices(normalizedQuestion, session, safeEnv, 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: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },\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: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n };\n\n if (safeEnv.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 (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.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 'audioInstructionsMaxWidthPx',\n 'narrowHorizontalChoiceMaxWidthPx',\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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"}
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 audioInstructionsMaxWidthPx: 875,\n narrowHorizontalChoiceMaxWidthPx: 230,\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 defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults';\n\nconst isEmptyObject = (value: unknown): boolean =>\n !!value &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0;\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 || isEmptyObject(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 || isEmptyObject(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 normalize = (question: any = {}) => createDefaultModel(question);\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\nfunction 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 safeEnv = env || {};\n const normalizedQuestion = normalize(question);\n const choices = await getOrderedChoices(normalizedQuestion, session, safeEnv, 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: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },\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: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n };\n\n if (safeEnv.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 (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.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 'audioInstructionsMaxWidthPx',\n 'narrowHorizontalChoiceMaxWidthPx',\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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"}
@@ -5423,12 +5423,7 @@ function McPopulatedBlank($$anchor, $$props) {
5423
5423
  const activeSelectedId = String(get(selectedId) || "");
5424
5424
  if (!get(isEvaluateMode) || !correctChoiceId) return map;
5425
5425
  if (get(showCorrectAnswer)) {
5426
- if (activeSelectedId === correctChoiceId) {
5427
- map.set(correctChoiceId, "correct");
5428
- return map;
5429
- }
5430
- if (activeSelectedId) map.set(activeSelectedId, "incorrect");
5431
- map.set(correctChoiceId, "incorrect");
5426
+ map.set(correctChoiceId, "correct");
5432
5427
  return map;
5433
5428
  }
5434
5429
  if (!activeSelectedId) {