@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 +519 -80
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +519 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +519 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +519 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -720,48 +720,55 @@ function generateLrsBridgeCode(options) {
|
|
|
720
720
|
|
|
721
721
|
function setupBridge() {
|
|
722
722
|
try {
|
|
723
|
-
// 1. Try PlayerIntegration first (Xyleme native mode)
|
|
723
|
+
// 1. Try PlayerIntegration first (Xyleme native mode) - for SCORM APIs
|
|
724
724
|
var integrationResult = findPlayerIntegration(10);
|
|
725
725
|
if (integrationResult) {
|
|
726
726
|
LRS.integration = integrationResult.integration;
|
|
727
727
|
LRS.integrationLevel = integrationResult.level;
|
|
728
|
-
|
|
729
|
-
log('Using PlayerIntegration mode');
|
|
728
|
+
log('Found PlayerIntegration at level', integrationResult.level);
|
|
730
729
|
|
|
731
730
|
if (LRS.integration.Internal_SCORM_2004_API) {
|
|
732
731
|
window.API_1484_11 = LRS.integration.Internal_SCORM_2004_API;
|
|
733
732
|
LRS.scormApi = window.API_1484_11;
|
|
733
|
+
LRS.scormApiFound = true;
|
|
734
|
+
LRS.scormApiType = '2004';
|
|
734
735
|
}
|
|
735
736
|
if (LRS.integration.Internal_SCORM_12_API) {
|
|
736
737
|
window.API = LRS.integration.Internal_SCORM_12_API;
|
|
738
|
+
if (!LRS.scormApiFound) {
|
|
739
|
+
LRS.scormApi = window.API;
|
|
740
|
+
LRS.scormApiFound = true;
|
|
741
|
+
LRS.scormApiType = '1.2';
|
|
742
|
+
}
|
|
737
743
|
}
|
|
738
|
-
return
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// 2. Search for SCORM APIs
|
|
742
|
-
log('No PlayerIntegration found, searching for SCORM APIs...');
|
|
743
|
-
|
|
744
|
-
var api2004 = findAPIInFrameHierarchy('API_1484_11', 10);
|
|
745
|
-
if (api2004) {
|
|
746
|
-
LRS.scormApi = api2004.api;
|
|
747
|
-
LRS.scormApiLevel = api2004.level;
|
|
748
|
-
LRS.scormApiWindow = api2004.window;
|
|
749
|
-
LRS.scormApiFound = true;
|
|
750
|
-
LRS.scormApiType = '2004';
|
|
751
|
-
log('Found SCORM 2004 API at level ' + api2004.level);
|
|
744
|
+
// Don't return early - continue to detect LRS endpoint for direct xAPI
|
|
752
745
|
} else {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
746
|
+
// 2. Search for SCORM APIs directly
|
|
747
|
+
log('No PlayerIntegration found, searching for SCORM APIs...');
|
|
748
|
+
|
|
749
|
+
var api2004 = findAPIInFrameHierarchy('API_1484_11', 10);
|
|
750
|
+
if (api2004) {
|
|
751
|
+
LRS.scormApi = api2004.api;
|
|
752
|
+
LRS.scormApiLevel = api2004.level;
|
|
753
|
+
LRS.scormApiWindow = api2004.window;
|
|
758
754
|
LRS.scormApiFound = true;
|
|
759
|
-
LRS.scormApiType = '
|
|
760
|
-
log('Found SCORM
|
|
755
|
+
LRS.scormApiType = '2004';
|
|
756
|
+
log('Found SCORM 2004 API at level ' + api2004.level);
|
|
757
|
+
} else {
|
|
758
|
+
var api12 = findAPIInFrameHierarchy('API', 10);
|
|
759
|
+
if (api12) {
|
|
760
|
+
LRS.scormApi = api12.api;
|
|
761
|
+
LRS.scormApiLevel = api12.level;
|
|
762
|
+
LRS.scormApiWindow = api12.window;
|
|
763
|
+
LRS.scormApiFound = true;
|
|
764
|
+
LRS.scormApiType = '1.2';
|
|
765
|
+
log('Found SCORM 1.2 API at level ' + api12.level);
|
|
766
|
+
}
|
|
761
767
|
}
|
|
762
768
|
}
|
|
763
769
|
|
|
764
|
-
// 3.
|
|
770
|
+
// 3. ALWAYS try to determine LRS endpoint for direct xAPI statements
|
|
771
|
+
// This is critical for detailed tracking (media, interactions, quizzes)
|
|
765
772
|
// Priority: 1) Configured endpoint, 2) Auto-detected Bravais endpoint
|
|
766
773
|
if (LRS_ENDPOINT_CONFIG && LRS_ENDPOINT_CONFIG.length > 0) {
|
|
767
774
|
LRS_ENDPOINT = LRS_ENDPOINT_CONFIG;
|
|
@@ -777,9 +784,17 @@ function generateLrsBridgeCode(options) {
|
|
|
777
784
|
LRS.lrsEndpoint = LRS_ENDPOINT;
|
|
778
785
|
|
|
779
786
|
// 4. Determine mode based on what we found
|
|
787
|
+
// Prefer directLRS when available for detailed tracking
|
|
780
788
|
if (LRS_ENDPOINT && LRS_ENDPOINT.length > 0) {
|
|
781
789
|
LRS.mode = 'directLRS';
|
|
782
790
|
log('Using direct LRS mode with endpoint:', LRS_ENDPOINT);
|
|
791
|
+
if (LRS.integration) {
|
|
792
|
+
log('PlayerIntegration also available for SCORM state');
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
} else if (LRS.integration) {
|
|
796
|
+
LRS.mode = 'playerIntegration';
|
|
797
|
+
log('Using PlayerIntegration mode (no direct LRS endpoint)');
|
|
783
798
|
return true;
|
|
784
799
|
} else if (LRS.scormApiFound) {
|
|
785
800
|
LRS.mode = 'scormOnly';
|
|
@@ -1998,6 +2013,154 @@ function generateLrsBridgeCode(options) {
|
|
|
1998
2013
|
return info;
|
|
1999
2014
|
}
|
|
2000
2015
|
|
|
2016
|
+
// ========================================================================
|
|
2017
|
+
// 4b. LESSON/SECTION NAME EXTRACTION
|
|
2018
|
+
// ========================================================================
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Get the current lesson/section information from Rise DOM
|
|
2022
|
+
* Returns { id, name, lessonIndex, sectionName }
|
|
2023
|
+
*/
|
|
2024
|
+
function getCurrentLessonInfo() {
|
|
2025
|
+
var lessonInfo = {
|
|
2026
|
+
id: window.location.hash || '#/',
|
|
2027
|
+
name: null,
|
|
2028
|
+
lessonIndex: null,
|
|
2029
|
+
sectionName: null
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
try {
|
|
2033
|
+
// 1. Try to get from Rise's navigation sidebar
|
|
2034
|
+
// Rise shows active lesson with specific classes
|
|
2035
|
+
var activeLesson = document.querySelector(
|
|
2036
|
+
'.sidebar__link--current, ' +
|
|
2037
|
+
'.sidebar-lesson--active, ' +
|
|
2038
|
+
'.nav-sidebar__lesson--active, ' +
|
|
2039
|
+
'[class*="sidebar"] [class*="current"], ' +
|
|
2040
|
+
'[class*="sidebar"] [class*="active"], ' +
|
|
2041
|
+
'[aria-current="page"], ' +
|
|
2042
|
+
'[aria-current="true"]'
|
|
2043
|
+
);
|
|
2044
|
+
|
|
2045
|
+
if (activeLesson) {
|
|
2046
|
+
var lessonTitle = activeLesson.querySelector(
|
|
2047
|
+
'.sidebar__link-text, ' +
|
|
2048
|
+
'.lesson-title, ' +
|
|
2049
|
+
'.sidebar-lesson__title, ' +
|
|
2050
|
+
'[class*="title"], ' +
|
|
2051
|
+
'[class*="label"]'
|
|
2052
|
+
);
|
|
2053
|
+
if (lessonTitle) {
|
|
2054
|
+
lessonInfo.name = lessonTitle.textContent.trim();
|
|
2055
|
+
} else {
|
|
2056
|
+
lessonInfo.name = activeLesson.textContent.trim();
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Try to get the section/module name (parent group)
|
|
2060
|
+
var section = activeLesson.closest(
|
|
2061
|
+
'.sidebar__section, ' +
|
|
2062
|
+
'.sidebar-section, ' +
|
|
2063
|
+
'[class*="section"], ' +
|
|
2064
|
+
'[class*="module"]'
|
|
2065
|
+
);
|
|
2066
|
+
if (section) {
|
|
2067
|
+
var sectionTitle = section.querySelector(
|
|
2068
|
+
'.sidebar__section-title, ' +
|
|
2069
|
+
'.section-title, ' +
|
|
2070
|
+
'[class*="section-title"], ' +
|
|
2071
|
+
'[class*="module-title"], ' +
|
|
2072
|
+
'h2, h3'
|
|
2073
|
+
);
|
|
2074
|
+
if (sectionTitle) {
|
|
2075
|
+
lessonInfo.sectionName = sectionTitle.textContent.trim();
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// 2. Try Rise's header/breadcrumb
|
|
2081
|
+
if (!lessonInfo.name) {
|
|
2082
|
+
var headerTitle = document.querySelector(
|
|
2083
|
+
'.lesson-header__title, ' +
|
|
2084
|
+
'.lesson__title, ' +
|
|
2085
|
+
'.content-header__title, ' +
|
|
2086
|
+
'[class*="lesson"] [class*="header"] [class*="title"], ' +
|
|
2087
|
+
'.blocks-lesson-header__title'
|
|
2088
|
+
);
|
|
2089
|
+
if (headerTitle) {
|
|
2090
|
+
lessonInfo.name = headerTitle.textContent.trim();
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// 3. Try breadcrumbs
|
|
2095
|
+
if (!lessonInfo.name || !lessonInfo.sectionName) {
|
|
2096
|
+
var breadcrumbs = document.querySelectorAll(
|
|
2097
|
+
'.breadcrumb__item, ' +
|
|
2098
|
+
'.breadcrumbs li, ' +
|
|
2099
|
+
'[class*="breadcrumb"] span, ' +
|
|
2100
|
+
'[class*="breadcrumb"] a'
|
|
2101
|
+
);
|
|
2102
|
+
if (breadcrumbs.length >= 2) {
|
|
2103
|
+
// Usually: Course > Section > Lesson
|
|
2104
|
+
if (!lessonInfo.sectionName && breadcrumbs.length >= 2) {
|
|
2105
|
+
lessonInfo.sectionName = breadcrumbs[breadcrumbs.length - 2].textContent.trim();
|
|
2106
|
+
}
|
|
2107
|
+
if (!lessonInfo.name) {
|
|
2108
|
+
lessonInfo.name = breadcrumbs[breadcrumbs.length - 1].textContent.trim();
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// 4. Try Rise's internal data
|
|
2114
|
+
if (window.__RISE_COURSE_DATA__ && window.__RISE_COURSE_DATA__.lessons) {
|
|
2115
|
+
var lessonId = lessonInfo.id.replace('#/lessons/', '').replace('#/', '');
|
|
2116
|
+
var lessons = window.__RISE_COURSE_DATA__.lessons;
|
|
2117
|
+
for (var i = 0; i < lessons.length; i++) {
|
|
2118
|
+
if (lessons[i].id === lessonId || lessons[i].slug === lessonId) {
|
|
2119
|
+
lessonInfo.name = lessons[i].title || lessonInfo.name;
|
|
2120
|
+
lessonInfo.lessonIndex = i;
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// 5. Clean up - remove any "Home" or generic names if we're on a real lesson
|
|
2127
|
+
if (lessonInfo.name) {
|
|
2128
|
+
lessonInfo.name = lessonInfo.name.replace(/^\\d+\\.\\s*/, ''); // Remove leading numbers "1. "
|
|
2129
|
+
if (lessonInfo.name.length > 200) {
|
|
2130
|
+
lessonInfo.name = lessonInfo.name.substring(0, 200) + '...';
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (lessonInfo.sectionName) {
|
|
2135
|
+
lessonInfo.sectionName = lessonInfo.sectionName.replace(/^\\d+\\.\\s*/, '');
|
|
2136
|
+
if (lessonInfo.sectionName.length > 200) {
|
|
2137
|
+
lessonInfo.sectionName = lessonInfo.sectionName.substring(0, 200) + '...';
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
} catch (e) {
|
|
2142
|
+
warn('Error extracting lesson info:', e);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
log('Current lesson info:', lessonInfo);
|
|
2146
|
+
return lessonInfo;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Cache the current lesson info (updates on navigation)
|
|
2151
|
+
*/
|
|
2152
|
+
var cachedLessonInfo = null;
|
|
2153
|
+
var cachedLessonHash = null;
|
|
2154
|
+
|
|
2155
|
+
function getCachedLessonInfo() {
|
|
2156
|
+
var currentHash = window.location.hash;
|
|
2157
|
+
if (cachedLessonHash !== currentHash) {
|
|
2158
|
+
cachedLessonInfo = getCurrentLessonInfo();
|
|
2159
|
+
cachedLessonHash = currentHash;
|
|
2160
|
+
}
|
|
2161
|
+
return cachedLessonInfo || getCurrentLessonInfo();
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2001
2164
|
// ========================================================================
|
|
2002
2165
|
// 5. XAPI STATEMENT BUILDING
|
|
2003
2166
|
// ========================================================================
|
|
@@ -2342,14 +2505,20 @@ function generateLrsBridgeCode(options) {
|
|
|
2342
2505
|
LRS.eventLog.push({
|
|
2343
2506
|
statement: statement,
|
|
2344
2507
|
timestamp: new Date().toISOString(),
|
|
2345
|
-
mode: LRS.mode
|
|
2508
|
+
mode: LRS.mode,
|
|
2509
|
+
lrsEndpoint: LRS_ENDPOINT
|
|
2346
2510
|
});
|
|
2347
2511
|
|
|
2348
|
-
//
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2512
|
+
// ALWAYS prefer direct LRS when endpoint is available
|
|
2513
|
+
// This ensures all our detailed tracking (media, interactions, etc.) goes to the LRS
|
|
2514
|
+
// PlayerIntegration is used only for SCORM state, not for xAPI statements
|
|
2515
|
+
if (LRS_ENDPOINT && LRS_ENDPOINT.length > 0) {
|
|
2516
|
+
log('Sending via directLRS:', LRS_ENDPOINT);
|
|
2352
2517
|
return sendViaDirectLRS(statement);
|
|
2518
|
+
} else if (LRS.mode === 'playerIntegration' && LRS.integration) {
|
|
2519
|
+
// Fallback to PlayerIntegration only if no direct LRS endpoint
|
|
2520
|
+
log('No LRS endpoint, falling back to PlayerIntegration');
|
|
2521
|
+
return sendViaPlayerIntegration(statement);
|
|
2353
2522
|
} else {
|
|
2354
2523
|
// SCORM-only or offline mode - just log
|
|
2355
2524
|
log('Statement logged (no LRS):', statement.verb.display['en-US']);
|
|
@@ -2488,13 +2657,16 @@ function generateLrsBridgeCode(options) {
|
|
|
2488
2657
|
LRS.contentOpened = function(data) {
|
|
2489
2658
|
if (!TRACK_NAVIGATION) return;
|
|
2490
2659
|
|
|
2660
|
+
// Get current lesson info from DOM (includes name)
|
|
2661
|
+
var lessonInfo = getCurrentLessonInfo();
|
|
2662
|
+
|
|
2491
2663
|
var result = null;
|
|
2492
2664
|
if (data.duration) {
|
|
2493
2665
|
result = { duration: data.duration };
|
|
2494
2666
|
}
|
|
2495
2667
|
|
|
2496
2668
|
// Extract clean page/lesson ID from Rise hash paths
|
|
2497
|
-
var lessonId = data.pageGuid || data.lessonId || '';
|
|
2669
|
+
var lessonId = data.pageGuid || data.lessonId || lessonInfo.id || '';
|
|
2498
2670
|
if (lessonId.indexOf('#/lessons/') === 0) {
|
|
2499
2671
|
lessonId = lessonId.substring(10); // Remove '#/lessons/'
|
|
2500
2672
|
} else if (lessonId.indexOf('#/') === 0) {
|
|
@@ -2505,7 +2677,9 @@ function generateLrsBridgeCode(options) {
|
|
|
2505
2677
|
var additionalContext = {
|
|
2506
2678
|
extensions: {
|
|
2507
2679
|
pageId: lessonId || 'home',
|
|
2508
|
-
pageTitle: data.title || 'Page',
|
|
2680
|
+
pageTitle: data.title || lessonInfo.name || 'Page',
|
|
2681
|
+
lessonName: lessonInfo.name,
|
|
2682
|
+
sectionName: lessonInfo.sectionName,
|
|
2509
2683
|
pageUrl: data.url || window.location.href
|
|
2510
2684
|
}
|
|
2511
2685
|
};
|
|
@@ -2515,7 +2689,12 @@ function generateLrsBridgeCode(options) {
|
|
|
2515
2689
|
var statement = buildStatement(
|
|
2516
2690
|
'experienced',
|
|
2517
2691
|
'http://xyleme.com/bravais/activities/document',
|
|
2518
|
-
{
|
|
2692
|
+
{
|
|
2693
|
+
event: 'page_viewed',
|
|
2694
|
+
pageId: lessonId,
|
|
2695
|
+
lessonName: lessonInfo.name,
|
|
2696
|
+
sectionName: lessonInfo.sectionName
|
|
2697
|
+
},
|
|
2519
2698
|
result,
|
|
2520
2699
|
additionalContext
|
|
2521
2700
|
);
|
|
@@ -2526,6 +2705,9 @@ function generateLrsBridgeCode(options) {
|
|
|
2526
2705
|
LRS.mediaPlayed = function(data) {
|
|
2527
2706
|
if (!TRACK_MEDIA) return;
|
|
2528
2707
|
|
|
2708
|
+
// Get current lesson context
|
|
2709
|
+
var lessonInfo = getCachedLessonInfo();
|
|
2710
|
+
|
|
2529
2711
|
var mediaType = data.type === 'audio' ? ACTIVITY_TYPES.audio : ACTIVITY_TYPES.video;
|
|
2530
2712
|
var verbKey = data.action === 'play' ? 'played' :
|
|
2531
2713
|
data.action === 'pause' ? 'paused' :
|
|
@@ -2536,7 +2718,11 @@ function generateLrsBridgeCode(options) {
|
|
|
2536
2718
|
mediaSrc: data.src || 'media-' + Date.now(),
|
|
2537
2719
|
mediaName: data.name || (data.type || 'Media'),
|
|
2538
2720
|
mediaType: data.type || 'video',
|
|
2539
|
-
'https://w3id.org/xapi/video/extensions/length': data.duration || 0
|
|
2721
|
+
'https://w3id.org/xapi/video/extensions/length': data.duration || 0,
|
|
2722
|
+
// Include lesson/section context
|
|
2723
|
+
lessonId: lessonInfo.id,
|
|
2724
|
+
lessonName: lessonInfo.name,
|
|
2725
|
+
sectionName: lessonInfo.sectionName
|
|
2540
2726
|
};
|
|
2541
2727
|
|
|
2542
2728
|
var result = {
|
|
@@ -2559,12 +2745,21 @@ function generateLrsBridgeCode(options) {
|
|
|
2559
2745
|
LRS.assessmentStarted = function(data) {
|
|
2560
2746
|
if (!TRACK_QUIZZES) return;
|
|
2561
2747
|
|
|
2748
|
+
// Get lesson context
|
|
2749
|
+
var lessonInfo = getCachedLessonInfo();
|
|
2750
|
+
|
|
2562
2751
|
// Build assessment activity object for Xyleme format
|
|
2563
2752
|
var assessmentObject = buildAssessmentActivityObject({
|
|
2564
2753
|
assessmentGuid: data.assessmentGuid || data.id,
|
|
2565
|
-
title: data.title || 'Assessment'
|
|
2754
|
+
title: data.title || data.assessmentName || 'Assessment'
|
|
2566
2755
|
});
|
|
2567
2756
|
|
|
2757
|
+
// Add lesson context to extensions
|
|
2758
|
+
assessmentObject.definition = assessmentObject.definition || {};
|
|
2759
|
+
assessmentObject.definition.extensions = assessmentObject.definition.extensions || {};
|
|
2760
|
+
assessmentObject.definition.extensions.lessonName = lessonInfo.name;
|
|
2761
|
+
assessmentObject.definition.extensions.sectionName = lessonInfo.sectionName;
|
|
2762
|
+
|
|
2568
2763
|
var statement = buildStatementXyleme('attempted', assessmentObject);
|
|
2569
2764
|
sendStatement(statement);
|
|
2570
2765
|
};
|
|
@@ -2573,13 +2768,31 @@ function generateLrsBridgeCode(options) {
|
|
|
2573
2768
|
LRS.assessmentEnded = function(data) {
|
|
2574
2769
|
if (!TRACK_QUIZZES) return;
|
|
2575
2770
|
|
|
2771
|
+
// Get lesson context (may be passed in from extractQuizResult)
|
|
2772
|
+
var lessonInfo = data.lessonName ? { name: data.lessonName, sectionName: data.sectionName } : getCachedLessonInfo();
|
|
2773
|
+
|
|
2576
2774
|
// Build assessment activity object for Xyleme format
|
|
2577
2775
|
var assessmentObject = buildAssessmentActivityObject({
|
|
2578
|
-
assessmentGuid: data.assessmentGuid || data.id,
|
|
2579
|
-
title: data.title || 'Assessment'
|
|
2776
|
+
assessmentGuid: data.assessmentGuid || data.assessmentId || data.id,
|
|
2777
|
+
title: data.title || data.assessmentName || 'Assessment'
|
|
2580
2778
|
});
|
|
2581
2779
|
|
|
2582
|
-
|
|
2780
|
+
// Add lesson context to extensions
|
|
2781
|
+
assessmentObject.definition = assessmentObject.definition || {};
|
|
2782
|
+
assessmentObject.definition.extensions = assessmentObject.definition.extensions || {};
|
|
2783
|
+
assessmentObject.definition.extensions.lessonName = lessonInfo.name;
|
|
2784
|
+
assessmentObject.definition.extensions.sectionName = lessonInfo.sectionName;
|
|
2785
|
+
assessmentObject.definition.extensions.assessmentName = data.assessmentName || data.title;
|
|
2786
|
+
|
|
2787
|
+
var result = {
|
|
2788
|
+
completion: true,
|
|
2789
|
+
extensions: {
|
|
2790
|
+
'https://patch-adams.io/xapi/extensions/assessmentName': data.assessmentName || data.title || null,
|
|
2791
|
+
'https://patch-adams.io/xapi/extensions/lessonName': lessonInfo.name || null,
|
|
2792
|
+
'https://patch-adams.io/xapi/extensions/sectionName': lessonInfo.sectionName || null,
|
|
2793
|
+
'https://patch-adams.io/xapi/extensions/questionCount': data.questionCount || null
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2583
2796
|
|
|
2584
2797
|
if (typeof data.score === 'number') {
|
|
2585
2798
|
result.score = {
|
|
@@ -2608,14 +2821,27 @@ function generateLrsBridgeCode(options) {
|
|
|
2608
2821
|
LRS.questionAnswered = function(data) {
|
|
2609
2822
|
if (!TRACK_QUIZZES) return;
|
|
2610
2823
|
|
|
2824
|
+
// Get lesson context if not provided
|
|
2825
|
+
var lessonInfo = data.lessonName ? data : getCachedLessonInfo();
|
|
2826
|
+
|
|
2611
2827
|
// Build question activity object for Xyleme format
|
|
2612
2828
|
var questionObject = buildQuestionActivityObject({
|
|
2613
2829
|
questionGuid: data.questionGuid || data.questionId,
|
|
2614
2830
|
text: data.questionText || 'Question'
|
|
2615
2831
|
});
|
|
2616
2832
|
|
|
2833
|
+
// Build comprehensive result with human-readable data
|
|
2617
2834
|
var result = {
|
|
2618
|
-
response: data.answer || data.response || ''
|
|
2835
|
+
response: data.answer || data.response || '',
|
|
2836
|
+
extensions: {
|
|
2837
|
+
'https://patch-adams.io/xapi/extensions/questionNumber': data.questionNumber || null,
|
|
2838
|
+
'https://patch-adams.io/xapi/extensions/questionText': data.questionText || null,
|
|
2839
|
+
'https://patch-adams.io/xapi/extensions/answerText': data.answer || data.response || null,
|
|
2840
|
+
'https://patch-adams.io/xapi/extensions/correctAnswer': data.correctAnswer || null,
|
|
2841
|
+
'https://patch-adams.io/xapi/extensions/assessmentName': data.assessmentName || null,
|
|
2842
|
+
'https://patch-adams.io/xapi/extensions/lessonName': lessonInfo.lessonName || lessonInfo.name || null,
|
|
2843
|
+
'https://patch-adams.io/xapi/extensions/sectionName': lessonInfo.sectionName || null
|
|
2844
|
+
}
|
|
2619
2845
|
};
|
|
2620
2846
|
|
|
2621
2847
|
if (data.result === 'correct' || data.correct === true) {
|
|
@@ -2647,6 +2873,9 @@ function generateLrsBridgeCode(options) {
|
|
|
2647
2873
|
LRS.interacted = function(data) {
|
|
2648
2874
|
if (!TRACK_INTERACTIONS) return;
|
|
2649
2875
|
|
|
2876
|
+
// Get current lesson context
|
|
2877
|
+
var lessonInfo = getCachedLessonInfo();
|
|
2878
|
+
|
|
2650
2879
|
// Activity type is interaction, details include interaction ID and type
|
|
2651
2880
|
var statement = buildStatement(
|
|
2652
2881
|
'interacted',
|
|
@@ -2654,7 +2883,11 @@ function generateLrsBridgeCode(options) {
|
|
|
2654
2883
|
{
|
|
2655
2884
|
interactionId: data.id || 'interaction-' + Date.now(),
|
|
2656
2885
|
interactionName: data.name || data.type || 'Interaction',
|
|
2657
|
-
interactionType: data.interactionType || data.type || 'other'
|
|
2886
|
+
interactionType: data.interactionType || data.type || 'other',
|
|
2887
|
+
// Include lesson/section context
|
|
2888
|
+
lessonId: lessonInfo.id,
|
|
2889
|
+
lessonName: lessonInfo.name,
|
|
2890
|
+
sectionName: lessonInfo.sectionName
|
|
2658
2891
|
}
|
|
2659
2892
|
);
|
|
2660
2893
|
sendStatement(statement);
|
|
@@ -2735,49 +2968,146 @@ function generateLrsBridgeCode(options) {
|
|
|
2735
2968
|
function setupMediaInterceptors() {
|
|
2736
2969
|
if (!TRACK_MEDIA) return;
|
|
2737
2970
|
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2971
|
+
// Track which elements we've already attached listeners to
|
|
2972
|
+
var trackedMedia = new WeakSet();
|
|
2973
|
+
|
|
2974
|
+
// Helper to get media name/title from element or surrounding context
|
|
2975
|
+
function getMediaName(el) {
|
|
2976
|
+
// Check element attributes
|
|
2977
|
+
var name = el.title || el.getAttribute('aria-label') || el.getAttribute('data-name');
|
|
2978
|
+
if (name) return name;
|
|
2979
|
+
|
|
2980
|
+
// Check parent Rise video block for title
|
|
2981
|
+
var videoBlock = findClosest(el, '[data-block-type="video"], .blocks-video, .video-block');
|
|
2982
|
+
if (videoBlock) {
|
|
2983
|
+
var titleEl = videoBlock.querySelector('.video-title, .block-title, [class*="title"]');
|
|
2984
|
+
if (titleEl) return titleEl.textContent.trim();
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// Use src filename as fallback
|
|
2988
|
+
var src = el.src || el.currentSrc || '';
|
|
2989
|
+
if (src) {
|
|
2990
|
+
var filename = src.split('/').pop().split('?')[0];
|
|
2991
|
+
if (filename && filename.length < 100) return filename;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
return el.tagName.toLowerCase();
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// Attach media event listeners to a single element
|
|
2998
|
+
function attachMediaListeners(el) {
|
|
2999
|
+
if (trackedMedia.has(el)) return;
|
|
3000
|
+
trackedMedia.add(el);
|
|
3001
|
+
|
|
3002
|
+
var mediaType = el.tagName.toLowerCase();
|
|
3003
|
+
log('Attaching media listeners to:', mediaType, el.src || el.currentSrc || 'no-src');
|
|
3004
|
+
|
|
3005
|
+
el.addEventListener('play', function() {
|
|
3006
|
+
log('Media play event captured:', mediaType);
|
|
2741
3007
|
LRS.mediaPlayed({
|
|
2742
|
-
type:
|
|
2743
|
-
src:
|
|
2744
|
-
name:
|
|
2745
|
-
currentTime:
|
|
2746
|
-
duration:
|
|
3008
|
+
type: mediaType,
|
|
3009
|
+
src: el.src || el.currentSrc || '',
|
|
3010
|
+
name: getMediaName(el),
|
|
3011
|
+
currentTime: el.currentTime || 0,
|
|
3012
|
+
duration: el.duration || 0,
|
|
2747
3013
|
action: 'play'
|
|
2748
3014
|
});
|
|
2749
|
-
}
|
|
2750
|
-
}, true);
|
|
3015
|
+
});
|
|
2751
3016
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
3017
|
+
el.addEventListener('pause', function() {
|
|
3018
|
+
// Ignore pause events that fire right before ended
|
|
3019
|
+
if (el.ended) return;
|
|
3020
|
+
log('Media pause event captured:', mediaType);
|
|
2755
3021
|
LRS.mediaPlayed({
|
|
2756
|
-
type:
|
|
2757
|
-
src:
|
|
2758
|
-
name:
|
|
2759
|
-
currentTime:
|
|
2760
|
-
duration:
|
|
3022
|
+
type: mediaType,
|
|
3023
|
+
src: el.src || el.currentSrc || '',
|
|
3024
|
+
name: getMediaName(el),
|
|
3025
|
+
currentTime: el.currentTime || 0,
|
|
3026
|
+
duration: el.duration || 0,
|
|
2761
3027
|
action: 'pause'
|
|
2762
3028
|
});
|
|
2763
|
-
}
|
|
2764
|
-
}, true);
|
|
3029
|
+
});
|
|
2765
3030
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
|
|
3031
|
+
el.addEventListener('ended', function() {
|
|
3032
|
+
log('Media ended event captured:', mediaType);
|
|
2769
3033
|
LRS.mediaPlayed({
|
|
2770
|
-
type:
|
|
2771
|
-
src:
|
|
2772
|
-
name:
|
|
2773
|
-
currentTime:
|
|
2774
|
-
duration:
|
|
3034
|
+
type: mediaType,
|
|
3035
|
+
src: el.src || el.currentSrc || '',
|
|
3036
|
+
name: getMediaName(el),
|
|
3037
|
+
currentTime: el.duration || 0,
|
|
3038
|
+
duration: el.duration || 0,
|
|
2775
3039
|
action: 'completed'
|
|
2776
3040
|
});
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// Scan for and attach listeners to all video/audio elements
|
|
3045
|
+
function scanForMedia(root) {
|
|
3046
|
+
var elements = (root || document).querySelectorAll('video, audio');
|
|
3047
|
+
log('Scanning for media elements, found:', elements.length);
|
|
3048
|
+
for (var i = 0; i < elements.length; i++) {
|
|
3049
|
+
attachMediaListeners(elements[i]);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// Initial scan
|
|
3054
|
+
scanForMedia();
|
|
3055
|
+
|
|
3056
|
+
// Document-level capture for any we might miss
|
|
3057
|
+
document.addEventListener('play', function(e) {
|
|
3058
|
+
var target = e.target;
|
|
3059
|
+
if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
|
|
3060
|
+
// Attach listeners if not already tracked
|
|
3061
|
+
if (!trackedMedia.has(target)) {
|
|
3062
|
+
log('Captured play from untracked media, attaching listeners');
|
|
3063
|
+
attachMediaListeners(target);
|
|
3064
|
+
}
|
|
2777
3065
|
}
|
|
2778
3066
|
}, true);
|
|
2779
3067
|
|
|
2780
|
-
|
|
3068
|
+
// MutationObserver to detect dynamically added video/audio elements
|
|
3069
|
+
var mediaObserver = new MutationObserver(function(mutations) {
|
|
3070
|
+
var needsScan = false;
|
|
3071
|
+
mutations.forEach(function(mutation) {
|
|
3072
|
+
mutation.addedNodes.forEach(function(node) {
|
|
3073
|
+
if (node.nodeType === 1) {
|
|
3074
|
+
if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') {
|
|
3075
|
+
attachMediaListeners(node);
|
|
3076
|
+
} else if (node.querySelectorAll) {
|
|
3077
|
+
// Check for nested video/audio
|
|
3078
|
+
var nested = node.querySelectorAll('video, audio');
|
|
3079
|
+
if (nested.length > 0) {
|
|
3080
|
+
needsScan = true;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
});
|
|
3086
|
+
if (needsScan) {
|
|
3087
|
+
scanForMedia();
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
|
|
3091
|
+
// Start observing once DOM is ready
|
|
3092
|
+
function startMediaObserver() {
|
|
3093
|
+
if (document.body) {
|
|
3094
|
+
mediaObserver.observe(document.body, {
|
|
3095
|
+
childList: true,
|
|
3096
|
+
subtree: true
|
|
3097
|
+
});
|
|
3098
|
+
log('Media mutation observer started');
|
|
3099
|
+
} else {
|
|
3100
|
+
setTimeout(startMediaObserver, 100);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
startMediaObserver();
|
|
3104
|
+
|
|
3105
|
+
// Periodic rescan for Rise lazy-loaded content
|
|
3106
|
+
setInterval(function() {
|
|
3107
|
+
scanForMedia();
|
|
3108
|
+
}, 3000);
|
|
3109
|
+
|
|
3110
|
+
log('Media interceptors set up (enhanced)');
|
|
2781
3111
|
}
|
|
2782
3112
|
|
|
2783
3113
|
function setupNavigationInterceptors() {
|
|
@@ -2858,43 +3188,152 @@ function generateLrsBridgeCode(options) {
|
|
|
2858
3188
|
'quiz-' + Date.now();
|
|
2859
3189
|
}
|
|
2860
3190
|
|
|
3191
|
+
/**
|
|
3192
|
+
* Extract quiz/assessment name from the DOM
|
|
3193
|
+
*/
|
|
3194
|
+
function extractAssessmentName(node) {
|
|
3195
|
+
// Try various selectors for assessment/quiz titles
|
|
3196
|
+
var titleEl = node.querySelector(
|
|
3197
|
+
'.quiz-title, .assessment-title, .knowledge-check-title, ' +
|
|
3198
|
+
'.blocks-quiz__title, .block-title, ' +
|
|
3199
|
+
'[class*="quiz"] [class*="title"], ' +
|
|
3200
|
+
'[class*="assessment"] [class*="title"], ' +
|
|
3201
|
+
'h2, h3'
|
|
3202
|
+
);
|
|
3203
|
+
if (titleEl) {
|
|
3204
|
+
return titleEl.textContent.trim().substring(0, 200);
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// Try to get from parent block
|
|
3208
|
+
var parentBlock = node.closest('[data-block-type]');
|
|
3209
|
+
if (parentBlock) {
|
|
3210
|
+
var blockTitle = parentBlock.querySelector('.block-title, h2, h3');
|
|
3211
|
+
if (blockTitle) {
|
|
3212
|
+
return blockTitle.textContent.trim().substring(0, 200);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
// Get current lesson name as context
|
|
3217
|
+
var lessonInfo = getCachedLessonInfo();
|
|
3218
|
+
if (lessonInfo.name) {
|
|
3219
|
+
return 'Quiz in ' + lessonInfo.name;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
return 'Knowledge Check';
|
|
3223
|
+
}
|
|
3224
|
+
|
|
2861
3225
|
function extractQuizResult(node) {
|
|
2862
3226
|
var scoreEl = node.querySelector('.score-value, .quiz-score, .score-percentage, [class*="score"]');
|
|
2863
3227
|
var score = scoreEl ? parseFloat(scoreEl.textContent.replace(/[^0-9.]/g, '')) : null;
|
|
2864
3228
|
|
|
2865
|
-
|
|
3229
|
+
// Get assessment name and lesson context
|
|
3230
|
+
var assessmentName = extractAssessmentName(node);
|
|
3231
|
+
var lessonInfo = getCachedLessonInfo();
|
|
3232
|
+
|
|
3233
|
+
// Find all question containers - Rise uses various structures
|
|
3234
|
+
var questions = node.querySelectorAll(
|
|
3235
|
+
'.question-result, .question-feedback, .question-item, ' +
|
|
3236
|
+
'.blocks-quiz__question, .quiz-question, ' +
|
|
3237
|
+
'[class*="question-"][class*="result"], ' +
|
|
3238
|
+
'[class*="question-"][class*="feedback"], ' +
|
|
3239
|
+
'[data-question-id]'
|
|
3240
|
+
);
|
|
3241
|
+
|
|
3242
|
+
log('Found', questions.length, 'question elements in quiz result');
|
|
2866
3243
|
|
|
2867
3244
|
if (questions.length > 0) {
|
|
2868
3245
|
questions.forEach(function(q, index) {
|
|
3246
|
+
// Determine if correct - check multiple indicators
|
|
2869
3247
|
var isCorrect = q.classList.contains('correct') ||
|
|
2870
|
-
q.
|
|
2871
|
-
q.
|
|
3248
|
+
q.classList.contains('is-correct') ||
|
|
3249
|
+
q.querySelector('.correct, .is-correct, [class*="correct"]') !== null ||
|
|
3250
|
+
q.getAttribute('data-correct') === 'true' ||
|
|
3251
|
+
q.getAttribute('data-result') === 'correct';
|
|
2872
3252
|
|
|
3253
|
+
// Get the question text - try multiple selectors
|
|
2873
3254
|
var questionText = '';
|
|
2874
|
-
var questionEl = q.querySelector(
|
|
3255
|
+
var questionEl = q.querySelector(
|
|
3256
|
+
'.question-text, .question-stem, .question-title, ' +
|
|
3257
|
+
'.blocks-quiz__question-text, .quiz-question__text, ' +
|
|
3258
|
+
'[class*="question-text"], [class*="question-stem"], ' +
|
|
3259
|
+
'[class*="prompt"], p:first-of-type'
|
|
3260
|
+
);
|
|
2875
3261
|
if (questionEl) {
|
|
2876
3262
|
questionText = questionEl.textContent.trim();
|
|
2877
3263
|
}
|
|
2878
3264
|
|
|
3265
|
+
// Get the learner's selected answer text
|
|
2879
3266
|
var answerText = '';
|
|
2880
|
-
var answerEl = q.querySelector(
|
|
3267
|
+
var answerEl = q.querySelector(
|
|
3268
|
+
'.selected-answer, .learner-response, .answer-text, ' +
|
|
3269
|
+
'.user-answer, .chosen-answer, .response-text, ' +
|
|
3270
|
+
'.blocks-quiz__response, .quiz-question__response, ' +
|
|
3271
|
+
'[class*="selected"], [class*="chosen"], [class*="response"], ' +
|
|
3272
|
+
'[class*="user-answer"]'
|
|
3273
|
+
);
|
|
2881
3274
|
if (answerEl) {
|
|
2882
3275
|
answerText = answerEl.textContent.trim();
|
|
2883
3276
|
}
|
|
2884
3277
|
|
|
3278
|
+
// If no specific answer element, try to find selected radio/checkbox label
|
|
3279
|
+
if (!answerText) {
|
|
3280
|
+
var selectedInput = q.querySelector('input:checked, [aria-checked="true"]');
|
|
3281
|
+
if (selectedInput) {
|
|
3282
|
+
var label = q.querySelector('label[for="' + selectedInput.id + '"]');
|
|
3283
|
+
if (label) {
|
|
3284
|
+
answerText = label.textContent.trim();
|
|
3285
|
+
} else {
|
|
3286
|
+
var parentLabel = selectedInput.closest('label');
|
|
3287
|
+
if (parentLabel) {
|
|
3288
|
+
answerText = parentLabel.textContent.trim();
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
// Get question ID if available
|
|
3295
|
+
var questionId = q.getAttribute('data-question-id') ||
|
|
3296
|
+
q.getAttribute('data-block-id') ||
|
|
3297
|
+
q.getAttribute('id') ||
|
|
3298
|
+
'q' + (index + 1);
|
|
3299
|
+
|
|
3300
|
+
// Get correct answer text if available (for reporting)
|
|
3301
|
+
var correctAnswerText = '';
|
|
3302
|
+
var correctEl = q.querySelector(
|
|
3303
|
+
'.correct-answer, .right-answer, ' +
|
|
3304
|
+
'[class*="correct-answer"], [class*="right-answer"]'
|
|
3305
|
+
);
|
|
3306
|
+
if (correctEl) {
|
|
3307
|
+
correctAnswerText = correctEl.textContent.trim();
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
log('Question', index + 1, ':', {
|
|
3311
|
+
questionText: questionText.substring(0, 50) + '...',
|
|
3312
|
+
answerText: answerText.substring(0, 50) + '...',
|
|
3313
|
+
isCorrect: isCorrect
|
|
3314
|
+
});
|
|
3315
|
+
|
|
2885
3316
|
LRS.questionAnswered({
|
|
2886
|
-
questionId:
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3317
|
+
questionId: questionId,
|
|
3318
|
+
questionNumber: index + 1,
|
|
3319
|
+
questionText: questionText.substring(0, 500),
|
|
3320
|
+
answer: answerText.substring(0, 500),
|
|
3321
|
+
correctAnswer: correctAnswerText.substring(0, 500),
|
|
3322
|
+
result: isCorrect ? 'correct' : 'incorrect',
|
|
3323
|
+
assessmentName: assessmentName,
|
|
3324
|
+
lessonName: lessonInfo.name,
|
|
3325
|
+
sectionName: lessonInfo.sectionName
|
|
2890
3326
|
});
|
|
2891
3327
|
});
|
|
2892
3328
|
}
|
|
2893
3329
|
|
|
2894
3330
|
LRS.assessmentEnded({
|
|
2895
3331
|
assessmentId: extractAssessmentId(node),
|
|
3332
|
+
assessmentName: assessmentName,
|
|
2896
3333
|
score: score,
|
|
2897
|
-
questionCount: questions.length
|
|
3334
|
+
questionCount: questions.length,
|
|
3335
|
+
lessonName: lessonInfo.name,
|
|
3336
|
+
sectionName: lessonInfo.sectionName
|
|
2898
3337
|
});
|
|
2899
3338
|
}
|
|
2900
3339
|
|
|
@@ -3018,7 +3457,7 @@ function generateLrsBridgeCode(options) {
|
|
|
3018
3457
|
// ========================================================================
|
|
3019
3458
|
|
|
3020
3459
|
function init() {
|
|
3021
|
-
log('Initializing LRS bridge v2.
|
|
3460
|
+
log('Initializing LRS bridge v2.5.0...');
|
|
3022
3461
|
|
|
3023
3462
|
// Extract course info early
|
|
3024
3463
|
extractCourseInfo();
|