@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.cjs CHANGED
@@ -3819,8 +3819,9 @@ function generateLrsBridgeCode(options) {
3819
3819
  });
3820
3820
  }
3821
3821
 
3822
- // Track submitted Knowledge Check blocks to avoid duplicates
3822
+ // Track submitted Knowledge Check questions to avoid duplicates
3823
3823
  var submittedKnowledgeChecks = {};
3824
+ var kcQuestionCounter = 0;
3824
3825
 
3825
3826
  /**
3826
3827
  * Set up interceptors specifically for Rise Knowledge Check blocks
@@ -3830,7 +3831,11 @@ function generateLrsBridgeCode(options) {
3830
3831
  if (!TRACK_QUIZZES) return;
3831
3832
 
3832
3833
  // Intercept submit button clicks on Knowledge Check blocks
3834
+ // NOTE: If the SCORM tracker is active, it handles quiz tracking via
3835
+ // cmi.interactions \u2014 skip DOM scraping to avoid duplicate statements
3833
3836
  document.addEventListener('click', function(e) {
3837
+ if (scormTrackerActive) return; // SCORM tracker handles this
3838
+
3834
3839
  var submitBtn = e.target.closest('.quiz-card__button');
3835
3840
  if (!submitBtn) return;
3836
3841
 
@@ -3852,16 +3857,26 @@ function generateLrsBridgeCode(options) {
3852
3857
  * Extract and send xAPI statement for a Knowledge Check submission
3853
3858
  */
3854
3859
  function extractKnowledgeCheckResult(kcBlock) {
3855
- // Get block ID for deduplication
3860
+ // Get block ID for context
3856
3861
  var blockContainer = kcBlock.closest('[data-block-id]');
3857
3862
  var blockId = blockContainer ? blockContainer.getAttribute('data-block-id') : null;
3858
3863
 
3864
+ // Get question text first \u2014 needed for per-question dedup key
3865
+ var questionText = '';
3866
+ var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
3867
+ if (questionTextEl) {
3868
+ questionText = questionTextEl.textContent.trim();
3869
+ }
3870
+
3859
3871
  // Get question ID from the title element
3860
3872
  var questionTitleEl = kcBlock.querySelector('.quiz-card__title');
3861
- var questionId = questionTitleEl ? questionTitleEl.id : (blockId ? 'q-' + blockId : 'q-' + generateUUID());
3873
+ var questionId = questionTitleEl ? questionTitleEl.id : null;
3874
+
3875
+ // Build a question-specific dedup key using question text hash
3876
+ // This ensures each question in a multi-question quiz block gets its own key
3877
+ var questionHash = questionText ? questionText.substring(0, 100) : (questionId || blockId || generateUUID());
3878
+ var submissionKey = 'kc-' + questionHash;
3862
3879
 
3863
- // Check if we already processed this submission (avoid duplicates)
3864
- var submissionKey = blockId || questionId;
3865
3880
  var feedbackLabel = kcBlock.querySelector('.quiz-card__feedback-label');
3866
3881
  if (!feedbackLabel) {
3867
3882
  log('Knowledge Check: No feedback visible yet');
@@ -3877,11 +3892,9 @@ function generateLrsBridgeCode(options) {
3877
3892
  }
3878
3893
  submittedKnowledgeChecks[submissionId] = true;
3879
3894
 
3880
- // Get question text
3881
- var questionText = '';
3882
- var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
3883
- if (questionTextEl) {
3884
- questionText = questionTextEl.textContent.trim();
3895
+ // Use question-specific ID for the statement (not the shared block ID)
3896
+ if (!questionId) {
3897
+ questionId = blockId ? 'q-' + blockId + '-' + questionHash.substring(0, 20) : 'q-' + generateUUID();
3885
3898
  }
3886
3899
 
3887
3900
  // Determine question type from aria-label
@@ -3910,11 +3923,14 @@ function generateLrsBridgeCode(options) {
3910
3923
  correct: isCorrect
3911
3924
  });
3912
3925
 
3926
+ // Increment question counter for this session
3927
+ kcQuestionCounter++;
3928
+
3913
3929
  // Send question answered statement using existing LRS method
3914
3930
  LRS.questionAnswered({
3915
3931
  questionId: questionId,
3916
3932
  questionGuid: blockId || generateUUID(),
3917
- questionNumber: 1,
3933
+ questionNumber: kcQuestionCounter,
3918
3934
  questionText: questionText.substring(0, 500),
3919
3935
  questionType: questionType,
3920
3936
  answer: answerText.substring(0, 500),
@@ -3975,6 +3991,142 @@ function generateLrsBridgeCode(options) {
3975
3991
  return answerText;
3976
3992
  }
3977
3993
 
3994
+ // ========================================================================
3995
+ // SCORM INTERACTION TRACKER
3996
+ // Intercepts cmi.interactions.N.* SetValue calls to capture quiz answers
3997
+ // directly from the SCORM data model \u2014 works regardless of Rise UI format
3998
+ // (Knowledge Check blocks, quiz lessons, etc.)
3999
+ // ========================================================================
4000
+ var scormInteractions = {}; // Pending interactions keyed by index N
4001
+ var scormInteractionsSent = {}; // Track which interactions were already sent
4002
+ var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4003
+
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;
4013
+
4014
+ // Determine which SetValue function to wrap
4015
+ var setValueFn = apiType === '2004' ? 'SetValue' : 'LMSSetValue';
4016
+ var originalSetValue = api[setValueFn];
4017
+
4018
+ if (typeof originalSetValue !== 'function') {
4019
+ log('SCORM Interaction Tracker: No ' + setValueFn + ' function found');
4020
+ return;
4021
+ }
4022
+
4023
+ // SCORM 1.2 uses cmi.interactions.N.*, SCORM 2004 uses cmi.interactions.N.*
4024
+ var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4025
+
4026
+ api[setValueFn] = function(key, value) {
4027
+ // Always call the original first
4028
+ var result = originalSetValue.apply(api, arguments);
4029
+
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];
4036
+
4037
+ // Initialize interaction tracking for this index
4038
+ if (!scormInteractions[idx]) {
4039
+ scormInteractions[idx] = {};
4040
+ }
4041
+
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
+ };
4063
+
4064
+ var scormType = interaction.type || 'unknown';
4065
+ var questionType = typeMap[scormType] || scormType;
4066
+
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';
4071
+
4072
+ // Get student response \u2014 for 'choice' type, this might be indices like '1,3'
4073
+ var studentResponse = interaction.student_response || interaction['student_response'] || '';
4074
+
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
+ });
4084
+
4085
+ // Get interaction ID (Rise sets this to a unique question identifier)
4086
+ var interactionId = interaction.id || ('interaction-' + idx);
4087
+
4088
+ // Get lesson context
4089
+ var lessonInfo = getCachedLessonInfo();
4090
+
4091
+ // Increment question counter
4092
+ kcQuestionCounter++;
4093
+
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
+ });
4103
+
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
+ });
4119
+ }
4120
+ }
4121
+ }
4122
+
4123
+ return result;
4124
+ };
4125
+
4126
+ scormTrackerActive = true;
4127
+ log('SCORM Interaction Tracker: Wrapped ' + setValueFn + ' (apiType=' + apiType + ') \u2014 KC DOM handler will defer to SCORM tracker');
4128
+ }
4129
+
3978
4130
  function setupInteractionInterceptors() {
3979
4131
  if (!TRACK_INTERACTIONS) return;
3980
4132
 
@@ -4193,6 +4345,9 @@ function generateLrsBridgeCode(options) {
4193
4345
  LRS.scormApiFound = true;
4194
4346
  LRS.scormApiType = result.type;
4195
4347
 
4348
+ // Wrap SCORM SetValue to intercept cmi.interactions for quiz tracking
4349
+ setupScormInteractionTracker();
4350
+
4196
4351
  // Read learner_id for diagnostics
4197
4352
  var scormLearnerId = 'n/a', scormLearnerName = 'n/a';
4198
4353
  try {