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