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