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,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AssessmentFactory.js
|
|
3
|
+
* @description Internal factory for creating assessment instances.
|
|
4
|
+
* Encapsulates the State-UI-Actions wiring pattern.
|
|
5
|
+
*
|
|
6
|
+
* This file should NOT be imported directly by course authors.
|
|
7
|
+
* Use the public API in assessment-manager.js instead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { eventBus } from '../core/event-bus.js';
|
|
11
|
+
import stateManager from '../state/index.js';
|
|
12
|
+
import { createAssessmentState } from './AssessmentState.js';
|
|
13
|
+
import { createAssessmentUI } from './AssessmentUI.js';
|
|
14
|
+
import { createAssessmentActions } from './AssessmentActions.js';
|
|
15
|
+
import { shuffleArray } from '../utilities/utilities.js';
|
|
16
|
+
import * as AppUI from '../app/AppUI.js';
|
|
17
|
+
import { getVisitedSlides } from '../navigation/NavigationState.js';
|
|
18
|
+
|
|
19
|
+
// Import from the central interaction type catalog
|
|
20
|
+
// This provides unified access to built-in and custom interaction types
|
|
21
|
+
import { getCreator, getMetadata, isRegistered } from '../core/interaction-catalog.js';
|
|
22
|
+
import { logger } from '../utilities/logger.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get interaction type info from the registry.
|
|
26
|
+
* Returns { creator, metadata } or null if type is not registered.
|
|
27
|
+
* @param {string} type - Interaction type name
|
|
28
|
+
* @returns {{ creator: function, metadata: object }|null}
|
|
29
|
+
*/
|
|
30
|
+
function getInteractionTypeInfo(type) {
|
|
31
|
+
if (!isRegistered(type)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
creator: getCreator(type),
|
|
36
|
+
metadata: getMetadata(type)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Selects questions from question banks according to configuration.
|
|
42
|
+
* @private
|
|
43
|
+
* @param {Array} questionBanks - Array of bank configurations
|
|
44
|
+
* @returns {Array} Selected questions with metadata
|
|
45
|
+
*/
|
|
46
|
+
function _selectQuestionsFromBanks(questionBanks) {
|
|
47
|
+
const selectedQuestions = [];
|
|
48
|
+
|
|
49
|
+
questionBanks.forEach(bank => {
|
|
50
|
+
const { id: bankId, questions, selectCount } = bank;
|
|
51
|
+
|
|
52
|
+
// Handle 'all' case - select all questions without randomization
|
|
53
|
+
if (selectCount === 'all') {
|
|
54
|
+
questions.forEach((q, idx) => {
|
|
55
|
+
selectedQuestions.push({
|
|
56
|
+
...q,
|
|
57
|
+
_meta: {
|
|
58
|
+
bankId,
|
|
59
|
+
originalIndex: idx
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Randomize and select N questions
|
|
67
|
+
const shuffled = shuffleArray(questions);
|
|
68
|
+
const selected = shuffled.slice(0, selectCount);
|
|
69
|
+
|
|
70
|
+
selected.forEach((q, _idx) => {
|
|
71
|
+
const originalIndex = questions.indexOf(q);
|
|
72
|
+
selectedQuestions.push({
|
|
73
|
+
...q,
|
|
74
|
+
_meta: {
|
|
75
|
+
bankId,
|
|
76
|
+
originalIndex
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return selectedQuestions;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Required interface for all question instances.
|
|
87
|
+
* This documents the contract that interaction components must fulfill.
|
|
88
|
+
*/
|
|
89
|
+
const REQUIRED_METHODS = {
|
|
90
|
+
'render': 'function', // render(container) - Renders question to DOM
|
|
91
|
+
'getResponse': 'function', // getResponse() - Returns current user response
|
|
92
|
+
'setResponse': 'function', // setResponse(value) - Sets response programmatically
|
|
93
|
+
'evaluate': 'function', // evaluate(response) - Returns {correct: boolean}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Optional methods that interaction components may implement:
|
|
98
|
+
* - reset(): function - Clears user input
|
|
99
|
+
* - checkAnswer(): function - Shows immediate feedback
|
|
100
|
+
* - showHint(): function - Displays hint to user
|
|
101
|
+
* - getCorrectAnswer(): function - Returns the correct answer for automation/review
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validates that a question instance implements the required interface.
|
|
106
|
+
* @private
|
|
107
|
+
* @throws {Error} If any required method is missing
|
|
108
|
+
*/
|
|
109
|
+
function _validateQuestionInstance(instance, questionConfig, assessmentId) {
|
|
110
|
+
const errors = [];
|
|
111
|
+
|
|
112
|
+
for (const [methodName, expectedType] of Object.entries(REQUIRED_METHODS)) {
|
|
113
|
+
if (typeof instance[methodName] !== expectedType) {
|
|
114
|
+
errors.push(`Missing required method '${methodName}' (expected ${expectedType}, got ${typeof instance[methodName]})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (errors.length > 0) {
|
|
119
|
+
const errorMessage = `[AssessmentFactory:${assessmentId}] Question type '${questionConfig.type}' (ID: ${questionConfig.id}) has invalid interface:\n - ${errors.join('\n - ')}`;
|
|
120
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'validateQuestionInstance', assessmentId, questionType: questionConfig.type, questionId: questionConfig.id });
|
|
121
|
+
throw new Error(errorMessage);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates a question instance with SCORM persistence methods.
|
|
127
|
+
* Attaches metadata to the instance for type-specific formatting.
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
function _createQuestionInstance(questionConfig, index, assessmentState, assessmentId) {
|
|
131
|
+
const type = questionConfig.type;
|
|
132
|
+
const typeInfo = getInteractionTypeInfo(type);
|
|
133
|
+
|
|
134
|
+
// This should never happen because validation happens first, but guard anyway
|
|
135
|
+
if (!typeInfo) {
|
|
136
|
+
const errorMessage = `[AssessmentFactory:${assessmentId}] Unknown question type: ${type}. Ensure it is registered in the interaction registry.`;
|
|
137
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'createQuestionInstance', assessmentId, questionType: type });
|
|
138
|
+
throw new Error(errorMessage);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create base instance
|
|
142
|
+
// Assessments use controlled mode: no Check Answer buttons, centralized evaluation/SCORM recording
|
|
143
|
+
// For immediate feedback, use standalone interactions in regular slides instead
|
|
144
|
+
const baseInstance = typeInfo.creator({ ...questionConfig, controlled: true });
|
|
145
|
+
|
|
146
|
+
// Validate interface
|
|
147
|
+
_validateQuestionInstance(baseInstance, questionConfig, assessmentId);
|
|
148
|
+
|
|
149
|
+
// Wrap with SCORM persistence layer and attach metadata
|
|
150
|
+
return {
|
|
151
|
+
...baseInstance,
|
|
152
|
+
metadata: typeInfo.metadata, // Attach metadata for AssessmentUI/Actions to use
|
|
153
|
+
async persistToSCORM() {
|
|
154
|
+
const response = baseInstance.getResponse();
|
|
155
|
+
await assessmentState.saveResponse(index, response);
|
|
156
|
+
},
|
|
157
|
+
restoreFromSCORM() {
|
|
158
|
+
const savedResponse = assessmentState.getResponse(index);
|
|
159
|
+
if (savedResponse !== null && savedResponse !== undefined) {
|
|
160
|
+
baseInstance.setResponse(savedResponse);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Creates a complete assessment instance with all wiring.
|
|
168
|
+
* Follows the State-UI-Actions pattern.
|
|
169
|
+
*
|
|
170
|
+
* @param {Object} config - Assessment configuration (validated by runtime-linter in dev mode)
|
|
171
|
+
* @returns {Object} Assessment instance with render() method
|
|
172
|
+
* @throws {Error} If critical configuration properties are missing
|
|
173
|
+
*/
|
|
174
|
+
export function createAssessmentInstance(config) {
|
|
175
|
+
// Config validation done by runtime-linter in dev mode
|
|
176
|
+
// Runtime: only validate that required properties exist (quick check)
|
|
177
|
+
if (!config.id) {
|
|
178
|
+
const error = new Error('Assessment ID required');
|
|
179
|
+
logger.error(error.message, { domain: 'assessment', operation: 'createAssessmentInstance' });
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
if (!config.questions && !config.questionBanks) {
|
|
183
|
+
const error = new Error(`Assessment '${config.id}' needs questions or questionBanks`);
|
|
184
|
+
logger.error(error.message, { domain: 'assessment', operation: 'createAssessmentInstance', assessmentId: config.id });
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const assessmentId = config.id;
|
|
189
|
+
|
|
190
|
+
// Initialize State layer
|
|
191
|
+
const assessmentState = createAssessmentState(config, stateManager);
|
|
192
|
+
|
|
193
|
+
// Determine active questions (from banks or direct config)
|
|
194
|
+
let activeQuestions = config.questions || [];
|
|
195
|
+
let questionInstances = [];
|
|
196
|
+
let finalConfig = config;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Resolves which questions to use for this assessment session.
|
|
200
|
+
* Priority:
|
|
201
|
+
* 1. Persisted selected questions (resume in-progress session)
|
|
202
|
+
* 2. Question banks (new session with randomization)
|
|
203
|
+
* 3. Direct questions array (legacy/simple mode)
|
|
204
|
+
*
|
|
205
|
+
* In dev mode, throws if persisted question IDs don't match current config.
|
|
206
|
+
* In prod mode, filters out missing questions and continues gracefully.
|
|
207
|
+
*/
|
|
208
|
+
function _resolveActiveQuestions() {
|
|
209
|
+
const savedQuestionIds = assessmentState.getSelectedQuestions();
|
|
210
|
+
|
|
211
|
+
if (savedQuestionIds && Array.isArray(savedQuestionIds)) {
|
|
212
|
+
// OPTIMIZATION FIX: Reconstruct full question objects from saved IDs
|
|
213
|
+
// This is needed because we now only store IDs to save suspend_data space
|
|
214
|
+
const allQuestions = _getAllAvailableQuestions();
|
|
215
|
+
const questionMap = new Map(allQuestions.map(q => [q.id, q]));
|
|
216
|
+
|
|
217
|
+
// Check for missing questions (course structure changed)
|
|
218
|
+
const missingIds = savedQuestionIds.filter(id => !questionMap.has(id));
|
|
219
|
+
|
|
220
|
+
if (missingIds.length > 0) {
|
|
221
|
+
const errorMessage = `Assessment "${assessmentId}" has ${missingIds.length} stored question ID(s) that no longer exist: ${missingIds.join(', ')}. ` +
|
|
222
|
+
'The assessment questions have changed since the learner started.';
|
|
223
|
+
|
|
224
|
+
if (import.meta.env.DEV) {
|
|
225
|
+
// Dev mode: FAIL FAST to help developers catch stale data issues
|
|
226
|
+
const error = new Error(`[AssessmentFactory] ${errorMessage}`);
|
|
227
|
+
logger.error(errorMessage, {
|
|
228
|
+
domain: 'assessment', operation: 'resolve-questions', stack: error.stack,
|
|
229
|
+
assessmentId, missingIds, storedIds: savedQuestionIds,
|
|
230
|
+
availableIds: Array.from(questionMap.keys())
|
|
231
|
+
});
|
|
232
|
+
throw error;
|
|
233
|
+
} else {
|
|
234
|
+
// Production mode: Log warning and filter out missing questions
|
|
235
|
+
// Also emit event so course can potentially track this
|
|
236
|
+
logger.warn(`[AssessmentFactory] ${errorMessage} Continuing with available questions.`);
|
|
237
|
+
eventBus.emit('state:recovered', {
|
|
238
|
+
domain: 'assessment',
|
|
239
|
+
message: errorMessage,
|
|
240
|
+
context: { assessmentId, missingIds },
|
|
241
|
+
action: 'filtered_missing_questions'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return savedQuestionIds
|
|
247
|
+
.map(id => questionMap.get(id))
|
|
248
|
+
.filter(q => q !== undefined); // Filter out any missing questions
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (config.questionBanks && Array.isArray(config.questionBanks) && config.questionBanks.length > 0) {
|
|
252
|
+
// New session with banks: select and optionally randomize
|
|
253
|
+
let selected = _selectQuestionsFromBanks(config.questionBanks);
|
|
254
|
+
|
|
255
|
+
if (config.settings?.randomizeQuestions) {
|
|
256
|
+
selected = shuffleArray(selected);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return selected;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Direct mode: use questions array as-is
|
|
263
|
+
if (config.settings?.randomizeQuestions && Array.isArray(config.questions)) {
|
|
264
|
+
return shuffleArray(config.questions);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return config.questions || [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Helper to get all available questions from either banks or direct questions array
|
|
272
|
+
*/
|
|
273
|
+
function _getAllAvailableQuestions() {
|
|
274
|
+
if (config.questionBanks && Array.isArray(config.questionBanks)) {
|
|
275
|
+
return config.questionBanks.flatMap(bank => bank.questions || []);
|
|
276
|
+
}
|
|
277
|
+
return config.questions || [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Create question instances with SCORM wrappers
|
|
281
|
+
function _initializeQuestionInstances(questions) {
|
|
282
|
+
return questions.map((q, i) =>
|
|
283
|
+
_createQuestionInstance(q, i, assessmentState, assessmentId)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Track view manager and actions (initialized on render)
|
|
288
|
+
let viewManager = null;
|
|
289
|
+
let assessmentActions = null;
|
|
290
|
+
let _currentContainer = null;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Renders the assessment to the target container.
|
|
294
|
+
* @param {HTMLElement} targetContainer - DOM element to render into
|
|
295
|
+
* @param {Object} context - Context from ViewManager (contains fromSlide)
|
|
296
|
+
*/
|
|
297
|
+
function render(targetContainer, context = {}) {
|
|
298
|
+
if (!targetContainer) {
|
|
299
|
+
const errorMessage = `[AssessmentFactory:${assessmentId}] targetContainer is required for render()`;
|
|
300
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'render', assessmentId });
|
|
301
|
+
throw new Error(errorMessage);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
targetContainer.className = 'assessment-navigator';
|
|
305
|
+
_currentContainer = targetContainer;
|
|
306
|
+
|
|
307
|
+
// Load persisted state
|
|
308
|
+
let summary = assessmentState.getSummary();
|
|
309
|
+
const session = assessmentState.getSession();
|
|
310
|
+
|
|
311
|
+
// Initialize summary if this is first time rendering (fire-and-forget)
|
|
312
|
+
if (!summary) {
|
|
313
|
+
assessmentState.updateSummary({ attempts: 0 }).catch(error => {
|
|
314
|
+
logger.error(`Failed to initialize summary: ${error.message}`, { domain: 'assessment', operation: 'initialize-summary', assessmentId });
|
|
315
|
+
});
|
|
316
|
+
// Use optimistic local value until persist completes
|
|
317
|
+
summary = { attempts: 0 };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Resolve active questions for this session
|
|
321
|
+
activeQuestions = _resolveActiveQuestions();
|
|
322
|
+
|
|
323
|
+
// Persist selection if this is a new bank-based session (fire-and-forget)
|
|
324
|
+
if (config.questionBanks && !session?.selectedQuestions) {
|
|
325
|
+
assessmentState.setSelectedQuestions(activeQuestions).catch(error => {
|
|
326
|
+
logger.error(`Failed to persist selected questions: ${error.message}`, { domain: 'assessment', operation: 'persist-selected-questions', assessmentId });
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Create question instances based on active questions
|
|
331
|
+
questionInstances = _initializeQuestionInstances(activeQuestions);
|
|
332
|
+
|
|
333
|
+
// Update config with active questions for UI/Actions layers
|
|
334
|
+
finalConfig = { ...config, questions: activeQuestions };
|
|
335
|
+
|
|
336
|
+
// Initialize UI and Actions with immutable references
|
|
337
|
+
// If questions need to change (retake with randomization), slide creates new assessment instance
|
|
338
|
+
const assessmentUIWithQuestions = createAssessmentUI(finalConfig, assessmentState, questionInstances);
|
|
339
|
+
viewManager = assessmentUIWithQuestions.initialize(targetContainer);
|
|
340
|
+
assessmentActions = createAssessmentActions(
|
|
341
|
+
assessmentState,
|
|
342
|
+
viewManager,
|
|
343
|
+
questionInstances,
|
|
344
|
+
finalConfig,
|
|
345
|
+
assessmentUIWithQuestions // Pass full UI object for modal access
|
|
346
|
+
);
|
|
347
|
+
assessmentActions.initialize(targetContainer);
|
|
348
|
+
|
|
349
|
+
// Check for auto-restart from remedial
|
|
350
|
+
const fromSlide = context.fromSlide;
|
|
351
|
+
const remedialSlideIds = config.settings?.remedialSlideIds || [];
|
|
352
|
+
const isReturningFromRemedial = fromSlide && remedialSlideIds.includes(fromSlide);
|
|
353
|
+
|
|
354
|
+
if (isReturningFromRemedial && summary?.lastResults && !summary.lastResults.passed) {
|
|
355
|
+
// Clear session to force a fresh start (Intro screen)
|
|
356
|
+
// We don't increment attempts here; handleStart does that when user clicks Start
|
|
357
|
+
assessmentState.clearSession().then(() => {
|
|
358
|
+
AppUI.showFooter();
|
|
359
|
+
viewManager.showView('intro');
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Determine initial view and set footer visibility
|
|
365
|
+
const savedResults = summary?.lastResults;
|
|
366
|
+
if (savedResults) {
|
|
367
|
+
// Show completed results (summary only - details not persisted to save space)
|
|
368
|
+
// Prepare display data since we don't have full details anymore
|
|
369
|
+
const displayData = _prepareResumedResultsDisplayData(savedResults, finalConfig);
|
|
370
|
+
AppUI.showFooter();
|
|
371
|
+
viewManager.showView('results', displayData);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (session && !session.submitted) {
|
|
376
|
+
// Restore in-progress session
|
|
377
|
+
const currentView = session.currentView || 'intro';
|
|
378
|
+
if (currentView === 'question' || currentView === 'review') {
|
|
379
|
+
AppUI.hideFooter();
|
|
380
|
+
} else {
|
|
381
|
+
AppUI.showFooter();
|
|
382
|
+
}
|
|
383
|
+
viewManager.showView(currentView);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Start fresh - intro view shows footer
|
|
388
|
+
AppUI.showFooter();
|
|
389
|
+
viewManager.showView('intro');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Prepares display data for results screen when resuming from saved summary.
|
|
394
|
+
* Since we only persist summary stats (not full details), reconstruct what's needed.
|
|
395
|
+
* @param {Object} resultsSummary - Minimal summary saved in suspend_data
|
|
396
|
+
* @param {Object} config - Assessment configuration
|
|
397
|
+
* @returns {Object} Display data for results screen
|
|
398
|
+
*/
|
|
399
|
+
function _prepareResumedResultsDisplayData(resultsSummary, config) {
|
|
400
|
+
const summary = assessmentState.getSummary();
|
|
401
|
+
const currentAttempts = summary?.attempts || 0;
|
|
402
|
+
const { attemptsBeforeRemedial, attemptsBeforeRestart, allowRetake, settings: _settings } = config;
|
|
403
|
+
|
|
404
|
+
// Reconstruct minimal results object for display
|
|
405
|
+
// Note: detailed question-by-question review not available on resume (only summary)
|
|
406
|
+
const resultsForDisplay = {
|
|
407
|
+
attemptNumber: resultsSummary.attemptNumber,
|
|
408
|
+
totalQuestions: resultsSummary.totalQuestions,
|
|
409
|
+
correctCount: resultsSummary.correctCount,
|
|
410
|
+
scorePercentage: resultsSummary.scorePercentage,
|
|
411
|
+
passed: resultsSummary.passed,
|
|
412
|
+
details: null // Not available after resume - only summary stats
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Determine action button (same logic as in AssessmentActions)
|
|
416
|
+
let actionButton = null;
|
|
417
|
+
|
|
418
|
+
if (attemptsBeforeRestart && currentAttempts >= attemptsBeforeRestart) {
|
|
419
|
+
actionButton = {
|
|
420
|
+
type: 'restart',
|
|
421
|
+
action: 'restart-course',
|
|
422
|
+
label: 'Restart Course',
|
|
423
|
+
message: `You've completed ${currentAttempts} attempt(s). You must restart the course to try again.`,
|
|
424
|
+
messageType: 'error'
|
|
425
|
+
};
|
|
426
|
+
} else {
|
|
427
|
+
// Check remedial logic
|
|
428
|
+
let showRemedial = false;
|
|
429
|
+
if (attemptsBeforeRemedial && currentAttempts >= attemptsBeforeRemedial && !resultsSummary.passed) {
|
|
430
|
+
const remedialSlideIds = config.settings?.remedialSlideIds || (config.settings?.remedialSlideId ? [config.settings.remedialSlideId] : []);
|
|
431
|
+
|
|
432
|
+
// Check if remedial content has already been viewed
|
|
433
|
+
const visitedSlides = getVisitedSlides() || [];
|
|
434
|
+
|
|
435
|
+
const remedialViewed = remedialSlideIds.length > 0 && remedialSlideIds.every(id => visitedSlides.includes(id));
|
|
436
|
+
|
|
437
|
+
if (remedialSlideIds.length > 0 && !remedialViewed) {
|
|
438
|
+
showRemedial = true;
|
|
439
|
+
actionButton = {
|
|
440
|
+
type: 'remedial',
|
|
441
|
+
action: 'go-to-remedial',
|
|
442
|
+
label: 'Review Content',
|
|
443
|
+
message: `Please review the content before attempting again (Attempt ${currentAttempts}/${attemptsBeforeRestart || '∞'}).`,
|
|
444
|
+
messageType: 'warning',
|
|
445
|
+
attemptsMessage: `Attempts: ${currentAttempts}${attemptsBeforeRestart ? `/${attemptsBeforeRestart}` : ''}`
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Fallback to standard retake if remedial not shown
|
|
451
|
+
if (!showRemedial && allowRetake !== false && !resultsSummary.passed) {
|
|
452
|
+
actionButton = {
|
|
453
|
+
type: 'retake',
|
|
454
|
+
action: 'retake',
|
|
455
|
+
label: 'Retake Assessment',
|
|
456
|
+
attemptsMessage: `Attempts: ${currentAttempts}${attemptsBeforeRestart ? `/${attemptsBeforeRestart}` : ''}`
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
...resultsForDisplay,
|
|
463
|
+
actionButton,
|
|
464
|
+
timeSpent: null // Time data not preserved after session ends
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
render,
|
|
470
|
+
};
|
|
471
|
+
}
|