@patch-adams/core 1.5.4 → 1.5.5

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/dist/index.cjs CHANGED
@@ -3996,135 +3996,212 @@ function generateLrsBridgeCode(options) {
3996
3996
  // Intercepts cmi.interactions.N.* SetValue calls to capture quiz answers
3997
3997
  // directly from the SCORM data model \u2014 works regardless of Rise UI format
3998
3998
  // (Knowledge Check blocks, quiz lessons, etc.)
3999
+ //
4000
+ // IMPORTANT: This finds and wraps the ACTUAL SCORM API that Rise uses
4001
+ // (window.API, parent.API, or global functions), NOT the bridge's copy.
4002
+ // It runs unconditionally at init, not gated by actor resolution.
3999
4003
  // ========================================================================
4000
4004
  var scormInteractions = {}; // Pending interactions keyed by index N
4001
4005
  var scormInteractionsSent = {}; // Track which interactions were already sent
4002
4006
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4003
4007
 
4004
- function setupScormInteractionTracker() {
4005
- if (!TRACK_QUIZZES) return;
4006
- if (!LRS.scormApi) {
4007
- log('SCORM Interaction Tracker: No SCORM API found, skipping');
4008
- return;
4009
- }
4010
-
4011
- var api = LRS.scormApi;
4012
- var apiType = LRS.scormApiType;
4008
+ function interceptScormSetValue(key, value) {
4009
+ if (typeof key !== 'string') return;
4010
+ var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4011
+ var match = key.match(interactionPattern);
4012
+ if (!match) return;
4013
4013
 
4014
- // Determine which SetValue function to wrap
4015
- var setValueFn = apiType === '2004' ? 'SetValue' : 'LMSSetValue';
4016
- var originalSetValue = api[setValueFn];
4014
+ var idx = match[1];
4015
+ var field = match[2];
4017
4016
 
4018
- if (typeof originalSetValue !== 'function') {
4019
- log('SCORM Interaction Tracker: No ' + setValueFn + ' function found');
4020
- return;
4017
+ // Initialize interaction tracking for this index
4018
+ if (!scormInteractions[idx]) {
4019
+ scormInteractions[idx] = {};
4021
4020
  }
4022
4021
 
4023
- // SCORM 1.2 uses cmi.interactions.N.*, SCORM 2004 uses cmi.interactions.N.*
4024
- var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4022
+ // Store the field value
4023
+ scormInteractions[idx][field] = String(value);
4025
4024
 
4026
- api[setValueFn] = function(key, value) {
4027
- // Always call the original first
4028
- var result = originalSetValue.apply(api, arguments);
4025
+ log('SCORM Interaction [' + idx + '].' + field + ' = ' + String(value).substring(0, 100));
4029
4026
 
4030
- // Check if this is an interaction data element
4031
- if (typeof key === 'string') {
4032
- var match = key.match(interactionPattern);
4033
- if (match) {
4034
- var idx = match[1];
4035
- var field = match[2];
4027
+ // When 'result' is set, the interaction is complete \u2014 fire xAPI statement
4028
+ if (field === 'result' && !scormInteractionsSent[idx]) {
4029
+ scormInteractionsSent[idx] = true;
4030
+ var interaction = scormInteractions[idx];
4036
4031
 
4037
- // Initialize interaction tracking for this index
4038
- if (!scormInteractions[idx]) {
4039
- scormInteractions[idx] = {};
4040
- }
4032
+ // Map SCORM interaction type to readable type
4033
+ var typeMap = {
4034
+ 'choice': 'multiple-choice',
4035
+ 'true-false': 'true-false',
4036
+ 'fill-in': 'fill-in-blank',
4037
+ 'matching': 'matching',
4038
+ 'performance': 'performance',
4039
+ 'sequencing': 'sequencing',
4040
+ 'likert': 'likert',
4041
+ 'numeric': 'numeric'
4042
+ };
4041
4043
 
4042
- // Store the field value
4043
- scormInteractions[idx][field] = value;
4044
-
4045
- log('SCORM Interaction [' + idx + '].' + field + ' = ' + (value ? value.substring(0, 80) : value));
4046
-
4047
- // When 'result' is set, the interaction is complete \u2014 fire xAPI statement
4048
- if (field === 'result' && !scormInteractionsSent[idx]) {
4049
- scormInteractionsSent[idx] = true;
4050
- var interaction = scormInteractions[idx];
4051
-
4052
- // Map SCORM interaction type to readable type
4053
- var typeMap = {
4054
- 'choice': 'multiple-choice',
4055
- 'true-false': 'true-false',
4056
- 'fill-in': 'fill-in-blank',
4057
- 'matching': 'matching',
4058
- 'performance': 'performance',
4059
- 'sequencing': 'sequencing',
4060
- 'likert': 'likert',
4061
- 'numeric': 'numeric'
4062
- };
4044
+ var scormType = interaction.type || 'unknown';
4045
+ var questionType = typeMap[scormType] || scormType;
4063
4046
 
4064
- var scormType = interaction.type || 'unknown';
4065
- var questionType = typeMap[scormType] || scormType;
4047
+ // Determine correctness from SCORM result value
4048
+ // SCORM 1.2: 'correct', 'wrong', 'unanticipated', 'neutral'
4049
+ // SCORM 2004: 'correct', 'incorrect', 'unanticipated', 'neutral'
4050
+ var isCorrect = String(value) === 'correct';
4066
4051
 
4067
- // Determine correctness from SCORM result value
4068
- // SCORM 1.2: 'correct', 'wrong', 'unanticipated', 'neutral'
4069
- // SCORM 2004: 'correct', 'incorrect', 'unanticipated', 'neutral'
4070
- var isCorrect = value === 'correct';
4052
+ // Get student response
4053
+ var studentResponse = interaction.student_response || '';
4071
4054
 
4072
- // Get student response \u2014 for 'choice' type, this might be indices like '1,3'
4073
- var studentResponse = interaction.student_response || interaction['student_response'] || '';
4055
+ // Get correct response pattern
4056
+ var correctResponse = '';
4057
+ Object.keys(interaction).forEach(function(k) {
4058
+ if (k.indexOf('correct_responses') > -1 && k.indexOf('pattern') > -1) {
4059
+ correctResponse = interaction[k];
4060
+ }
4061
+ });
4074
4062
 
4075
- // Get correct response pattern
4076
- var correctResponse = '';
4077
- // SCORM 1.2: correct_responses.0.pattern
4078
- // Check stored fields for correct_responses
4079
- Object.keys(interaction).forEach(function(k) {
4080
- if (k.indexOf('correct_responses') > -1 && k.indexOf('pattern') > -1) {
4081
- correctResponse = interaction[k];
4082
- }
4083
- });
4063
+ // Rise interaction IDs encode the question text (underscored)
4064
+ // e.g. "Development_Week_Qui_I_want_a_clear_picture_of_..."
4065
+ var interactionId = interaction.id || ('interaction-' + idx);
4066
+
4067
+ // Try to make a readable question text from the interaction ID
4068
+ var questionText = interactionId
4069
+ .replace(/_\\d+$/, '') // remove trailing _0
4070
+ .replace(/_/g, ' ') // underscores to spaces
4071
+ .substring(0, 200);
4072
+
4073
+ // Get lesson context
4074
+ var lessonInfo = getCachedLessonInfo();
4075
+
4076
+ // Increment question counter
4077
+ kcQuestionCounter++;
4078
+
4079
+ log('SCORM Interaction complete [' + idx + ']:', {
4080
+ questionNumber: kcQuestionCounter,
4081
+ questionText: questionText.substring(0, 60) + '...',
4082
+ type: questionType,
4083
+ studentResponse: studentResponse,
4084
+ correctResponse: correctResponse,
4085
+ result: String(value),
4086
+ correct: isCorrect
4087
+ });
4084
4088
 
4085
- // Get interaction ID (Rise sets this to a unique question identifier)
4086
- var interactionId = interaction.id || ('interaction-' + idx);
4089
+ // Send xAPI answered statement
4090
+ LRS.questionAnswered({
4091
+ questionId: interactionId,
4092
+ questionGuid: interactionId,
4093
+ questionNumber: kcQuestionCounter,
4094
+ questionText: questionText,
4095
+ questionType: questionType,
4096
+ answer: studentResponse,
4097
+ correctAnswer: correctResponse,
4098
+ correct: isCorrect,
4099
+ result: isCorrect ? 'correct' : 'incorrect',
4100
+ assessmentName: lessonInfo.name || 'Quiz',
4101
+ lessonName: lessonInfo.name,
4102
+ sectionName: lessonInfo.sectionName
4103
+ });
4104
+ }
4105
+ }
4087
4106
 
4088
- // Get lesson context
4089
- var lessonInfo = getCachedLessonInfo();
4107
+ function setupScormInteractionTracker() {
4108
+ if (!TRACK_QUIZZES) return;
4090
4109
 
4091
- // Increment question counter
4092
- kcQuestionCounter++;
4110
+ // Find the ACTUAL SCORM API that Rise uses \u2014 NOT our bridge's copy.
4111
+ // Rise discovers the API via standard SCORM lookup (window.API, parent chain).
4112
+ // The Bravais CDS player's ProxyApi.injectLmsApi() sets this up.
4113
+ var wrapped = false;
4093
4114
 
4094
- log('SCORM Interaction complete [' + idx + ']:', {
4095
- id: interactionId,
4096
- type: questionType,
4097
- response: studentResponse,
4098
- correctResponse: correctResponse,
4099
- result: value,
4100
- correct: isCorrect,
4101
- questionNumber: kcQuestionCounter
4102
- });
4115
+ // Try 1: window.API (SCORM 1.2) or window.API_1484_11 (SCORM 2004)
4116
+ try {
4117
+ if (window.API && typeof window.API.LMSSetValue === 'function') {
4118
+ var origSetValue = window.API.LMSSetValue;
4119
+ window.API.LMSSetValue = function(key, value) {
4120
+ var result = origSetValue.apply(window.API, arguments);
4121
+ interceptScormSetValue(key, value);
4122
+ return result;
4123
+ };
4124
+ wrapped = true;
4125
+ log('SCORM Interaction Tracker: Wrapped window.API.LMSSetValue');
4126
+ }
4127
+ } catch (e) { log('SCORM Tracker: Cannot access window.API:', e.message); }
4103
4128
 
4104
- // Send xAPI answered statement
4105
- LRS.questionAnswered({
4106
- questionId: interactionId,
4107
- questionGuid: interactionId,
4108
- questionNumber: kcQuestionCounter,
4109
- questionText: 'Question ' + kcQuestionCounter + ' (' + interactionId + ')',
4110
- questionType: questionType,
4111
- answer: studentResponse,
4112
- correctAnswer: correctResponse,
4113
- correct: isCorrect,
4114
- result: isCorrect ? 'correct' : 'incorrect',
4115
- assessmentName: lessonInfo.name || 'Quiz',
4116
- lessonName: lessonInfo.name,
4117
- sectionName: lessonInfo.sectionName
4118
- });
4129
+ if (!wrapped) {
4130
+ try {
4131
+ if (window.API_1484_11 && typeof window.API_1484_11.SetValue === 'function') {
4132
+ var origSetValue2004 = window.API_1484_11.SetValue;
4133
+ window.API_1484_11.SetValue = function(key, value) {
4134
+ var result = origSetValue2004.apply(window.API_1484_11, arguments);
4135
+ interceptScormSetValue(key, value);
4136
+ return result;
4137
+ };
4138
+ wrapped = true;
4139
+ log('SCORM Interaction Tracker: Wrapped window.API_1484_11.SetValue');
4140
+ }
4141
+ } catch (e) { log('SCORM Tracker: Cannot access window.API_1484_11:', e.message); }
4142
+ }
4143
+
4144
+ // Try 2: Parent frame API
4145
+ if (!wrapped) {
4146
+ try {
4147
+ if (window.parent && window.parent !== window) {
4148
+ if (window.parent.API && typeof window.parent.API.LMSSetValue === 'function') {
4149
+ var origParentSetValue = window.parent.API.LMSSetValue;
4150
+ window.parent.API.LMSSetValue = function(key, value) {
4151
+ var result = origParentSetValue.apply(window.parent.API, arguments);
4152
+ interceptScormSetValue(key, value);
4153
+ return result;
4154
+ };
4155
+ wrapped = true;
4156
+ log('SCORM Interaction Tracker: Wrapped window.parent.API.LMSSetValue');
4119
4157
  }
4120
4158
  }
4121
- }
4159
+ } catch (e) { log('SCORM Tracker: Cannot access parent API (cross-origin)'); }
4160
+ }
4122
4161
 
4123
- return result;
4124
- };
4162
+ // Try 3: Global LMS functions (Bravais/Xyleme mock API)
4163
+ if (!wrapped) {
4164
+ try {
4165
+ if (typeof window.LMSSetValue === 'function') {
4166
+ var origGlobalSetValue = window.LMSSetValue;
4167
+ window.LMSSetValue = function(key, value) {
4168
+ var result = origGlobalSetValue.apply(window, arguments);
4169
+ interceptScormSetValue(key, value);
4170
+ return result;
4171
+ };
4172
+ wrapped = true;
4173
+ log('SCORM Interaction Tracker: Wrapped window.LMSSetValue (global)');
4174
+ }
4175
+ } catch (e) { log('SCORM Tracker: Cannot wrap global LMSSetValue:', e.message); }
4176
+ }
4177
+
4178
+ // Try 4: Fall back to bridge's copy (least likely to work but worth trying)
4179
+ if (!wrapped && LRS.scormApi) {
4180
+ var setValueFn = LRS.scormApiType === '2004' ? 'SetValue' : 'LMSSetValue';
4181
+ if (typeof LRS.scormApi[setValueFn] === 'function') {
4182
+ var origBridgeSetValue = LRS.scormApi[setValueFn];
4183
+ LRS.scormApi[setValueFn] = function(key, value) {
4184
+ var result = origBridgeSetValue.apply(LRS.scormApi, arguments);
4185
+ interceptScormSetValue(key, value);
4186
+ return result;
4187
+ };
4188
+ wrapped = true;
4189
+ log('SCORM Interaction Tracker: Wrapped LRS.scormApi.' + setValueFn + ' (bridge copy)');
4190
+ }
4191
+ }
4125
4192
 
4126
- scormTrackerActive = true;
4127
- log('SCORM Interaction Tracker: Wrapped ' + setValueFn + ' (apiType=' + apiType + ') \u2014 KC DOM handler will defer to SCORM tracker');
4193
+ if (wrapped) {
4194
+ scormTrackerActive = true;
4195
+ log('SCORM Interaction Tracker active \u2014 KC DOM handler will defer');
4196
+ } else {
4197
+ // API not available yet \u2014 retry in 2 seconds (Bravais proxy may not be ready)
4198
+ log('SCORM Interaction Tracker: No SCORM API found yet, retrying in 2s...');
4199
+ setTimeout(function() {
4200
+ if (!scormTrackerActive) {
4201
+ setupScormInteractionTracker();
4202
+ }
4203
+ }, 2000);
4204
+ }
4128
4205
  }
4129
4206
 
4130
4207
  function setupInteractionInterceptors() {
@@ -4345,9 +4422,6 @@ function generateLrsBridgeCode(options) {
4345
4422
  LRS.scormApiFound = true;
4346
4423
  LRS.scormApiType = result.type;
4347
4424
 
4348
- // Wrap SCORM SetValue to intercept cmi.interactions for quiz tracking
4349
- setupScormInteractionTracker();
4350
-
4351
4425
  // Read learner_id for diagnostics
4352
4426
  var scormLearnerId = 'n/a', scormLearnerName = 'n/a';
4353
4427
  try {
@@ -4430,6 +4504,10 @@ function generateLrsBridgeCode(options) {
4430
4504
  setupQuizInterceptors();
4431
4505
  setupInteractionInterceptors();
4432
4506
 
4507
+ // Intercept SCORM cmi.interactions to capture quiz answers as xAPI statements
4508
+ // This wraps the ACTUAL SCORM API (window.API etc.) that Rise calls
4509
+ setupScormInteractionTracker();
4510
+
4433
4511
  // Fetch document metadata from API to get GUIDs (async)
4434
4512
  // Then send course launched event
4435
4513
  var sharedLinkToken = LRS.courseInfo ? LRS.courseInfo.sharedLinkToken : null;