@patch-adams/core 1.4.2 → 1.4.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/cli.js CHANGED
@@ -1047,48 +1047,55 @@ function generateLrsBridgeCode(options) {
1047
1047
 
1048
1048
  function setupBridge() {
1049
1049
  try {
1050
- // 1. Try PlayerIntegration first (Xyleme native mode)
1050
+ // 1. Try PlayerIntegration first (Xyleme native mode) - for SCORM APIs
1051
1051
  var integrationResult = findPlayerIntegration(10);
1052
1052
  if (integrationResult) {
1053
1053
  LRS.integration = integrationResult.integration;
1054
1054
  LRS.integrationLevel = integrationResult.level;
1055
- LRS.mode = 'playerIntegration';
1056
- log('Using PlayerIntegration mode');
1055
+ log('Found PlayerIntegration at level', integrationResult.level);
1057
1056
 
1058
1057
  if (LRS.integration.Internal_SCORM_2004_API) {
1059
1058
  window.API_1484_11 = LRS.integration.Internal_SCORM_2004_API;
1060
1059
  LRS.scormApi = window.API_1484_11;
1060
+ LRS.scormApiFound = true;
1061
+ LRS.scormApiType = '2004';
1061
1062
  }
1062
1063
  if (LRS.integration.Internal_SCORM_12_API) {
1063
1064
  window.API = LRS.integration.Internal_SCORM_12_API;
1065
+ if (!LRS.scormApiFound) {
1066
+ LRS.scormApi = window.API;
1067
+ LRS.scormApiFound = true;
1068
+ LRS.scormApiType = '1.2';
1069
+ }
1064
1070
  }
1065
- return true;
1066
- }
1067
-
1068
- // 2. Search for SCORM APIs
1069
- log('No PlayerIntegration found, searching for SCORM APIs...');
1070
-
1071
- var api2004 = findAPIInFrameHierarchy('API_1484_11', 10);
1072
- if (api2004) {
1073
- LRS.scormApi = api2004.api;
1074
- LRS.scormApiLevel = api2004.level;
1075
- LRS.scormApiWindow = api2004.window;
1076
- LRS.scormApiFound = true;
1077
- LRS.scormApiType = '2004';
1078
- log('Found SCORM 2004 API at level ' + api2004.level);
1071
+ // Don't return early - continue to detect LRS endpoint for direct xAPI
1079
1072
  } else {
1080
- var api12 = findAPIInFrameHierarchy('API', 10);
1081
- if (api12) {
1082
- LRS.scormApi = api12.api;
1083
- LRS.scormApiLevel = api12.level;
1084
- LRS.scormApiWindow = api12.window;
1073
+ // 2. Search for SCORM APIs directly
1074
+ log('No PlayerIntegration found, searching for SCORM APIs...');
1075
+
1076
+ var api2004 = findAPIInFrameHierarchy('API_1484_11', 10);
1077
+ if (api2004) {
1078
+ LRS.scormApi = api2004.api;
1079
+ LRS.scormApiLevel = api2004.level;
1080
+ LRS.scormApiWindow = api2004.window;
1085
1081
  LRS.scormApiFound = true;
1086
- LRS.scormApiType = '1.2';
1087
- log('Found SCORM 1.2 API at level ' + api12.level);
1082
+ LRS.scormApiType = '2004';
1083
+ log('Found SCORM 2004 API at level ' + api2004.level);
1084
+ } else {
1085
+ var api12 = findAPIInFrameHierarchy('API', 10);
1086
+ if (api12) {
1087
+ LRS.scormApi = api12.api;
1088
+ LRS.scormApiLevel = api12.level;
1089
+ LRS.scormApiWindow = api12.window;
1090
+ LRS.scormApiFound = true;
1091
+ LRS.scormApiType = '1.2';
1092
+ log('Found SCORM 1.2 API at level ' + api12.level);
1093
+ }
1088
1094
  }
1089
1095
  }
1090
1096
 
1091
- // 3. Determine LRS endpoint
1097
+ // 3. ALWAYS try to determine LRS endpoint for direct xAPI statements
1098
+ // This is critical for detailed tracking (media, interactions, quizzes)
1092
1099
  // Priority: 1) Configured endpoint, 2) Auto-detected Bravais endpoint
1093
1100
  if (LRS_ENDPOINT_CONFIG && LRS_ENDPOINT_CONFIG.length > 0) {
1094
1101
  LRS_ENDPOINT = LRS_ENDPOINT_CONFIG;
@@ -1104,9 +1111,17 @@ function generateLrsBridgeCode(options) {
1104
1111
  LRS.lrsEndpoint = LRS_ENDPOINT;
1105
1112
 
1106
1113
  // 4. Determine mode based on what we found
1114
+ // Prefer directLRS when available for detailed tracking
1107
1115
  if (LRS_ENDPOINT && LRS_ENDPOINT.length > 0) {
1108
1116
  LRS.mode = 'directLRS';
1109
1117
  log('Using direct LRS mode with endpoint:', LRS_ENDPOINT);
1118
+ if (LRS.integration) {
1119
+ log('PlayerIntegration also available for SCORM state');
1120
+ }
1121
+ return true;
1122
+ } else if (LRS.integration) {
1123
+ LRS.mode = 'playerIntegration';
1124
+ log('Using PlayerIntegration mode (no direct LRS endpoint)');
1110
1125
  return true;
1111
1126
  } else if (LRS.scormApiFound) {
1112
1127
  LRS.mode = 'scormOnly';
@@ -2325,6 +2340,154 @@ function generateLrsBridgeCode(options) {
2325
2340
  return info;
2326
2341
  }
2327
2342
 
2343
+ // ========================================================================
2344
+ // 4b. LESSON/SECTION NAME EXTRACTION
2345
+ // ========================================================================
2346
+
2347
+ /**
2348
+ * Get the current lesson/section information from Rise DOM
2349
+ * Returns { id, name, lessonIndex, sectionName }
2350
+ */
2351
+ function getCurrentLessonInfo() {
2352
+ var lessonInfo = {
2353
+ id: window.location.hash || '#/',
2354
+ name: null,
2355
+ lessonIndex: null,
2356
+ sectionName: null
2357
+ };
2358
+
2359
+ try {
2360
+ // 1. Try to get from Rise's navigation sidebar
2361
+ // Rise shows active lesson with specific classes
2362
+ var activeLesson = document.querySelector(
2363
+ '.sidebar__link--current, ' +
2364
+ '.sidebar-lesson--active, ' +
2365
+ '.nav-sidebar__lesson--active, ' +
2366
+ '[class*="sidebar"] [class*="current"], ' +
2367
+ '[class*="sidebar"] [class*="active"], ' +
2368
+ '[aria-current="page"], ' +
2369
+ '[aria-current="true"]'
2370
+ );
2371
+
2372
+ if (activeLesson) {
2373
+ var lessonTitle = activeLesson.querySelector(
2374
+ '.sidebar__link-text, ' +
2375
+ '.lesson-title, ' +
2376
+ '.sidebar-lesson__title, ' +
2377
+ '[class*="title"], ' +
2378
+ '[class*="label"]'
2379
+ );
2380
+ if (lessonTitle) {
2381
+ lessonInfo.name = lessonTitle.textContent.trim();
2382
+ } else {
2383
+ lessonInfo.name = activeLesson.textContent.trim();
2384
+ }
2385
+
2386
+ // Try to get the section/module name (parent group)
2387
+ var section = activeLesson.closest(
2388
+ '.sidebar__section, ' +
2389
+ '.sidebar-section, ' +
2390
+ '[class*="section"], ' +
2391
+ '[class*="module"]'
2392
+ );
2393
+ if (section) {
2394
+ var sectionTitle = section.querySelector(
2395
+ '.sidebar__section-title, ' +
2396
+ '.section-title, ' +
2397
+ '[class*="section-title"], ' +
2398
+ '[class*="module-title"], ' +
2399
+ 'h2, h3'
2400
+ );
2401
+ if (sectionTitle) {
2402
+ lessonInfo.sectionName = sectionTitle.textContent.trim();
2403
+ }
2404
+ }
2405
+ }
2406
+
2407
+ // 2. Try Rise's header/breadcrumb
2408
+ if (!lessonInfo.name) {
2409
+ var headerTitle = document.querySelector(
2410
+ '.lesson-header__title, ' +
2411
+ '.lesson__title, ' +
2412
+ '.content-header__title, ' +
2413
+ '[class*="lesson"] [class*="header"] [class*="title"], ' +
2414
+ '.blocks-lesson-header__title'
2415
+ );
2416
+ if (headerTitle) {
2417
+ lessonInfo.name = headerTitle.textContent.trim();
2418
+ }
2419
+ }
2420
+
2421
+ // 3. Try breadcrumbs
2422
+ if (!lessonInfo.name || !lessonInfo.sectionName) {
2423
+ var breadcrumbs = document.querySelectorAll(
2424
+ '.breadcrumb__item, ' +
2425
+ '.breadcrumbs li, ' +
2426
+ '[class*="breadcrumb"] span, ' +
2427
+ '[class*="breadcrumb"] a'
2428
+ );
2429
+ if (breadcrumbs.length >= 2) {
2430
+ // Usually: Course > Section > Lesson
2431
+ if (!lessonInfo.sectionName && breadcrumbs.length >= 2) {
2432
+ lessonInfo.sectionName = breadcrumbs[breadcrumbs.length - 2].textContent.trim();
2433
+ }
2434
+ if (!lessonInfo.name) {
2435
+ lessonInfo.name = breadcrumbs[breadcrumbs.length - 1].textContent.trim();
2436
+ }
2437
+ }
2438
+ }
2439
+
2440
+ // 4. Try Rise's internal data
2441
+ if (window.__RISE_COURSE_DATA__ && window.__RISE_COURSE_DATA__.lessons) {
2442
+ var lessonId = lessonInfo.id.replace('#/lessons/', '').replace('#/', '');
2443
+ var lessons = window.__RISE_COURSE_DATA__.lessons;
2444
+ for (var i = 0; i < lessons.length; i++) {
2445
+ if (lessons[i].id === lessonId || lessons[i].slug === lessonId) {
2446
+ lessonInfo.name = lessons[i].title || lessonInfo.name;
2447
+ lessonInfo.lessonIndex = i;
2448
+ break;
2449
+ }
2450
+ }
2451
+ }
2452
+
2453
+ // 5. Clean up - remove any "Home" or generic names if we're on a real lesson
2454
+ if (lessonInfo.name) {
2455
+ lessonInfo.name = lessonInfo.name.replace(/^\\d+\\.\\s*/, ''); // Remove leading numbers "1. "
2456
+ if (lessonInfo.name.length > 200) {
2457
+ lessonInfo.name = lessonInfo.name.substring(0, 200) + '...';
2458
+ }
2459
+ }
2460
+
2461
+ if (lessonInfo.sectionName) {
2462
+ lessonInfo.sectionName = lessonInfo.sectionName.replace(/^\\d+\\.\\s*/, '');
2463
+ if (lessonInfo.sectionName.length > 200) {
2464
+ lessonInfo.sectionName = lessonInfo.sectionName.substring(0, 200) + '...';
2465
+ }
2466
+ }
2467
+
2468
+ } catch (e) {
2469
+ warn('Error extracting lesson info:', e);
2470
+ }
2471
+
2472
+ log('Current lesson info:', lessonInfo);
2473
+ return lessonInfo;
2474
+ }
2475
+
2476
+ /**
2477
+ * Cache the current lesson info (updates on navigation)
2478
+ */
2479
+ var cachedLessonInfo = null;
2480
+ var cachedLessonHash = null;
2481
+
2482
+ function getCachedLessonInfo() {
2483
+ var currentHash = window.location.hash;
2484
+ if (cachedLessonHash !== currentHash) {
2485
+ cachedLessonInfo = getCurrentLessonInfo();
2486
+ cachedLessonHash = currentHash;
2487
+ }
2488
+ return cachedLessonInfo || getCurrentLessonInfo();
2489
+ }
2490
+
2328
2491
  // ========================================================================
2329
2492
  // 5. XAPI STATEMENT BUILDING
2330
2493
  // ========================================================================
@@ -2669,14 +2832,20 @@ function generateLrsBridgeCode(options) {
2669
2832
  LRS.eventLog.push({
2670
2833
  statement: statement,
2671
2834
  timestamp: new Date().toISOString(),
2672
- mode: LRS.mode
2835
+ mode: LRS.mode,
2836
+ lrsEndpoint: LRS_ENDPOINT
2673
2837
  });
2674
2838
 
2675
- // Route based on mode
2676
- if (LRS.mode === 'playerIntegration' && LRS.integration) {
2677
- return sendViaPlayerIntegration(statement);
2678
- } else if (LRS.mode === 'directLRS' && LRS_ENDPOINT) {
2839
+ // ALWAYS prefer direct LRS when endpoint is available
2840
+ // This ensures all our detailed tracking (media, interactions, etc.) goes to the LRS
2841
+ // PlayerIntegration is used only for SCORM state, not for xAPI statements
2842
+ if (LRS_ENDPOINT && LRS_ENDPOINT.length > 0) {
2843
+ log('Sending via directLRS:', LRS_ENDPOINT);
2679
2844
  return sendViaDirectLRS(statement);
2845
+ } else if (LRS.mode === 'playerIntegration' && LRS.integration) {
2846
+ // Fallback to PlayerIntegration only if no direct LRS endpoint
2847
+ log('No LRS endpoint, falling back to PlayerIntegration');
2848
+ return sendViaPlayerIntegration(statement);
2680
2849
  } else {
2681
2850
  // SCORM-only or offline mode - just log
2682
2851
  log('Statement logged (no LRS):', statement.verb.display['en-US']);
@@ -2815,13 +2984,16 @@ function generateLrsBridgeCode(options) {
2815
2984
  LRS.contentOpened = function(data) {
2816
2985
  if (!TRACK_NAVIGATION) return;
2817
2986
 
2987
+ // Get current lesson info from DOM (includes name)
2988
+ var lessonInfo = getCurrentLessonInfo();
2989
+
2818
2990
  var result = null;
2819
2991
  if (data.duration) {
2820
2992
  result = { duration: data.duration };
2821
2993
  }
2822
2994
 
2823
2995
  // Extract clean page/lesson ID from Rise hash paths
2824
- var lessonId = data.pageGuid || data.lessonId || '';
2996
+ var lessonId = data.pageGuid || data.lessonId || lessonInfo.id || '';
2825
2997
  if (lessonId.indexOf('#/lessons/') === 0) {
2826
2998
  lessonId = lessonId.substring(10); // Remove '#/lessons/'
2827
2999
  } else if (lessonId.indexOf('#/') === 0) {
@@ -2832,7 +3004,9 @@ function generateLrsBridgeCode(options) {
2832
3004
  var additionalContext = {
2833
3005
  extensions: {
2834
3006
  pageId: lessonId || 'home',
2835
- pageTitle: data.title || 'Page',
3007
+ pageTitle: data.title || lessonInfo.name || 'Page',
3008
+ lessonName: lessonInfo.name,
3009
+ sectionName: lessonInfo.sectionName,
2836
3010
  pageUrl: data.url || window.location.href
2837
3011
  }
2838
3012
  };
@@ -2842,7 +3016,12 @@ function generateLrsBridgeCode(options) {
2842
3016
  var statement = buildStatement(
2843
3017
  'experienced',
2844
3018
  'http://xyleme.com/bravais/activities/document',
2845
- { event: 'page_viewed', pageId: lessonId },
3019
+ {
3020
+ event: 'page_viewed',
3021
+ pageId: lessonId,
3022
+ lessonName: lessonInfo.name,
3023
+ sectionName: lessonInfo.sectionName
3024
+ },
2846
3025
  result,
2847
3026
  additionalContext
2848
3027
  );
@@ -2853,6 +3032,9 @@ function generateLrsBridgeCode(options) {
2853
3032
  LRS.mediaPlayed = function(data) {
2854
3033
  if (!TRACK_MEDIA) return;
2855
3034
 
3035
+ // Get current lesson context
3036
+ var lessonInfo = getCachedLessonInfo();
3037
+
2856
3038
  var mediaType = data.type === 'audio' ? ACTIVITY_TYPES.audio : ACTIVITY_TYPES.video;
2857
3039
  var verbKey = data.action === 'play' ? 'played' :
2858
3040
  data.action === 'pause' ? 'paused' :
@@ -2863,7 +3045,11 @@ function generateLrsBridgeCode(options) {
2863
3045
  mediaSrc: data.src || 'media-' + Date.now(),
2864
3046
  mediaName: data.name || (data.type || 'Media'),
2865
3047
  mediaType: data.type || 'video',
2866
- 'https://w3id.org/xapi/video/extensions/length': data.duration || 0
3048
+ 'https://w3id.org/xapi/video/extensions/length': data.duration || 0,
3049
+ // Include lesson/section context
3050
+ lessonId: lessonInfo.id,
3051
+ lessonName: lessonInfo.name,
3052
+ sectionName: lessonInfo.sectionName
2867
3053
  };
2868
3054
 
2869
3055
  var result = {
@@ -2886,12 +3072,21 @@ function generateLrsBridgeCode(options) {
2886
3072
  LRS.assessmentStarted = function(data) {
2887
3073
  if (!TRACK_QUIZZES) return;
2888
3074
 
3075
+ // Get lesson context
3076
+ var lessonInfo = getCachedLessonInfo();
3077
+
2889
3078
  // Build assessment activity object for Xyleme format
2890
3079
  var assessmentObject = buildAssessmentActivityObject({
2891
3080
  assessmentGuid: data.assessmentGuid || data.id,
2892
- title: data.title || 'Assessment'
3081
+ title: data.title || data.assessmentName || 'Assessment'
2893
3082
  });
2894
3083
 
3084
+ // Add lesson context to extensions
3085
+ assessmentObject.definition = assessmentObject.definition || {};
3086
+ assessmentObject.definition.extensions = assessmentObject.definition.extensions || {};
3087
+ assessmentObject.definition.extensions.lessonName = lessonInfo.name;
3088
+ assessmentObject.definition.extensions.sectionName = lessonInfo.sectionName;
3089
+
2895
3090
  var statement = buildStatementXyleme('attempted', assessmentObject);
2896
3091
  sendStatement(statement);
2897
3092
  };
@@ -2900,13 +3095,31 @@ function generateLrsBridgeCode(options) {
2900
3095
  LRS.assessmentEnded = function(data) {
2901
3096
  if (!TRACK_QUIZZES) return;
2902
3097
 
3098
+ // Get lesson context (may be passed in from extractQuizResult)
3099
+ var lessonInfo = data.lessonName ? { name: data.lessonName, sectionName: data.sectionName } : getCachedLessonInfo();
3100
+
2903
3101
  // Build assessment activity object for Xyleme format
2904
3102
  var assessmentObject = buildAssessmentActivityObject({
2905
- assessmentGuid: data.assessmentGuid || data.id,
2906
- title: data.title || 'Assessment'
3103
+ assessmentGuid: data.assessmentGuid || data.assessmentId || data.id,
3104
+ title: data.title || data.assessmentName || 'Assessment'
2907
3105
  });
2908
3106
 
2909
- var result = { completion: true };
3107
+ // Add lesson context to extensions
3108
+ assessmentObject.definition = assessmentObject.definition || {};
3109
+ assessmentObject.definition.extensions = assessmentObject.definition.extensions || {};
3110
+ assessmentObject.definition.extensions.lessonName = lessonInfo.name;
3111
+ assessmentObject.definition.extensions.sectionName = lessonInfo.sectionName;
3112
+ assessmentObject.definition.extensions.assessmentName = data.assessmentName || data.title;
3113
+
3114
+ var result = {
3115
+ completion: true,
3116
+ extensions: {
3117
+ 'https://patch-adams.io/xapi/extensions/assessmentName': data.assessmentName || data.title || null,
3118
+ 'https://patch-adams.io/xapi/extensions/lessonName': lessonInfo.name || null,
3119
+ 'https://patch-adams.io/xapi/extensions/sectionName': lessonInfo.sectionName || null,
3120
+ 'https://patch-adams.io/xapi/extensions/questionCount': data.questionCount || null
3121
+ }
3122
+ };
2910
3123
 
2911
3124
  if (typeof data.score === 'number') {
2912
3125
  result.score = {
@@ -2935,14 +3148,27 @@ function generateLrsBridgeCode(options) {
2935
3148
  LRS.questionAnswered = function(data) {
2936
3149
  if (!TRACK_QUIZZES) return;
2937
3150
 
3151
+ // Get lesson context if not provided
3152
+ var lessonInfo = data.lessonName ? data : getCachedLessonInfo();
3153
+
2938
3154
  // Build question activity object for Xyleme format
2939
3155
  var questionObject = buildQuestionActivityObject({
2940
3156
  questionGuid: data.questionGuid || data.questionId,
2941
3157
  text: data.questionText || 'Question'
2942
3158
  });
2943
3159
 
3160
+ // Build comprehensive result with human-readable data
2944
3161
  var result = {
2945
- response: data.answer || data.response || ''
3162
+ response: data.answer || data.response || '',
3163
+ extensions: {
3164
+ 'https://patch-adams.io/xapi/extensions/questionNumber': data.questionNumber || null,
3165
+ 'https://patch-adams.io/xapi/extensions/questionText': data.questionText || null,
3166
+ 'https://patch-adams.io/xapi/extensions/answerText': data.answer || data.response || null,
3167
+ 'https://patch-adams.io/xapi/extensions/correctAnswer': data.correctAnswer || null,
3168
+ 'https://patch-adams.io/xapi/extensions/assessmentName': data.assessmentName || null,
3169
+ 'https://patch-adams.io/xapi/extensions/lessonName': lessonInfo.lessonName || lessonInfo.name || null,
3170
+ 'https://patch-adams.io/xapi/extensions/sectionName': lessonInfo.sectionName || null
3171
+ }
2946
3172
  };
2947
3173
 
2948
3174
  if (data.result === 'correct' || data.correct === true) {
@@ -2974,6 +3200,9 @@ function generateLrsBridgeCode(options) {
2974
3200
  LRS.interacted = function(data) {
2975
3201
  if (!TRACK_INTERACTIONS) return;
2976
3202
 
3203
+ // Get current lesson context
3204
+ var lessonInfo = getCachedLessonInfo();
3205
+
2977
3206
  // Activity type is interaction, details include interaction ID and type
2978
3207
  var statement = buildStatement(
2979
3208
  'interacted',
@@ -2981,7 +3210,11 @@ function generateLrsBridgeCode(options) {
2981
3210
  {
2982
3211
  interactionId: data.id || 'interaction-' + Date.now(),
2983
3212
  interactionName: data.name || data.type || 'Interaction',
2984
- interactionType: data.interactionType || data.type || 'other'
3213
+ interactionType: data.interactionType || data.type || 'other',
3214
+ // Include lesson/section context
3215
+ lessonId: lessonInfo.id,
3216
+ lessonName: lessonInfo.name,
3217
+ sectionName: lessonInfo.sectionName
2985
3218
  }
2986
3219
  );
2987
3220
  sendStatement(statement);
@@ -3062,49 +3295,146 @@ function generateLrsBridgeCode(options) {
3062
3295
  function setupMediaInterceptors() {
3063
3296
  if (!TRACK_MEDIA) return;
3064
3297
 
3065
- document.addEventListener('play', function(e) {
3066
- var target = e.target;
3067
- if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
3298
+ // Track which elements we've already attached listeners to
3299
+ var trackedMedia = new WeakSet();
3300
+
3301
+ // Helper to get media name/title from element or surrounding context
3302
+ function getMediaName(el) {
3303
+ // Check element attributes
3304
+ var name = el.title || el.getAttribute('aria-label') || el.getAttribute('data-name');
3305
+ if (name) return name;
3306
+
3307
+ // Check parent Rise video block for title
3308
+ var videoBlock = findClosest(el, '[data-block-type="video"], .blocks-video, .video-block');
3309
+ if (videoBlock) {
3310
+ var titleEl = videoBlock.querySelector('.video-title, .block-title, [class*="title"]');
3311
+ if (titleEl) return titleEl.textContent.trim();
3312
+ }
3313
+
3314
+ // Use src filename as fallback
3315
+ var src = el.src || el.currentSrc || '';
3316
+ if (src) {
3317
+ var filename = src.split('/').pop().split('?')[0];
3318
+ if (filename && filename.length < 100) return filename;
3319
+ }
3320
+
3321
+ return el.tagName.toLowerCase();
3322
+ }
3323
+
3324
+ // Attach media event listeners to a single element
3325
+ function attachMediaListeners(el) {
3326
+ if (trackedMedia.has(el)) return;
3327
+ trackedMedia.add(el);
3328
+
3329
+ var mediaType = el.tagName.toLowerCase();
3330
+ log('Attaching media listeners to:', mediaType, el.src || el.currentSrc || 'no-src');
3331
+
3332
+ el.addEventListener('play', function() {
3333
+ log('Media play event captured:', mediaType);
3068
3334
  LRS.mediaPlayed({
3069
- type: target.tagName.toLowerCase(),
3070
- src: target.src || target.currentSrc || '',
3071
- name: target.title || target.getAttribute('aria-label') || '',
3072
- currentTime: target.currentTime || 0,
3073
- duration: target.duration || 0,
3335
+ type: mediaType,
3336
+ src: el.src || el.currentSrc || '',
3337
+ name: getMediaName(el),
3338
+ currentTime: el.currentTime || 0,
3339
+ duration: el.duration || 0,
3074
3340
  action: 'play'
3075
3341
  });
3076
- }
3077
- }, true);
3342
+ });
3078
3343
 
3079
- document.addEventListener('pause', function(e) {
3080
- var target = e.target;
3081
- if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
3344
+ el.addEventListener('pause', function() {
3345
+ // Ignore pause events that fire right before ended
3346
+ if (el.ended) return;
3347
+ log('Media pause event captured:', mediaType);
3082
3348
  LRS.mediaPlayed({
3083
- type: target.tagName.toLowerCase(),
3084
- src: target.src || target.currentSrc || '',
3085
- name: target.title || target.getAttribute('aria-label') || '',
3086
- currentTime: target.currentTime || 0,
3087
- duration: target.duration || 0,
3349
+ type: mediaType,
3350
+ src: el.src || el.currentSrc || '',
3351
+ name: getMediaName(el),
3352
+ currentTime: el.currentTime || 0,
3353
+ duration: el.duration || 0,
3088
3354
  action: 'pause'
3089
3355
  });
3090
- }
3091
- }, true);
3356
+ });
3092
3357
 
3093
- document.addEventListener('ended', function(e) {
3094
- var target = e.target;
3095
- if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
3358
+ el.addEventListener('ended', function() {
3359
+ log('Media ended event captured:', mediaType);
3096
3360
  LRS.mediaPlayed({
3097
- type: target.tagName.toLowerCase(),
3098
- src: target.src || target.currentSrc || '',
3099
- name: target.title || target.getAttribute('aria-label') || '',
3100
- currentTime: target.duration || 0,
3101
- duration: target.duration || 0,
3361
+ type: mediaType,
3362
+ src: el.src || el.currentSrc || '',
3363
+ name: getMediaName(el),
3364
+ currentTime: el.duration || 0,
3365
+ duration: el.duration || 0,
3102
3366
  action: 'completed'
3103
3367
  });
3368
+ });
3369
+ }
3370
+
3371
+ // Scan for and attach listeners to all video/audio elements
3372
+ function scanForMedia(root) {
3373
+ var elements = (root || document).querySelectorAll('video, audio');
3374
+ log('Scanning for media elements, found:', elements.length);
3375
+ for (var i = 0; i < elements.length; i++) {
3376
+ attachMediaListeners(elements[i]);
3377
+ }
3378
+ }
3379
+
3380
+ // Initial scan
3381
+ scanForMedia();
3382
+
3383
+ // Document-level capture for any we might miss
3384
+ document.addEventListener('play', function(e) {
3385
+ var target = e.target;
3386
+ if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
3387
+ // Attach listeners if not already tracked
3388
+ if (!trackedMedia.has(target)) {
3389
+ log('Captured play from untracked media, attaching listeners');
3390
+ attachMediaListeners(target);
3391
+ }
3104
3392
  }
3105
3393
  }, true);
3106
3394
 
3107
- log('Media interceptors set up');
3395
+ // MutationObserver to detect dynamically added video/audio elements
3396
+ var mediaObserver = new MutationObserver(function(mutations) {
3397
+ var needsScan = false;
3398
+ mutations.forEach(function(mutation) {
3399
+ mutation.addedNodes.forEach(function(node) {
3400
+ if (node.nodeType === 1) {
3401
+ if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') {
3402
+ attachMediaListeners(node);
3403
+ } else if (node.querySelectorAll) {
3404
+ // Check for nested video/audio
3405
+ var nested = node.querySelectorAll('video, audio');
3406
+ if (nested.length > 0) {
3407
+ needsScan = true;
3408
+ }
3409
+ }
3410
+ }
3411
+ });
3412
+ });
3413
+ if (needsScan) {
3414
+ scanForMedia();
3415
+ }
3416
+ });
3417
+
3418
+ // Start observing once DOM is ready
3419
+ function startMediaObserver() {
3420
+ if (document.body) {
3421
+ mediaObserver.observe(document.body, {
3422
+ childList: true,
3423
+ subtree: true
3424
+ });
3425
+ log('Media mutation observer started');
3426
+ } else {
3427
+ setTimeout(startMediaObserver, 100);
3428
+ }
3429
+ }
3430
+ startMediaObserver();
3431
+
3432
+ // Periodic rescan for Rise lazy-loaded content
3433
+ setInterval(function() {
3434
+ scanForMedia();
3435
+ }, 3000);
3436
+
3437
+ log('Media interceptors set up (enhanced)');
3108
3438
  }
3109
3439
 
3110
3440
  function setupNavigationInterceptors() {
@@ -3185,43 +3515,152 @@ function generateLrsBridgeCode(options) {
3185
3515
  'quiz-' + Date.now();
3186
3516
  }
3187
3517
 
3518
+ /**
3519
+ * Extract quiz/assessment name from the DOM
3520
+ */
3521
+ function extractAssessmentName(node) {
3522
+ // Try various selectors for assessment/quiz titles
3523
+ var titleEl = node.querySelector(
3524
+ '.quiz-title, .assessment-title, .knowledge-check-title, ' +
3525
+ '.blocks-quiz__title, .block-title, ' +
3526
+ '[class*="quiz"] [class*="title"], ' +
3527
+ '[class*="assessment"] [class*="title"], ' +
3528
+ 'h2, h3'
3529
+ );
3530
+ if (titleEl) {
3531
+ return titleEl.textContent.trim().substring(0, 200);
3532
+ }
3533
+
3534
+ // Try to get from parent block
3535
+ var parentBlock = node.closest('[data-block-type]');
3536
+ if (parentBlock) {
3537
+ var blockTitle = parentBlock.querySelector('.block-title, h2, h3');
3538
+ if (blockTitle) {
3539
+ return blockTitle.textContent.trim().substring(0, 200);
3540
+ }
3541
+ }
3542
+
3543
+ // Get current lesson name as context
3544
+ var lessonInfo = getCachedLessonInfo();
3545
+ if (lessonInfo.name) {
3546
+ return 'Quiz in ' + lessonInfo.name;
3547
+ }
3548
+
3549
+ return 'Knowledge Check';
3550
+ }
3551
+
3188
3552
  function extractQuizResult(node) {
3189
3553
  var scoreEl = node.querySelector('.score-value, .quiz-score, .score-percentage, [class*="score"]');
3190
3554
  var score = scoreEl ? parseFloat(scoreEl.textContent.replace(/[^0-9.]/g, '')) : null;
3191
3555
 
3192
- var questions = node.querySelectorAll('.question-result, .question-feedback, .question-item, [class*="question"]');
3556
+ // Get assessment name and lesson context
3557
+ var assessmentName = extractAssessmentName(node);
3558
+ var lessonInfo = getCachedLessonInfo();
3559
+
3560
+ // Find all question containers - Rise uses various structures
3561
+ var questions = node.querySelectorAll(
3562
+ '.question-result, .question-feedback, .question-item, ' +
3563
+ '.blocks-quiz__question, .quiz-question, ' +
3564
+ '[class*="question-"][class*="result"], ' +
3565
+ '[class*="question-"][class*="feedback"], ' +
3566
+ '[data-question-id]'
3567
+ );
3568
+
3569
+ log('Found', questions.length, 'question elements in quiz result');
3193
3570
 
3194
3571
  if (questions.length > 0) {
3195
3572
  questions.forEach(function(q, index) {
3573
+ // Determine if correct - check multiple indicators
3196
3574
  var isCorrect = q.classList.contains('correct') ||
3197
- q.querySelector('.correct, [class*="correct"]') !== null ||
3198
- q.getAttribute('data-correct') === 'true';
3575
+ q.classList.contains('is-correct') ||
3576
+ q.querySelector('.correct, .is-correct, [class*="correct"]') !== null ||
3577
+ q.getAttribute('data-correct') === 'true' ||
3578
+ q.getAttribute('data-result') === 'correct';
3199
3579
 
3580
+ // Get the question text - try multiple selectors
3200
3581
  var questionText = '';
3201
- var questionEl = q.querySelector('.question-text, .question-stem, .question-title, [class*="question-text"]');
3582
+ var questionEl = q.querySelector(
3583
+ '.question-text, .question-stem, .question-title, ' +
3584
+ '.blocks-quiz__question-text, .quiz-question__text, ' +
3585
+ '[class*="question-text"], [class*="question-stem"], ' +
3586
+ '[class*="prompt"], p:first-of-type'
3587
+ );
3202
3588
  if (questionEl) {
3203
3589
  questionText = questionEl.textContent.trim();
3204
3590
  }
3205
3591
 
3592
+ // Get the learner's selected answer text
3206
3593
  var answerText = '';
3207
- var answerEl = q.querySelector('.selected-answer, .learner-response, .answer-text, [class*="response"]');
3594
+ var answerEl = q.querySelector(
3595
+ '.selected-answer, .learner-response, .answer-text, ' +
3596
+ '.user-answer, .chosen-answer, .response-text, ' +
3597
+ '.blocks-quiz__response, .quiz-question__response, ' +
3598
+ '[class*="selected"], [class*="chosen"], [class*="response"], ' +
3599
+ '[class*="user-answer"]'
3600
+ );
3208
3601
  if (answerEl) {
3209
3602
  answerText = answerEl.textContent.trim();
3210
3603
  }
3211
3604
 
3605
+ // If no specific answer element, try to find selected radio/checkbox label
3606
+ if (!answerText) {
3607
+ var selectedInput = q.querySelector('input:checked, [aria-checked="true"]');
3608
+ if (selectedInput) {
3609
+ var label = q.querySelector('label[for="' + selectedInput.id + '"]');
3610
+ if (label) {
3611
+ answerText = label.textContent.trim();
3612
+ } else {
3613
+ var parentLabel = selectedInput.closest('label');
3614
+ if (parentLabel) {
3615
+ answerText = parentLabel.textContent.trim();
3616
+ }
3617
+ }
3618
+ }
3619
+ }
3620
+
3621
+ // Get question ID if available
3622
+ var questionId = q.getAttribute('data-question-id') ||
3623
+ q.getAttribute('data-block-id') ||
3624
+ q.getAttribute('id') ||
3625
+ 'q' + (index + 1);
3626
+
3627
+ // Get correct answer text if available (for reporting)
3628
+ var correctAnswerText = '';
3629
+ var correctEl = q.querySelector(
3630
+ '.correct-answer, .right-answer, ' +
3631
+ '[class*="correct-answer"], [class*="right-answer"]'
3632
+ );
3633
+ if (correctEl) {
3634
+ correctAnswerText = correctEl.textContent.trim();
3635
+ }
3636
+
3637
+ log('Question', index + 1, ':', {
3638
+ questionText: questionText.substring(0, 50) + '...',
3639
+ answerText: answerText.substring(0, 50) + '...',
3640
+ isCorrect: isCorrect
3641
+ });
3642
+
3212
3643
  LRS.questionAnswered({
3213
- questionId: 'q' + (index + 1),
3214
- questionText: questionText.substring(0, 200),
3215
- answer: answerText.substring(0, 200),
3216
- result: isCorrect ? 'correct' : 'incorrect'
3644
+ questionId: questionId,
3645
+ questionNumber: index + 1,
3646
+ questionText: questionText.substring(0, 500),
3647
+ answer: answerText.substring(0, 500),
3648
+ correctAnswer: correctAnswerText.substring(0, 500),
3649
+ result: isCorrect ? 'correct' : 'incorrect',
3650
+ assessmentName: assessmentName,
3651
+ lessonName: lessonInfo.name,
3652
+ sectionName: lessonInfo.sectionName
3217
3653
  });
3218
3654
  });
3219
3655
  }
3220
3656
 
3221
3657
  LRS.assessmentEnded({
3222
3658
  assessmentId: extractAssessmentId(node),
3659
+ assessmentName: assessmentName,
3223
3660
  score: score,
3224
- questionCount: questions.length
3661
+ questionCount: questions.length,
3662
+ lessonName: lessonInfo.name,
3663
+ sectionName: lessonInfo.sectionName
3225
3664
  });
3226
3665
  }
3227
3666
 
@@ -3345,7 +3784,7 @@ function generateLrsBridgeCode(options) {
3345
3784
  // ========================================================================
3346
3785
 
3347
3786
  function init() {
3348
- log('Initializing LRS bridge v2.2.0...');
3787
+ log('Initializing LRS bridge v2.5.0...');
3349
3788
 
3350
3789
  // Extract course info early
3351
3790
  extractCourseInfo();