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