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,88 @@
|
|
|
1
|
+
import interactionManager from './interaction-manager.js';
|
|
2
|
+
import { logger } from '../utilities/logger.js';
|
|
3
|
+
import stateManager from '../state/index.js';
|
|
4
|
+
import { generateScormTimestamp } from '../validation/scorm-validators.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} Comment
|
|
8
|
+
* @property {string} text - The content of the comment.
|
|
9
|
+
* @property {string} location - The slide or location where the comment was made.
|
|
10
|
+
* @property {string} timestamp - The ISO 8601 timestamp of when the comment was created.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DOMAIN_KEY = 'comments';
|
|
14
|
+
|
|
15
|
+
class CommentManager {
|
|
16
|
+
/**
|
|
17
|
+
* @type {Comment[]}
|
|
18
|
+
*/
|
|
19
|
+
#comments;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.#comments = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initializes the manager by hydrating comments from domain state.
|
|
27
|
+
* This must be called after the StateManager is initialized.
|
|
28
|
+
*/
|
|
29
|
+
initialize() {
|
|
30
|
+
this.#comments = stateManager.getDomainState(DOMAIN_KEY) || [];
|
|
31
|
+
logger.debug('CommentManager initialized');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Adds a new comment and persists it to the LMS.
|
|
36
|
+
* @param {string} text - The content of the comment.
|
|
37
|
+
* @param {string} [location=''] - The slide ID or location identifier.
|
|
38
|
+
* @throws {Error} If text is empty or not a valid string
|
|
39
|
+
*/
|
|
40
|
+
addComment(text, location = '') {
|
|
41
|
+
if (!text || typeof text !== 'string' || text.trim() === '') {
|
|
42
|
+
throw new Error('CommentManager: Comment text cannot be empty.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const newComment = {
|
|
46
|
+
text,
|
|
47
|
+
location,
|
|
48
|
+
timestamp: generateScormTimestamp(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Update in-memory state
|
|
52
|
+
this.#comments.push(newComment);
|
|
53
|
+
|
|
54
|
+
// Persist via domain state (stored in suspend_data)
|
|
55
|
+
stateManager.setDomainState(DOMAIN_KEY, this.#comments);
|
|
56
|
+
|
|
57
|
+
logger.debug('Comment added and persisted:', newComment);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Adds a course rating as a likert interaction.
|
|
62
|
+
* @param {number | string} rating - The rating value provided by the user.
|
|
63
|
+
*/
|
|
64
|
+
addRating(rating) {
|
|
65
|
+
if (rating === null || rating === undefined) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interactionManager.addLikertInteraction({
|
|
70
|
+
id: 'course-rating',
|
|
71
|
+
response: String(rating),
|
|
72
|
+
description: 'Overall course satisfaction rating on a 5-star scale.'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
logger.debug('Rating added and persisted to LMS:', rating);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Retrieves all comments from the in-memory store.
|
|
80
|
+
* @returns {Comment[]} An array of all comments.
|
|
81
|
+
*/
|
|
82
|
+
getComments() {
|
|
83
|
+
return [...this.#comments];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const commentManager = new CommentManager();
|
|
88
|
+
export default commentManager;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file flag-manager.js
|
|
3
|
+
* @description Manages global flags for the course. It provides a simple API
|
|
4
|
+
* for setting and retrieving boolean flags, persisting the entire flags object
|
|
5
|
+
* as a single domain in the StateManager.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import stateManager from '../state/index.js';
|
|
9
|
+
import { logger } from '../utilities/logger.js';
|
|
10
|
+
import { eventBus } from '../core/event-bus.js';
|
|
11
|
+
import { deepClone } from '../utilities/utilities.js';
|
|
12
|
+
|
|
13
|
+
class FlagManager {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.flags = {};
|
|
16
|
+
this.isInitialized = false;
|
|
17
|
+
this.DOMAIN_KEY = 'flags';
|
|
18
|
+
this.SOURCE = 'flag-manager';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initializes the manager by loading flags from the StateManager.
|
|
23
|
+
* @throws {Error} If already initialized
|
|
24
|
+
*/
|
|
25
|
+
initialize() {
|
|
26
|
+
if (this.isInitialized) {
|
|
27
|
+
throw new Error('FlagManager: Already initialized. Do not call initialize() more than once.');
|
|
28
|
+
}
|
|
29
|
+
const persistedFlags = stateManager.getDomainState(this.DOMAIN_KEY);
|
|
30
|
+
if (persistedFlags && typeof persistedFlags === 'object') {
|
|
31
|
+
this.flags = persistedFlags;
|
|
32
|
+
}
|
|
33
|
+
this.isInitialized = true;
|
|
34
|
+
logger.debug('FlagManager initialized.', this.flags);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Retrieves all flags.
|
|
39
|
+
* @returns {object} A deep-cloned object of all flags.
|
|
40
|
+
*/
|
|
41
|
+
getAllFlags() {
|
|
42
|
+
return deepClone(this.flags);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Retrieves a single flag by its key.
|
|
47
|
+
* @param {string} key - The key for the flag.
|
|
48
|
+
* @returns {any} The value of the flag, or undefined if not found.
|
|
49
|
+
*/
|
|
50
|
+
getFlag(key) {
|
|
51
|
+
const val = this.flags[key];
|
|
52
|
+
return (val && typeof val === 'object') ? deepClone(val) : val;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sets a flag's value and persists the change.
|
|
57
|
+
* @param {string} key - The key for the flag.
|
|
58
|
+
* @param {any} value - The value to set for the flag.
|
|
59
|
+
* @throws {Error} If key is not a non-empty string
|
|
60
|
+
*/
|
|
61
|
+
setFlag(key, value) {
|
|
62
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
63
|
+
throw new Error('FlagManager: setFlag requires a non-empty string key.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.flags[key] = value;
|
|
67
|
+
|
|
68
|
+
stateManager.setDomainState(this.DOMAIN_KEY, this.flags, { source: this.SOURCE });
|
|
69
|
+
eventBus.emit('flag:updated', { key, value });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Removes a flag and persists the change.
|
|
74
|
+
* @param {string} key - The key for the flag to remove.
|
|
75
|
+
*/
|
|
76
|
+
removeFlag(key) {
|
|
77
|
+
if (this.flags.hasOwnProperty(key)) {
|
|
78
|
+
delete this.flags[key];
|
|
79
|
+
stateManager.setDomainState(this.DOMAIN_KEY, this.flags, { source: this.SOURCE });
|
|
80
|
+
eventBus.emit('flag:removed', { key });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const instance = new FlagManager();
|
|
86
|
+
export default instance;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file interaction-manager.js
|
|
3
|
+
* @description Manages student interactions for SCORM persistence.
|
|
4
|
+
* This is the single source of truth for all interaction data and persists
|
|
5
|
+
* its state through the StateManager.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logger } from '../utilities/logger.js';
|
|
9
|
+
|
|
10
|
+
import { deepClone, generateId } from '../utilities/utilities.js';
|
|
11
|
+
import stateManager from '../state/index.js';
|
|
12
|
+
import { eventBus } from '../core/event-bus.js';
|
|
13
|
+
import {
|
|
14
|
+
SCORM_INTERACTION_TYPES as _SCORM_INTERACTION_TYPES,
|
|
15
|
+
SCORM_INTERACTION_RESULTS as _SCORM_INTERACTION_RESULTS,
|
|
16
|
+
isValidISO8601Timestamp,
|
|
17
|
+
isValidISO8601Duration,
|
|
18
|
+
validateInteractionType,
|
|
19
|
+
validateInteractionResult,
|
|
20
|
+
validateNumeric,
|
|
21
|
+
validateStringArray,
|
|
22
|
+
validateRequiredFields,
|
|
23
|
+
formatValidationError,
|
|
24
|
+
generateScormTimestamp
|
|
25
|
+
} from '../validation/scorm-validators.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} InteractionData
|
|
29
|
+
* @property {string} [id] - Unique identifier (auto-generated if not provided)
|
|
30
|
+
* @property {string} type - SCORM interaction type
|
|
31
|
+
* @property {string} [learner_response] - The learner's response
|
|
32
|
+
* @property {string} [result] - Result of the interaction
|
|
33
|
+
* @property {string} [description] - Description of the interaction
|
|
34
|
+
* @property {string} [timestamp] - ISO 8601 timestamp
|
|
35
|
+
* @property {number} [weighting] - Weight of the interaction
|
|
36
|
+
* @property {string} [latency] - ISO 8601 duration format
|
|
37
|
+
* @property {string[]} [correct_responses] - Array of correct response patterns
|
|
38
|
+
* @property {string[]} [objectives] - Array of linked objective IDs
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
class InteractionManager {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.interactions = [];
|
|
44
|
+
this.isInitialized = false;
|
|
45
|
+
this.DOMAIN_KEY = 'interactions';
|
|
46
|
+
this.SOURCE = 'interaction-manager';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initializes the manager by loading state from the StateManager.
|
|
51
|
+
* @throws {Error} If already initialized
|
|
52
|
+
*/
|
|
53
|
+
initialize() {
|
|
54
|
+
if (this.isInitialized) {
|
|
55
|
+
throw new Error('InteractionManager: Already initialized. Do not call initialize() more than once.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load interactions via stateManager domain API
|
|
59
|
+
// stateManager transparently routes to cmi.interactions.n.*
|
|
60
|
+
this.interactions = stateManager.getDomainState(this.DOMAIN_KEY) || [];
|
|
61
|
+
|
|
62
|
+
this.isInitialized = true;
|
|
63
|
+
logger.debug('InteractionManager initialized.', this.interactions);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Retrieves all interactions.
|
|
68
|
+
* @returns {Array<object>} A deep-cloned array of all interaction objects.
|
|
69
|
+
*/
|
|
70
|
+
getAllInteractions() {
|
|
71
|
+
return deepClone(this.interactions);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Retrieves a single interaction by its ID.
|
|
76
|
+
* @param {string} id - The unique identifier for the interaction.
|
|
77
|
+
* @returns {object|null} A deep-cloned interaction object or null if not found.
|
|
78
|
+
*/
|
|
79
|
+
getInteraction(id) {
|
|
80
|
+
const interaction = this.interactions.find(i => i.id === id);
|
|
81
|
+
return interaction ? deepClone(interaction) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validates interaction data against SCORM 2004 4th Edition specification.
|
|
86
|
+
* @private
|
|
87
|
+
* @param {InteractionData} data - The interaction data to validate
|
|
88
|
+
* @throws {Error} If validation fails with detailed error messages
|
|
89
|
+
*/
|
|
90
|
+
_validateInteractionData(data) {
|
|
91
|
+
if (!data || typeof data !== 'object') {
|
|
92
|
+
throw new Error('[InteractionManager] Interaction data must be a non-null object');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const errors = [];
|
|
96
|
+
|
|
97
|
+
// Validate required field: type
|
|
98
|
+
const requiredCheck = validateRequiredFields(data, ['type']);
|
|
99
|
+
if (!requiredCheck.valid) {
|
|
100
|
+
errors.push(...requiredCheck.errors);
|
|
101
|
+
} else {
|
|
102
|
+
// Only validate type value if field is present
|
|
103
|
+
const typeCheck = validateInteractionType(data.type);
|
|
104
|
+
if (!typeCheck.valid) {
|
|
105
|
+
errors.push(typeCheck.error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate result if provided
|
|
110
|
+
if (data.result) {
|
|
111
|
+
const resultCheck = validateInteractionResult(data.result);
|
|
112
|
+
if (!resultCheck.valid) {
|
|
113
|
+
errors.push(resultCheck.error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate timestamp format if provided
|
|
118
|
+
if (data.timestamp && !isValidISO8601Timestamp(data.timestamp)) {
|
|
119
|
+
errors.push(`Invalid timestamp "${data.timestamp}". Must be ISO 8601 format`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Validate weighting if provided
|
|
123
|
+
if (data.weighting !== undefined && data.weighting !== null) {
|
|
124
|
+
const weightCheck = validateNumeric(data.weighting, 'weighting');
|
|
125
|
+
if (!weightCheck.valid) {
|
|
126
|
+
errors.push(weightCheck.error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate latency format if provided (ISO 8601 duration)
|
|
131
|
+
if (data.latency && !isValidISO8601Duration(data.latency)) {
|
|
132
|
+
errors.push(`Invalid latency "${data.latency}". Must be ISO 8601 duration format (e.g., "PT1H30M")`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validate correct_responses if provided
|
|
136
|
+
if (data.correct_responses !== undefined && !Array.isArray(data.correct_responses)) {
|
|
137
|
+
errors.push('Field "correct_responses" must be an array');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Validate objectives if provided
|
|
141
|
+
if (data.objectives !== undefined) {
|
|
142
|
+
const objCheck = validateStringArray(data.objectives, 'objectives');
|
|
143
|
+
if (!objCheck.valid) {
|
|
144
|
+
errors.push(objCheck.error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (errors.length > 0) {
|
|
149
|
+
const context = data.id ? `interaction "${data.id}"` : 'interaction';
|
|
150
|
+
const errorMsg = '[InteractionManager] ' + formatValidationError(errors, context);
|
|
151
|
+
throw new Error(errorMsg);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Records a new interaction.
|
|
157
|
+
* Uses stateManager's domain API with append-only semantics.
|
|
158
|
+
* stateManager handles all CMI routing transparently.
|
|
159
|
+
*
|
|
160
|
+
* @param {InteractionData} interactionData - The interaction data conforming to SCORM CMI data model
|
|
161
|
+
* @returns {object} The recorded interaction object, including generated ID and timestamp
|
|
162
|
+
* @throws {Error} If validation fails or state persistence fails
|
|
163
|
+
*/
|
|
164
|
+
record(interactionData) {
|
|
165
|
+
// Validate input - this will throw if validation fails
|
|
166
|
+
this._validateInteractionData(interactionData);
|
|
167
|
+
|
|
168
|
+
const newInteraction = {
|
|
169
|
+
id: interactionData.id || generateId('interaction'),
|
|
170
|
+
type: interactionData.type,
|
|
171
|
+
learner_response: interactionData.learner_response || '',
|
|
172
|
+
result: interactionData.result || 'neutral',
|
|
173
|
+
description: interactionData.description || '',
|
|
174
|
+
timestamp: interactionData.timestamp || generateScormTimestamp(),
|
|
175
|
+
weighting: interactionData.weighting,
|
|
176
|
+
latency: interactionData.latency,
|
|
177
|
+
correct_responses: interactionData.correct_responses,
|
|
178
|
+
objectives: interactionData.objectives
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Persist via stateManager domain API (append-only semantics)
|
|
182
|
+
// stateManager transparently routes to cmi.interactions.n.*
|
|
183
|
+
try {
|
|
184
|
+
const result = stateManager.setDomainState(this.DOMAIN_KEY, newInteraction);
|
|
185
|
+
// Update in-memory array with the result (includes _index from stateManager)
|
|
186
|
+
this.interactions.push(result || newInteraction);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const wrappedError = new Error(`[InteractionManager] Failed to record interaction "${newInteraction.id}": ${error.message}`);
|
|
189
|
+
this._emitError('record', wrappedError, { interactionId: newInteraction.id });
|
|
190
|
+
throw wrappedError;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
eventBus.emit('interaction:recorded', newInteraction);
|
|
194
|
+
|
|
195
|
+
return deepClone(newInteraction);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Adds a likert interaction to the LMS.
|
|
200
|
+
* Likert interactions are used for ratings and feedback (1-5 scales, etc).
|
|
201
|
+
*
|
|
202
|
+
* This method now uses record() internally to ensure consistent validation,
|
|
203
|
+
* error handling, and state management.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} interaction - The interaction data.
|
|
206
|
+
* @param {string} interaction.id - A unique identifier for the interaction.
|
|
207
|
+
* @param {string} interaction.response - The learner's response.
|
|
208
|
+
* @param {string} interaction.description - A description of the interaction.
|
|
209
|
+
* @returns {object} The recorded interaction object
|
|
210
|
+
* @throws {Error} If InteractionManager is not initialized
|
|
211
|
+
* @throws {Error} If interaction data is invalid
|
|
212
|
+
* @throws {Error} If SCORM sync or state persistence fails
|
|
213
|
+
*/
|
|
214
|
+
addLikertInteraction({ id, response, description }) {
|
|
215
|
+
if (!this.isInitialized) {
|
|
216
|
+
const error = new Error('[InteractionManager] Not initialized. Call initialize() first.');
|
|
217
|
+
this._emitError('addLikertInteraction', error, { interactionId: id });
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (!id || typeof id !== 'string') {
|
|
221
|
+
const error = new Error('[InteractionManager] likert interaction id is required and must be a string');
|
|
222
|
+
this._emitError('addLikertInteraction', error, { interactionId: id });
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
if (response === undefined || response === null || String(response).trim() === '') {
|
|
226
|
+
const error = new Error('[InteractionManager] likert interaction response is required');
|
|
227
|
+
this._emitError('addLikertInteraction', error, { interactionId: id });
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Use record() to ensure consistent validation and state management
|
|
232
|
+
return this.record({
|
|
233
|
+
id: id,
|
|
234
|
+
type: 'likert',
|
|
235
|
+
learner_response: String(response),
|
|
236
|
+
description: description || '',
|
|
237
|
+
result: 'neutral' // Likert doesn't have right/wrong answers
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Logs a standardized error for interaction operations.
|
|
243
|
+
* @private
|
|
244
|
+
* @param {string} operation - The operation that failed
|
|
245
|
+
* @param {Error} error - The error object
|
|
246
|
+
* @param {object} context - Additional context about the error
|
|
247
|
+
*/
|
|
248
|
+
_emitError(operation, error, context) {
|
|
249
|
+
logger.error(error.message, { domain: 'interaction', operation, stack: error.stack, ...context });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const instance = new InteractionManager();
|
|
254
|
+
export default instance;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file interaction-registry.js
|
|
3
|
+
* @description Runtime registry for currently rendered interaction component instances.
|
|
4
|
+
* Separate from InteractionManager which handles SCORM persistence.
|
|
5
|
+
*
|
|
6
|
+
* This registry tracks interactions on the CURRENT SLIDE ONLY and is cleared on navigation.
|
|
7
|
+
* It provides a live reference to interaction instances for:
|
|
8
|
+
* - EngagementManager: counting interactions for completion requirements
|
|
9
|
+
* - Automation API: programmatic testing and manipulation
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This registry is NOT used by runtime-linter. The linter performs static analysis
|
|
12
|
+
* by rendering slides to detached DOM and querying for [data-interaction-id] elements.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { eventBus } from '../core/event-bus.js';
|
|
16
|
+
import { logger } from '../utilities/logger.js';
|
|
17
|
+
|
|
18
|
+
class InteractionRegistry {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.registry = new Map();
|
|
21
|
+
this.isReady = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Registers an interaction component instance when it is rendered.
|
|
26
|
+
* @param {object} config - The interaction configuration object
|
|
27
|
+
* @param {object} questionObj - The live interaction instance with methods like evaluate()
|
|
28
|
+
* @throws {Error} If configuration is invalid or duplicate ID detected
|
|
29
|
+
*/
|
|
30
|
+
register(config, questionObj) {
|
|
31
|
+
if (!config || !config.id) {
|
|
32
|
+
const error = new Error('[InteractionRegistry] Cannot register interaction: configuration or ID is missing.');
|
|
33
|
+
eventBus.emit('interaction:registry:error', {
|
|
34
|
+
domain: 'interaction-registry',
|
|
35
|
+
operation: 'register',
|
|
36
|
+
message: error.message,
|
|
37
|
+
stack: error.stack,
|
|
38
|
+
context: { config }
|
|
39
|
+
});
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.registry.has(config.id)) {
|
|
44
|
+
const error = new Error(`[InteractionRegistry] Interaction with ID "${config.id}" is already registered. Duplicate interaction IDs are not allowed. Each interaction must have a unique ID.`);
|
|
45
|
+
eventBus.emit('interaction:registry:error', {
|
|
46
|
+
domain: 'interaction-registry',
|
|
47
|
+
operation: 'register',
|
|
48
|
+
message: error.message,
|
|
49
|
+
stack: error.stack,
|
|
50
|
+
context: { interactionId: config.id }
|
|
51
|
+
});
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const registration = {
|
|
56
|
+
id: config.id,
|
|
57
|
+
type: config.type,
|
|
58
|
+
description: config.prompt,
|
|
59
|
+
config: config,
|
|
60
|
+
instance: questionObj,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.registry.set(config.id, registration);
|
|
64
|
+
eventBus.emit('interaction:registered', registration);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Retrieves all registered interactions for the current slide.
|
|
69
|
+
* @returns {Array<object>} Array of registered interaction objects with their instances and configs
|
|
70
|
+
*/
|
|
71
|
+
getAll() {
|
|
72
|
+
return Array.from(this.registry.values());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clears the registry. Should be called before rendering a new slide.
|
|
77
|
+
*/
|
|
78
|
+
clear() {
|
|
79
|
+
this.registry.clear();
|
|
80
|
+
this.isReady = false;
|
|
81
|
+
logger.debug('[InteractionRegistry] Registry cleared.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Marks the registry as ready after all interactions have been registered.
|
|
86
|
+
* Emits an event that other managers can listen to.
|
|
87
|
+
*/
|
|
88
|
+
setReady() {
|
|
89
|
+
this.isReady = true;
|
|
90
|
+
eventBus.emit('interaction:registry:ready', this.getAll());
|
|
91
|
+
logger.debug(`[InteractionRegistry] Registry ready. ${this.registry.size} interactions registered.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const instance = new InteractionRegistry();
|
|
96
|
+
export default instance;
|