@patch-adams/core 1.5.2 → 1.5.4

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
@@ -3810,8 +3810,9 @@ function generateLrsBridgeCode(options) {
3810
3810
  });
3811
3811
  }
3812
3812
 
3813
- // Track submitted Knowledge Check blocks to avoid duplicates
3813
+ // Track submitted Knowledge Check questions to avoid duplicates
3814
3814
  var submittedKnowledgeChecks = {};
3815
+ var kcQuestionCounter = 0;
3815
3816
 
3816
3817
  /**
3817
3818
  * Set up interceptors specifically for Rise Knowledge Check blocks
@@ -3821,7 +3822,11 @@ function generateLrsBridgeCode(options) {
3821
3822
  if (!TRACK_QUIZZES) return;
3822
3823
 
3823
3824
  // Intercept submit button clicks on Knowledge Check blocks
3825
+ // NOTE: If the SCORM tracker is active, it handles quiz tracking via
3826
+ // cmi.interactions \u2014 skip DOM scraping to avoid duplicate statements
3824
3827
  document.addEventListener('click', function(e) {
3828
+ if (scormTrackerActive) return; // SCORM tracker handles this
3829
+
3825
3830
  var submitBtn = e.target.closest('.quiz-card__button');
3826
3831
  if (!submitBtn) return;
3827
3832
 
@@ -3843,16 +3848,26 @@ function generateLrsBridgeCode(options) {
3843
3848
  * Extract and send xAPI statement for a Knowledge Check submission
3844
3849
  */
3845
3850
  function extractKnowledgeCheckResult(kcBlock) {
3846
- // Get block ID for deduplication
3851
+ // Get block ID for context
3847
3852
  var blockContainer = kcBlock.closest('[data-block-id]');
3848
3853
  var blockId = blockContainer ? blockContainer.getAttribute('data-block-id') : null;
3849
3854
 
3855
+ // Get question text first \u2014 needed for per-question dedup key
3856
+ var questionText = '';
3857
+ var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
3858
+ if (questionTextEl) {
3859
+ questionText = questionTextEl.textContent.trim();
3860
+ }
3861
+
3850
3862
  // Get question ID from the title element
3851
3863
  var questionTitleEl = kcBlock.querySelector('.quiz-card__title');
3852
- var questionId = questionTitleEl ? questionTitleEl.id : (blockId ? 'q-' + blockId : 'q-' + generateUUID());
3864
+ var questionId = questionTitleEl ? questionTitleEl.id : null;
3865
+
3866
+ // Build a question-specific dedup key using question text hash
3867
+ // This ensures each question in a multi-question quiz block gets its own key
3868
+ var questionHash = questionText ? questionText.substring(0, 100) : (questionId || blockId || generateUUID());
3869
+ var submissionKey = 'kc-' + questionHash;
3853
3870
 
3854
- // Check if we already processed this submission (avoid duplicates)
3855
- var submissionKey = blockId || questionId;
3856
3871
  var feedbackLabel = kcBlock.querySelector('.quiz-card__feedback-label');
3857
3872
  if (!feedbackLabel) {
3858
3873
  log('Knowledge Check: No feedback visible yet');
@@ -3868,11 +3883,9 @@ function generateLrsBridgeCode(options) {
3868
3883
  }
3869
3884
  submittedKnowledgeChecks[submissionId] = true;
3870
3885
 
3871
- // Get question text
3872
- var questionText = '';
3873
- var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
3874
- if (questionTextEl) {
3875
- questionText = questionTextEl.textContent.trim();
3886
+ // Use question-specific ID for the statement (not the shared block ID)
3887
+ if (!questionId) {
3888
+ questionId = blockId ? 'q-' + blockId + '-' + questionHash.substring(0, 20) : 'q-' + generateUUID();
3876
3889
  }
3877
3890
 
3878
3891
  // Determine question type from aria-label
@@ -3901,11 +3914,14 @@ function generateLrsBridgeCode(options) {
3901
3914
  correct: isCorrect
3902
3915
  });
3903
3916
 
3917
+ // Increment question counter for this session
3918
+ kcQuestionCounter++;
3919
+
3904
3920
  // Send question answered statement using existing LRS method
3905
3921
  LRS.questionAnswered({
3906
3922
  questionId: questionId,
3907
3923
  questionGuid: blockId || generateUUID(),
3908
- questionNumber: 1,
3924
+ questionNumber: kcQuestionCounter,
3909
3925
  questionText: questionText.substring(0, 500),
3910
3926
  questionType: questionType,
3911
3927
  answer: answerText.substring(0, 500),
@@ -3966,6 +3982,142 @@ function generateLrsBridgeCode(options) {
3966
3982
  return answerText;
3967
3983
  }
3968
3984
 
3985
+ // ========================================================================
3986
+ // SCORM INTERACTION TRACKER
3987
+ // Intercepts cmi.interactions.N.* SetValue calls to capture quiz answers
3988
+ // directly from the SCORM data model \u2014 works regardless of Rise UI format
3989
+ // (Knowledge Check blocks, quiz lessons, etc.)
3990
+ // ========================================================================
3991
+ var scormInteractions = {}; // Pending interactions keyed by index N
3992
+ var scormInteractionsSent = {}; // Track which interactions were already sent
3993
+ var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
3994
+
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;
4004
+
4005
+ // Determine which SetValue function to wrap
4006
+ var setValueFn = apiType === '2004' ? 'SetValue' : 'LMSSetValue';
4007
+ var originalSetValue = api[setValueFn];
4008
+
4009
+ if (typeof originalSetValue !== 'function') {
4010
+ log('SCORM Interaction Tracker: No ' + setValueFn + ' function found');
4011
+ return;
4012
+ }
4013
+
4014
+ // SCORM 1.2 uses cmi.interactions.N.*, SCORM 2004 uses cmi.interactions.N.*
4015
+ var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4016
+
4017
+ api[setValueFn] = function(key, value) {
4018
+ // Always call the original first
4019
+ var result = originalSetValue.apply(api, arguments);
4020
+
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];
4027
+
4028
+ // Initialize interaction tracking for this index
4029
+ if (!scormInteractions[idx]) {
4030
+ scormInteractions[idx] = {};
4031
+ }
4032
+
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
+ };
4054
+
4055
+ var scormType = interaction.type || 'unknown';
4056
+ var questionType = typeMap[scormType] || scormType;
4057
+
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';
4062
+
4063
+ // Get student response \u2014 for 'choice' type, this might be indices like '1,3'
4064
+ var studentResponse = interaction.student_response || interaction['student_response'] || '';
4065
+
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
+ });
4075
+
4076
+ // Get interaction ID (Rise sets this to a unique question identifier)
4077
+ var interactionId = interaction.id || ('interaction-' + idx);
4078
+
4079
+ // Get lesson context
4080
+ var lessonInfo = getCachedLessonInfo();
4081
+
4082
+ // Increment question counter
4083
+ kcQuestionCounter++;
4084
+
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
+ });
4094
+
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
+ });
4110
+ }
4111
+ }
4112
+ }
4113
+
4114
+ return result;
4115
+ };
4116
+
4117
+ scormTrackerActive = true;
4118
+ log('SCORM Interaction Tracker: Wrapped ' + setValueFn + ' (apiType=' + apiType + ') \u2014 KC DOM handler will defer to SCORM tracker');
4119
+ }
4120
+
3969
4121
  function setupInteractionInterceptors() {
3970
4122
  if (!TRACK_INTERACTIONS) return;
3971
4123
 
@@ -4184,6 +4336,9 @@ function generateLrsBridgeCode(options) {
4184
4336
  LRS.scormApiFound = true;
4185
4337
  LRS.scormApiType = result.type;
4186
4338
 
4339
+ // Wrap SCORM SetValue to intercept cmi.interactions for quiz tracking
4340
+ setupScormInteractionTracker();
4341
+
4187
4342
  // Read learner_id for diagnostics
4188
4343
  var scormLearnerId = 'n/a', scormLearnerName = 'n/a';
4189
4344
  try {