coursecode 0.1.0
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/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- package/template/vite.config.js +339 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file scorm-driver-base.js
|
|
3
|
+
* @description Base class for pipwerks-based SCORM drivers (2004, 1.2).
|
|
4
|
+
* Consolidates shared pipwerks initialization, connection state, and utilities.
|
|
5
|
+
*
|
|
6
|
+
* Subclasses must implement:
|
|
7
|
+
* - getFormat()
|
|
8
|
+
* - getCapabilities()
|
|
9
|
+
* - initialize() (calls _initPipwerks)
|
|
10
|
+
* - terminate()
|
|
11
|
+
* - commit()
|
|
12
|
+
* - ping()
|
|
13
|
+
* - _populateCache()
|
|
14
|
+
* - All semantic reads/writes
|
|
15
|
+
* - getSuspendData() / setSuspendData()
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { logger } from '../utilities/logger.js';
|
|
19
|
+
|
|
20
|
+
export class ScormDriverBase {
|
|
21
|
+
constructor() {
|
|
22
|
+
this._isConnected = false;
|
|
23
|
+
this._isTerminated = false;
|
|
24
|
+
this._scorm = null; // Initialized lazily via _initPipwerks()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =========================================================================
|
|
28
|
+
// Interface: Connection State
|
|
29
|
+
// =========================================================================
|
|
30
|
+
|
|
31
|
+
isConnected() {
|
|
32
|
+
return this._isConnected;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isTerminated() {
|
|
36
|
+
return this._isTerminated;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =========================================================================
|
|
40
|
+
// Shared Initialization
|
|
41
|
+
// =========================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dynamically imports pipwerks and configures it for the specified SCORM version.
|
|
45
|
+
* Call this from the subclass initialize() method.
|
|
46
|
+
* @param {'2004'|'1.2'} version - SCORM version to force
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
async _initPipwerks(version) {
|
|
50
|
+
const pipwerksModule = await import('../vendor/pipwerks.js');
|
|
51
|
+
const pipwerks = pipwerksModule.default;
|
|
52
|
+
|
|
53
|
+
this._scorm = pipwerks.SCORM;
|
|
54
|
+
this._scorm.version = version;
|
|
55
|
+
|
|
56
|
+
// Disable pipwerks auto-handling — we manage status ourselves
|
|
57
|
+
this._scorm.handleCompletionStatus = false;
|
|
58
|
+
this._scorm.handleExitMode = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// =========================================================================
|
|
62
|
+
// Shared Utilities
|
|
63
|
+
// =========================================================================
|
|
64
|
+
|
|
65
|
+
_ensureInitialized() {
|
|
66
|
+
if (!this._isConnected) {
|
|
67
|
+
throw new Error(`${this.getFormat()} not initialized. Call initialize() first.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Terminates the SCORM connection via pipwerks quit().
|
|
73
|
+
* Used directly by SCORM 1.2; overridden by SCORM 2004 for error details.
|
|
74
|
+
*/
|
|
75
|
+
async terminate() {
|
|
76
|
+
if (!this._isConnected || this._isTerminated) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const success = this._scorm.quit();
|
|
81
|
+
this._isTerminated = success;
|
|
82
|
+
|
|
83
|
+
if (!success) {
|
|
84
|
+
throw new Error(`[${this.constructor.name}] SCORM termination failed`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Commits buffered writes via pipwerks save().
|
|
92
|
+
* SCORM 2004 overrides this for recovery mode support.
|
|
93
|
+
*/
|
|
94
|
+
async commit() {
|
|
95
|
+
this._ensureInitialized();
|
|
96
|
+
|
|
97
|
+
if (this._isTerminated) {
|
|
98
|
+
if (import.meta.env.DEV) {
|
|
99
|
+
logger.warn(`[${this.constructor.name}] Ignoring commit() - SCORM session already terminated`);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const success = this._scorm.save();
|
|
105
|
+
|
|
106
|
+
if (!success) {
|
|
107
|
+
throw new Error(`[${this.constructor.name}] SCORM commit failed`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file engagement-manager.js
|
|
3
|
+
* @description Tracks user engagement with slide content to gate navigation.
|
|
4
|
+
*
|
|
5
|
+
* Stateless manager — all state is stored in StateManager's 'engagement' domain.
|
|
6
|
+
* Pure progress/formatting functions are in engagement-progress.js.
|
|
7
|
+
* Requirement evaluation strategies are in requirement-strategies.js.
|
|
8
|
+
* Component registration and tracking methods are in engagement-trackers.js.
|
|
9
|
+
*
|
|
10
|
+
* @version 2.2.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { eventBus } from '../core/event-bus.js';
|
|
14
|
+
import { logger } from '../utilities/logger.js';
|
|
15
|
+
import stateManager from '../state/index.js';
|
|
16
|
+
import * as NavigationState from '../navigation/NavigationState.js';
|
|
17
|
+
import strategies, { validTypes, getTrackedFieldDefaults } from './requirement-strategies.js';
|
|
18
|
+
import {
|
|
19
|
+
calculateProgress,
|
|
20
|
+
formatTimeHuman,
|
|
21
|
+
formatInteractionId,
|
|
22
|
+
mergeWithDefaults,
|
|
23
|
+
stripDefaultValues
|
|
24
|
+
} from './engagement-progress.js';
|
|
25
|
+
import * as trackers from './engagement-trackers.js';
|
|
26
|
+
|
|
27
|
+
class EngagementManager {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.domain = 'engagement';
|
|
30
|
+
this.isInitialized = false;
|
|
31
|
+
this.courseConfig = null;
|
|
32
|
+
this._timeTrackingIntervals = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Subscribes to events needed for dynamic tracking.
|
|
37
|
+
* @param {object} courseConfig - The course configuration object
|
|
38
|
+
*/
|
|
39
|
+
initialize(courseConfig) {
|
|
40
|
+
if (this.isInitialized) return;
|
|
41
|
+
|
|
42
|
+
if (!courseConfig || !courseConfig.structure) {
|
|
43
|
+
throw new Error('[EngagementManager] courseConfig with structure is REQUIRED');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.courseConfig = courseConfig;
|
|
47
|
+
|
|
48
|
+
eventBus.on('flag:updated', this._handleFlagUpdate.bind(this));
|
|
49
|
+
eventBus.on('flag:removed', this._handleFlagUpdate.bind(this));
|
|
50
|
+
|
|
51
|
+
eventBus.on('interaction:recorded', (interaction) => {
|
|
52
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
53
|
+
if (currentSlideId) {
|
|
54
|
+
const isCorrect = interaction.result === 'correct';
|
|
55
|
+
this.trackInteraction(currentSlideId, interaction.id, true, isCorrect);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.isInitialized = true;
|
|
60
|
+
logger.debug('[EngagementManager] Initialized (refactored v2.2)');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =========================================================================
|
|
64
|
+
// Slide Lifecycle
|
|
65
|
+
// =========================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets engagement requirements for a slide from course config.
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
_getRequirementsFromConfig(slideId) {
|
|
72
|
+
if (!this.courseConfig || !this.courseConfig.structure) {
|
|
73
|
+
throw new Error(`[EngagementManager] CRITICAL: courseConfig not available when looking up slide: ${slideId}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const findSlide = (items) => {
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
if (item.id === slideId) return item;
|
|
79
|
+
if (item.children) {
|
|
80
|
+
const found = findSlide(item.children);
|
|
81
|
+
if (found) return found;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const slide = findSlide(this.courseConfig.structure);
|
|
88
|
+
if (!slide) {
|
|
89
|
+
throw new Error(`[EngagementManager] CRITICAL: Slide "${slideId}" not found in courseConfig.structure`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!slide.engagement) {
|
|
93
|
+
throw new Error(`[EngagementManager] CRITICAL: Slide "${slideId}" has no engagement config in courseConfig`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return slide.engagement;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Initializes engagement tracking for a slide.
|
|
101
|
+
* Preserves existing completion state if slide was already completed.
|
|
102
|
+
*/
|
|
103
|
+
initSlide(slideId, requirements) {
|
|
104
|
+
if (!slideId || typeof slideId !== 'string') {
|
|
105
|
+
throw new Error('[EngagementManager] slideId must be a non-empty string');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!requirements || typeof requirements !== 'object') {
|
|
109
|
+
throw new Error(`[EngagementManager] Slide "${slideId}" has invalid engagement requirements`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isRequired = requirements.required || false;
|
|
113
|
+
|
|
114
|
+
if (!isRequired) {
|
|
115
|
+
logger.debug(`[EngagementManager] Slide "${slideId}" does not require tracking, skipping state initialization`);
|
|
116
|
+
eventBus.emit('engagement:initialized', { slideId, requirements });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const state = this._getState();
|
|
121
|
+
const existingState = state[slideId];
|
|
122
|
+
const wasCompleted = existingState?.complete === true;
|
|
123
|
+
|
|
124
|
+
if (wasCompleted) {
|
|
125
|
+
state[slideId].required = true;
|
|
126
|
+
logger.debug(`[EngagementManager] Slide "${slideId}" was already completed`);
|
|
127
|
+
} else if (existingState) {
|
|
128
|
+
state[slideId].required = true;
|
|
129
|
+
logger.debug(`[EngagementManager] Slide "${slideId}" has existing progress, preserving tracked data`);
|
|
130
|
+
} else {
|
|
131
|
+
state[slideId] = {
|
|
132
|
+
required: true,
|
|
133
|
+
tracked: getTrackedFieldDefaults(),
|
|
134
|
+
complete: false
|
|
135
|
+
};
|
|
136
|
+
logger.debug(`[EngagementManager] Initialized tracking for: ${slideId}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this._setState(state);
|
|
140
|
+
this._startTimeTrackingIfNeeded(slideId, requirements);
|
|
141
|
+
eventBus.emit('engagement:initialized', { slideId, requirements });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Cleans up engagement tracking when leaving a slide.
|
|
146
|
+
*/
|
|
147
|
+
cleanupSlide(slideId) {
|
|
148
|
+
if (!slideId) return;
|
|
149
|
+
|
|
150
|
+
this._stopTimeTracking(slideId);
|
|
151
|
+
|
|
152
|
+
const evaluation = this.evaluateRequirements(slideId);
|
|
153
|
+
|
|
154
|
+
if (!evaluation.complete) {
|
|
155
|
+
eventBus.emit('engagement:incomplete', {
|
|
156
|
+
slideId,
|
|
157
|
+
unmetRequirements: evaluation.unmetRequirements
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
logger.debug(`[EngagementManager] Cleaned up: ${slideId}`, evaluation);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// =========================================================================
|
|
165
|
+
// Time Tracking
|
|
166
|
+
// =========================================================================
|
|
167
|
+
|
|
168
|
+
/** @private */
|
|
169
|
+
_startTimeTrackingIfNeeded(slideId, requirements) {
|
|
170
|
+
this._stopTimeTracking(slideId);
|
|
171
|
+
|
|
172
|
+
if (!requirements.required) return;
|
|
173
|
+
|
|
174
|
+
const hasTimeRequirement = requirements.requirements?.some(
|
|
175
|
+
req => req.type === 'timeOnSlide'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!hasTimeRequirement) return;
|
|
179
|
+
|
|
180
|
+
const state = this._getState();
|
|
181
|
+
if (state[slideId]?.complete) {
|
|
182
|
+
logger.debug(`[EngagementManager] Slide "${slideId}" already complete, skipping time tracking`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
logger.debug(`[EngagementManager] Starting time tracking for: ${slideId}`);
|
|
187
|
+
|
|
188
|
+
const intervalId = setInterval(() => {
|
|
189
|
+
const evaluation = this.evaluateRequirements(slideId);
|
|
190
|
+
if (!evaluation.complete) {
|
|
191
|
+
eventBus.emit('engagement:progress', {
|
|
192
|
+
slideId,
|
|
193
|
+
complete: false,
|
|
194
|
+
progress: evaluation.progress
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
this._stopTimeTracking(slideId);
|
|
198
|
+
}
|
|
199
|
+
}, 100);
|
|
200
|
+
|
|
201
|
+
this._timeTrackingIntervals.set(slideId, intervalId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @private */
|
|
205
|
+
_stopTimeTracking(slideId) {
|
|
206
|
+
const intervalId = this._timeTrackingIntervals.get(slideId);
|
|
207
|
+
if (intervalId) {
|
|
208
|
+
clearInterval(intervalId);
|
|
209
|
+
this._timeTrackingIntervals.delete(slideId);
|
|
210
|
+
logger.debug(`[EngagementManager] Stopped time tracking for: ${slideId}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =========================================================================
|
|
215
|
+
// Queries
|
|
216
|
+
// =========================================================================
|
|
217
|
+
|
|
218
|
+
isSlideComplete(slideId) {
|
|
219
|
+
return this.evaluateRequirements(slideId).complete;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getSlideState(slideId) {
|
|
223
|
+
const state = this._getState();
|
|
224
|
+
return state[slideId] || null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getProgress(slideId) {
|
|
228
|
+
const slideState = this.getSlideState(slideId);
|
|
229
|
+
if (!slideState) return null;
|
|
230
|
+
const requirementsConfig = this._getRequirementsFromConfig(slideId);
|
|
231
|
+
const requirements = requirementsConfig?.requirements || [];
|
|
232
|
+
return calculateProgress(slideId, slideState.tracked, requirements, strategies, this._buildContext(slideId));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// =========================================================================
|
|
236
|
+
// Evaluation
|
|
237
|
+
// =========================================================================
|
|
238
|
+
|
|
239
|
+
evaluateRequirements(slideId) {
|
|
240
|
+
const state = this._getState();
|
|
241
|
+
const slideState = state[slideId];
|
|
242
|
+
|
|
243
|
+
if (!slideState) {
|
|
244
|
+
try {
|
|
245
|
+
const requirementsConfig = this._getRequirementsFromConfig(slideId);
|
|
246
|
+
const isRequired = requirementsConfig?.required || false;
|
|
247
|
+
return { complete: !isRequired, progress: {}, unmetRequirements: [] };
|
|
248
|
+
} catch (_error) {
|
|
249
|
+
return { complete: true, progress: {}, unmetRequirements: [] };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const isRequired = slideState.required || false;
|
|
254
|
+
if (!isRequired) {
|
|
255
|
+
return { complete: true, progress: {}, unmetRequirements: [] };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const requirementsConfig = this._getRequirementsFromConfig(slideId);
|
|
259
|
+
const requirements = requirementsConfig.requirements || [];
|
|
260
|
+
const mode = requirementsConfig.mode || 'all';
|
|
261
|
+
|
|
262
|
+
const unmetRequirements = [];
|
|
263
|
+
|
|
264
|
+
for (const req of requirements) {
|
|
265
|
+
try {
|
|
266
|
+
const result = this._evaluateRequirement(slideId, req, slideState.tracked);
|
|
267
|
+
if (!result.met) {
|
|
268
|
+
unmetRequirements.push(result);
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger.error(`[EngagementManager] Error evaluating requirement for slide "${slideId}": ${error.message}`, { requirement: req });
|
|
272
|
+
// Fail safe: treat as unmet requirement to prevent skipping
|
|
273
|
+
unmetRequirements.push({
|
|
274
|
+
met: false,
|
|
275
|
+
requirement: req,
|
|
276
|
+
progress: 0,
|
|
277
|
+
reason: `Evaluation Error: ${error.message}`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const complete = mode === 'all'
|
|
283
|
+
? unmetRequirements.length === 0
|
|
284
|
+
: unmetRequirements.length < requirements.length;
|
|
285
|
+
|
|
286
|
+
if (complete && !slideState.complete) {
|
|
287
|
+
slideState.complete = true;
|
|
288
|
+
this._setState(state);
|
|
289
|
+
eventBus.emit('engagement:complete', { slideId });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let progress = {};
|
|
293
|
+
try {
|
|
294
|
+
progress = calculateProgress(slideId, slideState.tracked, requirements, strategies, this._buildContext(slideId));
|
|
295
|
+
} catch (error) {
|
|
296
|
+
logger.error(`[EngagementManager] Error calculating progress for slide "${slideId}": ${error.message}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
complete,
|
|
301
|
+
progress,
|
|
302
|
+
unmetRequirements
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** @private */
|
|
307
|
+
_evaluateRequirement(slideId, requirement, tracked) {
|
|
308
|
+
const strategy = strategies[requirement.type];
|
|
309
|
+
if (!strategy) {
|
|
310
|
+
throw new Error(`[EngagementManager] Unknown requirement type: ${requirement.type}. Valid types: ${validTypes.join(', ')}.`);
|
|
311
|
+
}
|
|
312
|
+
return strategy.evaluate(requirement, tracked, this._buildContext(slideId));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** @private */
|
|
316
|
+
_buildContext(slideId) {
|
|
317
|
+
return {
|
|
318
|
+
slideId,
|
|
319
|
+
stateManager,
|
|
320
|
+
interactionRegistry: window.CourseCode?.interactionRegistry,
|
|
321
|
+
formatTime: formatTimeHuman,
|
|
322
|
+
formatInteractionId: formatInteractionId
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// =========================================================================
|
|
327
|
+
// Reset
|
|
328
|
+
// =========================================================================
|
|
329
|
+
|
|
330
|
+
resetSlide(slideId) {
|
|
331
|
+
const state = this._getState();
|
|
332
|
+
if (!state[slideId]) return;
|
|
333
|
+
const requirements = this._getRequirementsFromConfig(slideId);
|
|
334
|
+
if (!requirements) {
|
|
335
|
+
throw new Error(`[EngagementManager] No requirements found for slide: ${slideId}. Ensure slide has engagement config in course-config.js.`);
|
|
336
|
+
}
|
|
337
|
+
delete state[slideId];
|
|
338
|
+
this._setState(state);
|
|
339
|
+
this.initSlide(slideId, requirements);
|
|
340
|
+
logger.debug(`[EngagementManager] Reset: ${slideId}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
resetAllSlides() {
|
|
344
|
+
this._setState({});
|
|
345
|
+
logger.debug('[EngagementManager] Reset all engagement');
|
|
346
|
+
eventBus.emit('engagement:reset-all');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// =========================================================================
|
|
350
|
+
// Internal
|
|
351
|
+
// =========================================================================
|
|
352
|
+
|
|
353
|
+
/** @private */
|
|
354
|
+
_handleFlagUpdate({ key, value: _value }) {
|
|
355
|
+
const state = this._getState();
|
|
356
|
+
|
|
357
|
+
Object.entries(state).forEach(([slideId, slideState]) => {
|
|
358
|
+
const requirementsConfig = this._getRequirementsFromConfig(slideId);
|
|
359
|
+
if (!requirementsConfig || !requirementsConfig.required) return;
|
|
360
|
+
if (slideState.complete) return;
|
|
361
|
+
|
|
362
|
+
const hasFlagRequirements = requirementsConfig.requirements?.some(
|
|
363
|
+
req => req.type === 'flag' || req.type === 'allFlags'
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (hasFlagRequirements) {
|
|
367
|
+
logger.debug(`[EngagementManager] Flag "${key}" updated, re-evaluating: ${slideId}`);
|
|
368
|
+
this._checkAndEmitProgress(slideId);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** @private */
|
|
374
|
+
_checkAndEmitProgress(slideId) {
|
|
375
|
+
const evaluation = this.evaluateRequirements(slideId);
|
|
376
|
+
eventBus.emit('engagement:progress', {
|
|
377
|
+
slideId,
|
|
378
|
+
complete: evaluation.complete,
|
|
379
|
+
progress: evaluation.progress
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** @private */
|
|
384
|
+
_getState() {
|
|
385
|
+
const rawState = stateManager.getDomainState(this.domain) || {};
|
|
386
|
+
return mergeWithDefaults(rawState);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** @private */
|
|
390
|
+
_setState(state) {
|
|
391
|
+
try {
|
|
392
|
+
const optimizedState = stripDefaultValues(state);
|
|
393
|
+
stateManager.setDomainState(this.domain, optimizedState);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
logger.error('[EngagementManager] Failed to save state:', { domain: 'engagement', operation: '_setState', stack: error.stack, slideIds: Object.keys(state) });
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Mix in tracker methods from engagement-trackers.js
|
|
402
|
+
Object.assign(EngagementManager.prototype, trackers);
|
|
403
|
+
|
|
404
|
+
export default new EngagementManager();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file engagement-progress.js
|
|
3
|
+
* @description Pure functions for calculating engagement progress,
|
|
4
|
+
* building tooltips, and serializing/deserializing engagement state.
|
|
5
|
+
* No side effects or state manager dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Tracked field defaults are derived from strategy declarations in
|
|
8
|
+
* requirement-strategies.js — no hardcoded field lists here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getTrackedFieldDefaults } from './requirement-strategies.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Calculates progress with partial completion support.
|
|
15
|
+
* @param {string} slideId - The slide identifier
|
|
16
|
+
* @param {object} tracked - The tracked data
|
|
17
|
+
* @param {array} requirements - The requirements array
|
|
18
|
+
* @param {object} strategies - The requirement strategies map
|
|
19
|
+
* @param {object} ctx - The strategy context
|
|
20
|
+
* @returns {object} Progress summary with percentage, items, and tooltip
|
|
21
|
+
*/
|
|
22
|
+
export function calculateProgress(slideId, tracked, requirements, strategies, ctx) {
|
|
23
|
+
const items = [];
|
|
24
|
+
let totalProgress = 0;
|
|
25
|
+
|
|
26
|
+
for (const req of requirements) {
|
|
27
|
+
const strategy = strategies[req.type];
|
|
28
|
+
const result = strategy.evaluate(req, tracked, ctx);
|
|
29
|
+
const requirementProgress = result.met ? 1 : strategy.progress(req, tracked, result, ctx);
|
|
30
|
+
const dynamicLabel = req.message || strategy.label(req, tracked, result, ctx);
|
|
31
|
+
|
|
32
|
+
items.push({
|
|
33
|
+
type: req.type,
|
|
34
|
+
label: dynamicLabel,
|
|
35
|
+
complete: result.met
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
totalProgress += requirementProgress;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const percentage = requirements.length > 0
|
|
42
|
+
? Math.round((totalProgress / requirements.length) * 100)
|
|
43
|
+
: 100;
|
|
44
|
+
|
|
45
|
+
const tooltip = buildTooltip(items, percentage);
|
|
46
|
+
|
|
47
|
+
return { percentage, items, tooltip };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds a tooltip string from progress items.
|
|
52
|
+
* @param {array} items - Progress items with labels and completion status
|
|
53
|
+
* @param {number} percentage - Completion percentage
|
|
54
|
+
* @returns {string} Tooltip text
|
|
55
|
+
*/
|
|
56
|
+
export function buildTooltip(items, percentage) {
|
|
57
|
+
if (!items || items.length === 0) {
|
|
58
|
+
return `Slide Progress: ${percentage}%`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (percentage === 100) {
|
|
62
|
+
return 'All requirements complete';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const incomplete = items.filter(item => !item.complete);
|
|
66
|
+
if (incomplete.length === 0) {
|
|
67
|
+
return 'All requirements complete';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const labels = incomplete.map(item => item.label);
|
|
71
|
+
|
|
72
|
+
if (labels.length === 1) {
|
|
73
|
+
return labels[0];
|
|
74
|
+
} else if (labels.length === 2) {
|
|
75
|
+
return `${labels[0]} and ${labels[1]}`;
|
|
76
|
+
} else {
|
|
77
|
+
return `${labels.slice(0, -1).join(', ')}, and ${labels.at(-1)}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Formats seconds into a human-friendly string.
|
|
83
|
+
* @param {number} seconds - Time in seconds
|
|
84
|
+
* @returns {string} Human-readable time string (e.g., "2 minutes", "45 seconds")
|
|
85
|
+
*/
|
|
86
|
+
export function formatTimeHuman(seconds) {
|
|
87
|
+
if (seconds < 60) {
|
|
88
|
+
return `${seconds} second${seconds === 1 ? '' : 's'}`;
|
|
89
|
+
}
|
|
90
|
+
const minutes = Math.floor(seconds / 60);
|
|
91
|
+
const remainingSeconds = seconds % 60;
|
|
92
|
+
if (remainingSeconds === 0) {
|
|
93
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
94
|
+
}
|
|
95
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Formats an interaction ID into a human-readable label.
|
|
100
|
+
* Converts kebab-case IDs like "system-architecture-dd" to "System Architecture Dd".
|
|
101
|
+
* @param {string} interactionId - The interaction identifier
|
|
102
|
+
* @returns {string} Human-readable label
|
|
103
|
+
*/
|
|
104
|
+
export function formatInteractionId(interactionId) {
|
|
105
|
+
if (!interactionId) return 'interaction';
|
|
106
|
+
|
|
107
|
+
return interactionId
|
|
108
|
+
.split(/[-_]/)
|
|
109
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
110
|
+
.join(' ');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Merges stored engagement state with default values.
|
|
115
|
+
* Defaults are derived from strategy field declarations — no hardcoded list.
|
|
116
|
+
* @param {object} state - The raw state from storage
|
|
117
|
+
* @returns {object} State with defaults filled in
|
|
118
|
+
*/
|
|
119
|
+
export function mergeWithDefaults(state) {
|
|
120
|
+
const fieldDefaults = getTrackedFieldDefaults();
|
|
121
|
+
const merged = {};
|
|
122
|
+
|
|
123
|
+
for (const [slideId, slideState] of Object.entries(state)) {
|
|
124
|
+
const tracked = {};
|
|
125
|
+
for (const [key, defaultVal] of Object.entries(fieldDefaults)) {
|
|
126
|
+
const stored = slideState.tracked?.[key];
|
|
127
|
+
if (stored !== undefined && stored !== null) {
|
|
128
|
+
tracked[key] = stored;
|
|
129
|
+
} else {
|
|
130
|
+
// Deep-clone defaults so slides don't share array/object references
|
|
131
|
+
tracked[key] = Array.isArray(defaultVal) ? [] :
|
|
132
|
+
(typeof defaultVal === 'object' && defaultVal !== null) ? { ...defaultVal } :
|
|
133
|
+
defaultVal;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
merged[slideId] = {
|
|
137
|
+
required: slideState.required || false,
|
|
138
|
+
tracked,
|
|
139
|
+
complete: slideState.complete || false
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return merged;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Strips default values from engagement state to reduce storage size.
|
|
148
|
+
* Uses strategy field declarations to determine what constitutes a "default".
|
|
149
|
+
* @param {object} state - The full engagement state
|
|
150
|
+
* @returns {object} Optimized state with defaults removed
|
|
151
|
+
*/
|
|
152
|
+
export function stripDefaultValues(state) {
|
|
153
|
+
const fieldDefaults = getTrackedFieldDefaults();
|
|
154
|
+
const optimized = {};
|
|
155
|
+
|
|
156
|
+
for (const [slideId, slideState] of Object.entries(state)) {
|
|
157
|
+
const optimizedSlide = {
|
|
158
|
+
required: slideState.required
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (slideState.tracked) {
|
|
162
|
+
const tracked = {};
|
|
163
|
+
|
|
164
|
+
for (const [key, defaultVal] of Object.entries(fieldDefaults)) {
|
|
165
|
+
const val = slideState.tracked[key];
|
|
166
|
+
if (val === undefined || val === null) continue;
|
|
167
|
+
|
|
168
|
+
// Only include if value differs from default
|
|
169
|
+
if (Array.isArray(defaultVal)) {
|
|
170
|
+
if (val.length > 0) tracked[key] = val;
|
|
171
|
+
} else if (typeof defaultVal === 'object' && defaultVal !== null) {
|
|
172
|
+
if (Object.keys(val).length > 0) tracked[key] = val;
|
|
173
|
+
} else if (typeof defaultVal === 'boolean') {
|
|
174
|
+
if (val) tracked[key] = val;
|
|
175
|
+
} else if (typeof defaultVal === 'number') {
|
|
176
|
+
if (val > 0) tracked[key] = val;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (Object.keys(tracked).length > 0) {
|
|
181
|
+
optimizedSlide.tracked = tracked;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
optimizedSlide.complete = slideState.complete || false;
|
|
186
|
+
|
|
187
|
+
optimized[slideId] = optimizedSlide;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return optimized;
|
|
191
|
+
}
|