@pie-element/image-cloze-association 9.1.2-next.4 → 9.2.0-next.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/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [9.2.0-next.0](https://github.com/pie-framework/pie-elements/compare/@pie-element/image-cloze-association@9.1.2-next.5...@pie-element/image-cloze-association@9.2.0-next.0) (2026-03-18)
7
+
8
+ ### Bug Fixes
9
+
10
+ - test - include trace log in score response for empty placements ([4a46588](https://github.com/pie-framework/pie-elements/commit/4a46588819250b6efb11b458ed415c6346e70c94))
11
+
12
+ ### Features
13
+
14
+ - **image-cloze-association:** add trace log for scoring in outcome function PD-5437 ([c9e8e27](https://github.com/pie-framework/pie-elements/commit/c9e8e279113f144b9f316b2de833dbf9264884fd))
15
+
16
+ ## [9.1.2-next.5](https://github.com/pie-framework/pie-elements/compare/@pie-element/image-cloze-association@9.1.2-next.4...@pie-element/image-cloze-association@9.1.2-next.5) (2026-03-12)
17
+
18
+ ### Bug Fixes
19
+
20
+ - bump libs, update mathquill, switch interface PD-5791 ([686a7c0](https://github.com/pie-framework/pie-elements/commit/686a7c0d41ff82f5ddad7cecd93cc0c18324a81b))
21
+
6
22
  ## [9.1.2-next.4](https://github.com/pie-framework/pie-elements/compare/@pie-element/image-cloze-association@9.1.2-next.3...@pie-element/image-cloze-association@9.1.2-next.4) (2026-03-09)
7
23
 
8
24
  ### Bug Fixes
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [8.1.1-next.4](https://github.com/pie-framework/pie-elements/compare/@pie-element/image-cloze-association-configure@8.1.1-next.3...@pie-element/image-cloze-association-configure@8.1.1-next.4) (2026-03-12)
7
+
8
+ ### Bug Fixes
9
+
10
+ - bump libs, update mathquill, switch interface PD-5791 ([686a7c0](https://github.com/pie-framework/pie-elements/commit/686a7c0d41ff82f5ddad7cecd93cc0c18324a81b))
11
+
6
12
  ## [8.1.1-next.3](https://github.com/pie-framework/pie-elements/compare/@pie-element/image-cloze-association-configure@8.1.1-next.2...@pie-element/image-cloze-association-configure@8.1.1-next.3) (2026-03-06)
7
13
 
8
14
  **Note:** Version bump only for package @pie-element/image-cloze-association-configure
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pie-element/image-cloze-association-configure",
3
3
  "private": true,
4
- "version": "8.1.1-next.3",
4
+ "version": "8.1.1-next.4",
5
5
  "description": "",
6
6
  "main": "lib/index.js",
7
7
  "module": "src/index.js",
@@ -12,8 +12,8 @@
12
12
  "@mui/icons-material": "^7.3.4",
13
13
  "@mui/material": "^7.3.4",
14
14
  "@pie-framework/pie-configure-events": "^1.3.0",
15
- "@pie-lib/config-ui": "12.2.0-next.13",
16
- "@pie-lib/editable-html-tip-tap": "1.2.0-next.13",
15
+ "@pie-lib/config-ui": "12.2.0-next.17",
16
+ "@pie-lib/editable-html-tip-tap": "1.2.0-next.16",
17
17
  "debug": "^4.1.1",
18
18
  "prop-types": "^15.8.1",
19
19
  "react": "18.3.1",
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [7.2.0-next.0](https://github.com/pie-framework/pie-elements/compare/@pie-element/image-cloze-association-controller@7.1.1-next.1...@pie-element/image-cloze-association-controller@7.2.0-next.0) (2026-03-18)
7
+
8
+ ### Bug Fixes
9
+
10
+ - test - include trace log in score response for empty placements ([4a46588](https://github.com/pie-framework/pie-elements/commit/4a46588819250b6efb11b458ed415c6346e70c94))
11
+
12
+ ### Features
13
+
14
+ - **image-cloze-association:** add trace log for scoring in outcome function PD-5437 ([c9e8e27](https://github.com/pie-framework/pie-elements/commit/c9e8e279113f144b9f316b2de833dbf9264884fd))
15
+
6
16
  ## [7.1.1-next.1](https://github.com/pie-framework/pie-elements/compare/@pie-element/image-cloze-association-controller@7.1.0-next.1...@pie-element/image-cloze-association-controller@7.1.1-next.1) (2026-02-26)
7
17
 
8
18
  **Note:** Version bump only for package @pie-element/image-cloze-association-controller
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.validate = exports.outcome = exports.normalize = exports.model = exports.isResponseCorrect = exports.getPartialScore = exports.createCorrectResponseSession = void 0;
7
+ exports.validate = exports.outcome = exports.normalize = exports.model = exports.isResponseCorrect = exports.getPartialScore = exports.getLogTrace = exports.createCorrectResponseSession = void 0;
8
8
  var _debug = _interopRequireDefault(require("debug"));
9
9
  var _humps = require("humps");
10
10
  var _controllerUtils = require("@pie-lib/controller-utils");
@@ -173,20 +173,82 @@ const getScore = (config, session, env = {}) => {
173
173
  const correct = isDefaultOrAltResponseCorrect(config, session);
174
174
  return isPartialScoring ? getPartialScore(config, session) : correct ? 1 : 0;
175
175
  };
176
+
177
+ /**
178
+ * Generates detailed trace log for scoring evaluation
179
+ * @param {Object} model - the question model
180
+ * @param {Object} session - the student session
181
+ * @param {Object} env - the environment
182
+ * @returns {Array} traceLog - array of trace messages
183
+ */
184
+ const getLogTrace = (model, session, env) => {
185
+ const traceLog = [];
186
+ const {
187
+ answers
188
+ } = session || {};
189
+ const validResponse = model.validation?.validResponse?.value || [];
190
+ const totalContainers = validResponse.length;
191
+ traceLog.push(`${totalContainers} placement container(s) defined in this question.`);
192
+ if (answers && answers.length > 0) {
193
+ traceLog.push(`Student placed ${answers.length} image(s) into placement containers.`);
194
+ const answersByContainer = {};
195
+ answers.forEach(answer => {
196
+ if (!answersByContainer[answer.containerIndex]) {
197
+ answersByContainer[answer.containerIndex] = [];
198
+ }
199
+ answersByContainer[answer.containerIndex].push(answer.value);
200
+ });
201
+ validResponse.forEach((container, containerIndex) => {
202
+ const correctImages = container.images || [];
203
+ const studentImages = answersByContainer[containerIndex] || [];
204
+ if (correctImages.length > 0) {
205
+ if (studentImages.length === 0) {
206
+ traceLog.push(`Container ${containerIndex + 1}: student left empty (should contain ${correctImages.length} image(s)).`);
207
+ } else {
208
+ const correctCount = studentImages.filter(img => correctImages.includes(img)).length;
209
+ const incorrectCount = studentImages.length - correctCount;
210
+ if (correctCount > 0 && incorrectCount === 0) {
211
+ traceLog.push(`Container ${containerIndex + 1}: student placed ${correctCount} correct image(s).`);
212
+ } else if (correctCount === 0 && incorrectCount > 0) {
213
+ traceLog.push(`Container ${containerIndex + 1}: student placed ${incorrectCount} incorrect image(s).`);
214
+ } else {
215
+ traceLog.push(`Container ${containerIndex + 1}: student placed ${correctCount} correct and ${incorrectCount} incorrect image(s).`);
216
+ }
217
+ }
218
+ }
219
+ });
220
+ } else {
221
+ traceLog.push('Student did not place any images into placement containers.');
222
+ }
223
+ const altResponses = model.validation?.altResponses || [];
224
+ if (altResponses.length > 0) {
225
+ traceLog.push(`${altResponses.length} alternate response combination(s) are accepted for this question.`);
226
+ }
227
+ const partialScoringEnabled = _controllerUtils.partialScoring.enabled(model, env);
228
+ const scoringMethod = partialScoringEnabled ? 'partial scoring' : 'all-or-nothing scoring';
229
+ traceLog.push(`Score calculated using ${scoringMethod}.`);
230
+ const score = getScore(model, session, env);
231
+ traceLog.push(`Final score: ${score}.`);
232
+ return traceLog;
233
+ };
234
+ exports.getLogTrace = getLogTrace;
176
235
  const outcome = (config, session, env = {}) => {
177
236
  return new Promise(resolve => {
178
237
  log('outcome...');
179
238
  if (!session || (0, _lodashEs.isEmpty)(session)) {
180
239
  resolve({
181
240
  score: 0,
182
- empty: true
241
+ empty: true,
242
+ traceLog: ['Student did not place any images into placement containers. Score is 0.']
183
243
  });
184
- }
185
- const configCamelized = (0, _humps.camelizeKeys)(config);
186
- if (session.answers || []) {
244
+ } else {
245
+ const configCamelized = (0, _humps.camelizeKeys)(config);
246
+ const traceLog = getLogTrace(configCamelized, session, env);
187
247
  const score = getScore(configCamelized, session, env);
188
248
  resolve({
189
- score
249
+ score,
250
+ empty: false,
251
+ traceLog
190
252
  });
191
253
  }
192
254
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["_debug","_interopRequireDefault","require","_humps","_controllerUtils","_lodashEs","_defaults","_utils","log","debug","normalize","question","defaults","exports","model","session","env","questionNormalized","questionCamelized","camelizeKeys","Promise","resolve","shouldIncludeCorrectResponse","mode","responseAreasToBeFilled","possibleResponses","completeResponses","hasUnplacedChoices","getCompleteResponseDetails","validation","out","disabled","responseCorrect","getScore","undefined","shuffle","possible_responses","role","teacherInstructions","teacherInstructionsEnabled","rationale","isResponseCorrect","correctResponses","responses","cloneDeep","isCorrect","totalValidResponses","isEmpty","forEach","value","images","length","answers","answer","index","containerIndex","indexOf","splice","keepNonEmptyResponses","filtered","filter","response","isDefaultOrAltResponseCorrect","altResponses","validResponse","altResponse","getDeductionPerContainer","valid","totalStack","item","incorrectStack","maxValid","ignored","slice","getPartialScore","maxResponsePerZone","correctAnswers","incorrectAnswers","all","getAllUniqueCorrectness","deductionList","id","nonEmptyResponses","denominator","str","toFixed","parseFloat","config","isPartialScoring","partialScoring","enabled","correct","outcome","score","empty","configCamelized","createCorrectResponseSession","valid_response","container","i","v","push","getInnerText","html","replaceAll","getContent","replace","validate","errors","field","required"],"sources":["../src/index.js"],"sourcesContent":["import debug from 'debug';\nimport { camelizeKeys } from 'humps';\nimport { partialScoring } from '@pie-lib/controller-utils';\nimport { cloneDeep, isEmpty, shuffle } from 'lodash-es';\n\nimport defaults from './defaults';\nimport { getAllUniqueCorrectness, getCompleteResponseDetails } from './utils';\n\nconst log = debug('pie-elements:image-cloze-association:controller');\n\nexport const normalize = (question) => ({ ...defaults, ...question });\n\nexport const model = (question, session, env) => {\n const questionNormalized = normalize(question);\n const questionCamelized = camelizeKeys(questionNormalized);\n\n return new Promise((resolve) => {\n const shouldIncludeCorrectResponse = env.mode === 'evaluate';\n\n const {\n responseAreasToBeFilled,\n possibleResponses: completeResponses,\n hasUnplacedChoices,\n } = getCompleteResponseDetails(questionCamelized.validation, questionCamelized.possibleResponses);\n\n const out = {\n disabled: env.mode !== 'gather',\n mode: env.mode,\n ...questionCamelized,\n responseCorrect: shouldIncludeCorrectResponse ? getScore(questionCamelized, session) === 1 : undefined,\n validation: shouldIncludeCorrectResponse ? questionCamelized.validation : undefined,\n responseAreasToBeFilled,\n completeResponses,\n hasUnplacedChoices,\n };\n\n if (questionNormalized.shuffle) {\n out.possibleResponses = shuffle(questionNormalized.possible_responses);\n }\n\n if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {\n out.teacherInstructions = questionCamelized.teacherInstructionsEnabled\n ? questionCamelized.teacherInstructions\n : null;\n out.rationale = questionCamelized.rationale ? questionCamelized.rationale : null;\n } else {\n out.teacherInstructions = null;\n out.rationale = null;\n }\n\n resolve(out);\n });\n};\n\nexport const isResponseCorrect = (correctResponses, session) => {\n const responses = cloneDeep(correctResponses);\n let isCorrect = true;\n let totalValidResponses = 0;\n\n if (!session || isEmpty(session)) {\n return false;\n }\n\n responses.forEach((value) => (totalValidResponses += (value.images || []).length));\n\n if (session.answers && totalValidResponses === session.answers.length) {\n session.answers.forEach((answer) => {\n const index = (responses[answer.containerIndex]?.images || []).indexOf(answer.value);\n\n if (index >= 0) {\n // remove response from correct responses array to ensure that duplicates are evaluated correctly\n responses[answer.containerIndex].images.splice(index, 1);\n } else {\n isCorrect = false;\n }\n });\n } else {\n isCorrect = false;\n }\n\n return isCorrect;\n};\n\n// This applies for correct responses that have empty values\nconst keepNonEmptyResponses = (responses) => {\n const filtered = responses.filter((response) => response.images && response.images.length);\n return cloneDeep(filtered);\n};\n\n// This applies for items that don't support partial scoring.\nconst isDefaultOrAltResponseCorrect = (question, session) => {\n const {\n validation: { altResponses },\n } = question;\n let {\n validation: {\n validResponse: { value },\n },\n } = question;\n\n let isCorrect = isResponseCorrect(value, session);\n\n // Look for correct answers in alternate responses.\n if (!isCorrect && altResponses && altResponses.length) {\n altResponses.forEach((altResponse) => {\n if (isResponseCorrect(altResponse.value, session)) {\n isCorrect = true;\n }\n });\n }\n return isCorrect;\n};\n\n// Deduct only the items that exceeded the maximum valid response per container.\nconst getDeductionPerContainer = (containerIndex, answers, valid) => {\n const totalStack = answers.filter((item) => item.containerIndex === containerIndex);\n const incorrectStack = totalStack.filter((item) => !item.isCorrect);\n const maxValid = (valid.value[containerIndex].images || []).length;\n\n if (totalStack.length > maxValid) {\n const ignored = totalStack.length - maxValid;\n return incorrectStack.slice(-ignored);\n }\n return [];\n};\n\nexport const getPartialScore = (question, session) => {\n const {\n validation: { validResponse },\n maxResponsePerZone,\n } = question;\n let correctAnswers = 0;\n let incorrectAnswers = 0;\n let possibleResponses = 0;\n\n if (!session || isEmpty(session)) {\n return 0;\n }\n\n validResponse.value.forEach((value) => (possibleResponses += (value.images || []).length));\n\n if (session.answers && session.answers.length) {\n const all = getAllUniqueCorrectness(session.answers, validResponse.value);\n correctAnswers = all.filter((item) => item.isCorrect).length;\n incorrectAnswers = all.filter((item) => !item.isCorrect).length;\n\n // deduction rules: https://docs.google.com/document/d/1Oprm8Qs5fg_Dwoj2pNpsfu4D63QgCZgvcqTgeaVel7I/edit\n session.answers.forEach((answer) => {\n if (maxResponsePerZone > 1) {\n const deductionList = getDeductionPerContainer(answer.containerIndex, all, validResponse);\n\n if (deductionList.length) {\n deductionList.forEach((item) => {\n if (item.id === answer.id) {\n correctAnswers -= 1;\n }\n });\n }\n }\n });\n\n if (!maxResponsePerZone || maxResponsePerZone <= 1) {\n correctAnswers -= incorrectAnswers;\n }\n } else {\n correctAnswers = 0;\n }\n // negative values will implicitly make the score equal to zero\n correctAnswers = correctAnswers < 0 ? 0 : correctAnswers;\n\n // use length of validResponse since some containers can be left empty\n const nonEmptyResponses = keepNonEmptyResponses(validResponse.value);\n const denominator = maxResponsePerZone > 1 ? possibleResponses : (nonEmptyResponses || []).length;\n const str = (correctAnswers / denominator).toFixed(2);\n\n return parseFloat(str);\n};\n\nconst getScore = (config, session, env = {}) => {\n const isPartialScoring = partialScoring.enabled(config, env);\n const correct = isDefaultOrAltResponseCorrect(config, session);\n\n return isPartialScoring ? getPartialScore(config, session) : correct ? 1 : 0;\n};\n\nexport const outcome = (config, session, env = {}) => {\n return new Promise((resolve) => {\n log('outcome...');\n if (!session || isEmpty(session)) {\n resolve({ score: 0, empty: true });\n }\n\n const configCamelized = camelizeKeys(config);\n\n if (session.answers || []) {\n const score = getScore(configCamelized, session, env);\n resolve({ score });\n }\n });\n};\n\nexport const createCorrectResponseSession = (question, env) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n const {\n validation: {\n valid_response: { value },\n },\n } = question;\n const answers = [];\n\n if (value) {\n value.forEach((container, i) => {\n (container.images || []).forEach((v) => {\n answers.push({\n value: v,\n containerIndex: i,\n });\n });\n });\n }\n\n resolve({\n answers,\n id: '1',\n });\n } else {\n resolve(null);\n }\n });\n};\n\n// remove all html tags\nconst getInnerText = (html) => (html || '').replaceAll(/<[^>]*>/g, '');\n\n// remove all html tags except img, iframe and source tag for audio\nconst getContent = (html) => (html || '').replace(/(<(?!img|iframe|source)([^>]+)>)/gi, '');\n\nexport const validate = (model = {}, config = {}) => {\n const errors = {};\n\n ['teacherInstructions'].forEach((field) => {\n if (config[field]?.required && !getContent(model[field])) {\n errors[field] = 'This field is required.';\n }\n });\n\n return errors;\n};\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["_debug","_interopRequireDefault","require","_humps","_controllerUtils","_lodashEs","_defaults","_utils","log","debug","normalize","question","defaults","exports","model","session","env","questionNormalized","questionCamelized","camelizeKeys","Promise","resolve","shouldIncludeCorrectResponse","mode","responseAreasToBeFilled","possibleResponses","completeResponses","hasUnplacedChoices","getCompleteResponseDetails","validation","out","disabled","responseCorrect","getScore","undefined","shuffle","possible_responses","role","teacherInstructions","teacherInstructionsEnabled","rationale","isResponseCorrect","correctResponses","responses","cloneDeep","isCorrect","totalValidResponses","isEmpty","forEach","value","images","length","answers","answer","index","containerIndex","indexOf","splice","keepNonEmptyResponses","filtered","filter","response","isDefaultOrAltResponseCorrect","altResponses","validResponse","altResponse","getDeductionPerContainer","valid","totalStack","item","incorrectStack","maxValid","ignored","slice","getPartialScore","maxResponsePerZone","correctAnswers","incorrectAnswers","all","getAllUniqueCorrectness","deductionList","id","nonEmptyResponses","denominator","str","toFixed","parseFloat","config","isPartialScoring","partialScoring","enabled","correct","getLogTrace","traceLog","totalContainers","push","answersByContainer","container","correctImages","studentImages","correctCount","img","includes","incorrectCount","partialScoringEnabled","scoringMethod","score","outcome","empty","configCamelized","createCorrectResponseSession","valid_response","i","v","getInnerText","html","replaceAll","getContent","replace","validate","errors","field","required"],"sources":["../src/index.js"],"sourcesContent":["import debug from 'debug';\nimport { camelizeKeys } from 'humps';\nimport { partialScoring } from '@pie-lib/controller-utils';\nimport { cloneDeep, isEmpty, shuffle } from 'lodash-es';\n\nimport defaults from './defaults';\nimport { getAllUniqueCorrectness, getCompleteResponseDetails } from './utils';\n\nconst log = debug('pie-elements:image-cloze-association:controller');\n\nexport const normalize = (question) => ({ ...defaults, ...question });\n\nexport const model = (question, session, env) => {\n const questionNormalized = normalize(question);\n const questionCamelized = camelizeKeys(questionNormalized);\n\n return new Promise((resolve) => {\n const shouldIncludeCorrectResponse = env.mode === 'evaluate';\n\n const {\n responseAreasToBeFilled,\n possibleResponses: completeResponses,\n hasUnplacedChoices,\n } = getCompleteResponseDetails(questionCamelized.validation, questionCamelized.possibleResponses);\n\n const out = {\n disabled: env.mode !== 'gather',\n mode: env.mode,\n ...questionCamelized,\n responseCorrect: shouldIncludeCorrectResponse ? getScore(questionCamelized, session) === 1 : undefined,\n validation: shouldIncludeCorrectResponse ? questionCamelized.validation : undefined,\n responseAreasToBeFilled,\n completeResponses,\n hasUnplacedChoices,\n };\n\n if (questionNormalized.shuffle) {\n out.possibleResponses = shuffle(questionNormalized.possible_responses);\n }\n\n if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {\n out.teacherInstructions = questionCamelized.teacherInstructionsEnabled\n ? questionCamelized.teacherInstructions\n : null;\n out.rationale = questionCamelized.rationale ? questionCamelized.rationale : null;\n } else {\n out.teacherInstructions = null;\n out.rationale = null;\n }\n\n resolve(out);\n });\n};\n\nexport const isResponseCorrect = (correctResponses, session) => {\n const responses = cloneDeep(correctResponses);\n let isCorrect = true;\n let totalValidResponses = 0;\n\n if (!session || isEmpty(session)) {\n return false;\n }\n\n responses.forEach((value) => (totalValidResponses += (value.images || []).length));\n\n if (session.answers && totalValidResponses === session.answers.length) {\n session.answers.forEach((answer) => {\n const index = (responses[answer.containerIndex]?.images || []).indexOf(answer.value);\n\n if (index >= 0) {\n // remove response from correct responses array to ensure that duplicates are evaluated correctly\n responses[answer.containerIndex].images.splice(index, 1);\n } else {\n isCorrect = false;\n }\n });\n } else {\n isCorrect = false;\n }\n\n return isCorrect;\n};\n\n// This applies for correct responses that have empty values\nconst keepNonEmptyResponses = (responses) => {\n const filtered = responses.filter((response) => response.images && response.images.length);\n return cloneDeep(filtered);\n};\n\n// This applies for items that don't support partial scoring.\nconst isDefaultOrAltResponseCorrect = (question, session) => {\n const {\n validation: { altResponses },\n } = question;\n let {\n validation: {\n validResponse: { value },\n },\n } = question;\n\n let isCorrect = isResponseCorrect(value, session);\n\n // Look for correct answers in alternate responses.\n if (!isCorrect && altResponses && altResponses.length) {\n altResponses.forEach((altResponse) => {\n if (isResponseCorrect(altResponse.value, session)) {\n isCorrect = true;\n }\n });\n }\n return isCorrect;\n};\n\n// Deduct only the items that exceeded the maximum valid response per container.\nconst getDeductionPerContainer = (containerIndex, answers, valid) => {\n const totalStack = answers.filter((item) => item.containerIndex === containerIndex);\n const incorrectStack = totalStack.filter((item) => !item.isCorrect);\n const maxValid = (valid.value[containerIndex].images || []).length;\n\n if (totalStack.length > maxValid) {\n const ignored = totalStack.length - maxValid;\n return incorrectStack.slice(-ignored);\n }\n return [];\n};\n\nexport const getPartialScore = (question, session) => {\n const {\n validation: { validResponse },\n maxResponsePerZone,\n } = question;\n let correctAnswers = 0;\n let incorrectAnswers = 0;\n let possibleResponses = 0;\n\n if (!session || isEmpty(session)) {\n return 0;\n }\n\n validResponse.value.forEach((value) => (possibleResponses += (value.images || []).length));\n\n if (session.answers && session.answers.length) {\n const all = getAllUniqueCorrectness(session.answers, validResponse.value);\n correctAnswers = all.filter((item) => item.isCorrect).length;\n incorrectAnswers = all.filter((item) => !item.isCorrect).length;\n\n // deduction rules: https://docs.google.com/document/d/1Oprm8Qs5fg_Dwoj2pNpsfu4D63QgCZgvcqTgeaVel7I/edit\n session.answers.forEach((answer) => {\n if (maxResponsePerZone > 1) {\n const deductionList = getDeductionPerContainer(answer.containerIndex, all, validResponse);\n\n if (deductionList.length) {\n deductionList.forEach((item) => {\n if (item.id === answer.id) {\n correctAnswers -= 1;\n }\n });\n }\n }\n });\n\n if (!maxResponsePerZone || maxResponsePerZone <= 1) {\n correctAnswers -= incorrectAnswers;\n }\n } else {\n correctAnswers = 0;\n }\n // negative values will implicitly make the score equal to zero\n correctAnswers = correctAnswers < 0 ? 0 : correctAnswers;\n\n // use length of validResponse since some containers can be left empty\n const nonEmptyResponses = keepNonEmptyResponses(validResponse.value);\n const denominator = maxResponsePerZone > 1 ? possibleResponses : (nonEmptyResponses || []).length;\n const str = (correctAnswers / denominator).toFixed(2);\n\n return parseFloat(str);\n};\n\nconst getScore = (config, session, env = {}) => {\n const isPartialScoring = partialScoring.enabled(config, env);\n const correct = isDefaultOrAltResponseCorrect(config, session);\n\n return isPartialScoring ? getPartialScore(config, session) : correct ? 1 : 0;\n};\n\n/**\n * Generates detailed trace log for scoring evaluation\n * @param {Object} model - the question model\n * @param {Object} session - the student session\n * @param {Object} env - the environment\n * @returns {Array} traceLog - array of trace messages\n */\nexport const getLogTrace = (model, session, env) => {\n const traceLog = [];\n const { answers } = session || {};\n \n const validResponse = model.validation?.validResponse?.value || [];\n const totalContainers = validResponse.length;\n traceLog.push(`${totalContainers} placement container(s) defined in this question.`);\n\n if (answers && answers.length > 0) {\n traceLog.push(`Student placed ${answers.length} image(s) into placement containers.`);\n \n const answersByContainer = {};\n answers.forEach((answer) => {\n if (!answersByContainer[answer.containerIndex]) {\n answersByContainer[answer.containerIndex] = [];\n }\n answersByContainer[answer.containerIndex].push(answer.value);\n });\n \n validResponse.forEach((container, containerIndex) => {\n const correctImages = container.images || [];\n const studentImages = answersByContainer[containerIndex] || [];\n \n if (correctImages.length > 0) {\n if (studentImages.length === 0) {\n traceLog.push(`Container ${containerIndex + 1}: student left empty (should contain ${correctImages.length} image(s)).`);\n } else {\n const correctCount = studentImages.filter(img => correctImages.includes(img)).length;\n const incorrectCount = studentImages.length - correctCount;\n \n if (correctCount > 0 && incorrectCount === 0) {\n traceLog.push(`Container ${containerIndex + 1}: student placed ${correctCount} correct image(s).`);\n } else if (correctCount === 0 && incorrectCount > 0) {\n traceLog.push(`Container ${containerIndex + 1}: student placed ${incorrectCount} incorrect image(s).`);\n } else {\n traceLog.push(`Container ${containerIndex + 1}: student placed ${correctCount} correct and ${incorrectCount} incorrect image(s).`);\n }\n }\n }\n });\n } else {\n traceLog.push('Student did not place any images into placement containers.');\n }\n\n const altResponses = model.validation?.altResponses || [];\n if (altResponses.length > 0) {\n traceLog.push(`${altResponses.length} alternate response combination(s) are accepted for this question.`);\n }\n\n const partialScoringEnabled = partialScoring.enabled(model, env);\n const scoringMethod = partialScoringEnabled ? 'partial scoring' : 'all-or-nothing scoring';\n traceLog.push(`Score calculated using ${scoringMethod}.`);\n\n const score = getScore(model, session, env);\n traceLog.push(`Final score: ${score}.`);\n\n return traceLog;\n}\n\nexport const outcome = (config, session, env = {}) => {\n return new Promise((resolve) => {\n log('outcome...');\n if (!session || isEmpty(session)) {\n resolve({ \n score: 0, \n empty: true, \n traceLog: ['Student did not place any images into placement containers. Score is 0.'] \n });\n } else {\n const configCamelized = camelizeKeys(config);\n const traceLog = getLogTrace(configCamelized, session, env);\n const score = getScore(configCamelized, session, env);\n \n resolve({ \n score, \n empty: false,\n traceLog \n });\n }\n });\n};\n\nexport const createCorrectResponseSession = (question, env) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n const {\n validation: {\n valid_response: { value },\n },\n } = question;\n const answers = [];\n\n if (value) {\n value.forEach((container, i) => {\n (container.images || []).forEach((v) => {\n answers.push({\n value: v,\n containerIndex: i,\n });\n });\n });\n }\n\n resolve({\n answers,\n id: '1',\n });\n } else {\n resolve(null);\n }\n });\n};\n\n// remove all html tags\nconst getInnerText = (html) => (html || '').replaceAll(/<[^>]*>/g, '');\n\n// remove all html tags except img, iframe and source tag for audio\nconst getContent = (html) => (html || '').replace(/(<(?!img|iframe|source)([^>]+)>)/gi, '');\n\nexport const validate = (model = {}, config = {}) => {\n const errors = {};\n\n ['teacherInstructions'].forEach((field) => {\n if (config[field]?.required && !getContent(model[field])) {\n errors[field] = 'This field is required.';\n }\n });\n\n return errors;\n};\n"],"mappings":"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","ignoreList":[]}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pie-element/image-cloze-association-controller",
3
3
  "private": true,
4
- "version": "7.1.1-next.1",
4
+ "version": "7.2.0-next.0",
5
5
  "description": "",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -135,7 +135,7 @@ describe('controller', () => {
135
135
  const returnOutcome = (session) => {
136
136
  it(`returns score of 0 and empty: true if session is ${JSON.stringify(session)}`, async () => {
137
137
  const result = await outcome(question, session);
138
- expect(result).toEqual({ score: 0, empty: true });
138
+ expect(result).toEqual({ score: 0, empty: true, traceLog: ['Student did not place any images into placement containers. Score is 0.'] });
139
139
  });
140
140
  };
141
141
 
@@ -183,18 +183,91 @@ const getScore = (config, session, env = {}) => {
183
183
  return isPartialScoring ? getPartialScore(config, session) : correct ? 1 : 0;
184
184
  };
185
185
 
186
+ /**
187
+ * Generates detailed trace log for scoring evaluation
188
+ * @param {Object} model - the question model
189
+ * @param {Object} session - the student session
190
+ * @param {Object} env - the environment
191
+ * @returns {Array} traceLog - array of trace messages
192
+ */
193
+ export const getLogTrace = (model, session, env) => {
194
+ const traceLog = [];
195
+ const { answers } = session || {};
196
+
197
+ const validResponse = model.validation?.validResponse?.value || [];
198
+ const totalContainers = validResponse.length;
199
+ traceLog.push(`${totalContainers} placement container(s) defined in this question.`);
200
+
201
+ if (answers && answers.length > 0) {
202
+ traceLog.push(`Student placed ${answers.length} image(s) into placement containers.`);
203
+
204
+ const answersByContainer = {};
205
+ answers.forEach((answer) => {
206
+ if (!answersByContainer[answer.containerIndex]) {
207
+ answersByContainer[answer.containerIndex] = [];
208
+ }
209
+ answersByContainer[answer.containerIndex].push(answer.value);
210
+ });
211
+
212
+ validResponse.forEach((container, containerIndex) => {
213
+ const correctImages = container.images || [];
214
+ const studentImages = answersByContainer[containerIndex] || [];
215
+
216
+ if (correctImages.length > 0) {
217
+ if (studentImages.length === 0) {
218
+ traceLog.push(`Container ${containerIndex + 1}: student left empty (should contain ${correctImages.length} image(s)).`);
219
+ } else {
220
+ const correctCount = studentImages.filter(img => correctImages.includes(img)).length;
221
+ const incorrectCount = studentImages.length - correctCount;
222
+
223
+ if (correctCount > 0 && incorrectCount === 0) {
224
+ traceLog.push(`Container ${containerIndex + 1}: student placed ${correctCount} correct image(s).`);
225
+ } else if (correctCount === 0 && incorrectCount > 0) {
226
+ traceLog.push(`Container ${containerIndex + 1}: student placed ${incorrectCount} incorrect image(s).`);
227
+ } else {
228
+ traceLog.push(`Container ${containerIndex + 1}: student placed ${correctCount} correct and ${incorrectCount} incorrect image(s).`);
229
+ }
230
+ }
231
+ }
232
+ });
233
+ } else {
234
+ traceLog.push('Student did not place any images into placement containers.');
235
+ }
236
+
237
+ const altResponses = model.validation?.altResponses || [];
238
+ if (altResponses.length > 0) {
239
+ traceLog.push(`${altResponses.length} alternate response combination(s) are accepted for this question.`);
240
+ }
241
+
242
+ const partialScoringEnabled = partialScoring.enabled(model, env);
243
+ const scoringMethod = partialScoringEnabled ? 'partial scoring' : 'all-or-nothing scoring';
244
+ traceLog.push(`Score calculated using ${scoringMethod}.`);
245
+
246
+ const score = getScore(model, session, env);
247
+ traceLog.push(`Final score: ${score}.`);
248
+
249
+ return traceLog;
250
+ }
251
+
186
252
  export const outcome = (config, session, env = {}) => {
187
253
  return new Promise((resolve) => {
188
254
  log('outcome...');
189
255
  if (!session || isEmpty(session)) {
190
- resolve({ score: 0, empty: true });
191
- }
192
-
193
- const configCamelized = camelizeKeys(config);
194
-
195
- if (session.answers || []) {
256
+ resolve({
257
+ score: 0,
258
+ empty: true,
259
+ traceLog: ['Student did not place any images into placement containers. Score is 0.']
260
+ });
261
+ } else {
262
+ const configCamelized = camelizeKeys(config);
263
+ const traceLog = getLogTrace(configCamelized, session, env);
196
264
  const score = getScore(configCamelized, session, env);
197
- resolve({ score });
265
+
266
+ resolve({
267
+ score,
268
+ empty: false,
269
+ traceLog
270
+ });
198
271
  }
199
272
  });
200
273
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pie-element/image-cloze-association",
3
3
  "repository": "pie-framework/pie-elements",
4
- "version": "9.1.2-next.4",
4
+ "version": "9.2.0-next.0",
5
5
  "description": "",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "author": "pie framework developers",
28
28
  "license": "ISC",
29
- "gitHead": "a8b257ceb051bf973d80b1ae19138ae99117346b",
29
+ "gitHead": "c4198b2dfd0bfa1847c515a0c95c39147398395c",
30
30
  "scripts": {
31
31
  "postpublish": "../../scripts/postpublish"
32
32
  },