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,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssessmentActions - Handles all user interactions and business logic.
|
|
3
|
+
*
|
|
4
|
+
* This module orchestrates the assessment flow by responding to user actions
|
|
5
|
+
* (e.g., 'next', 'submit'). It uses the AssessmentState module to manage state
|
|
6
|
+
* and triggers UI updates via the ViewManager.
|
|
7
|
+
*
|
|
8
|
+
* This layer is also responsible for managing global UI state (footer visibility).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { eventBus } from '../core/event-bus.js';
|
|
12
|
+
import * as AppUI from '../app/AppUI.js';
|
|
13
|
+
import { goToSlide } from '../navigation/NavigationActions.js';
|
|
14
|
+
import { getCurrentSlideId, getVisitedSlides } from '../navigation/NavigationState.js';
|
|
15
|
+
import { getSlideById } from '../utilities/course-helpers.js';
|
|
16
|
+
import objectiveManager from '../managers/objective-manager.js';
|
|
17
|
+
import interactionManager from '../managers/interaction-manager.js';
|
|
18
|
+
|
|
19
|
+
import globalStateManager from '../state/index.js';
|
|
20
|
+
import { formatLearnerResponseForScorm } from '../validation/scorm-validators.js';
|
|
21
|
+
import { logger } from '../utilities/logger.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an object containing all action handlers for an assessment.
|
|
25
|
+
* @param {object} stateManager - The assessment-specific state manager.
|
|
26
|
+
* @param {object} uiManager - The assessment-specific UI manager (a view manager instance).
|
|
27
|
+
* @param {Array} questionInstances - An array of all question instances for the assessment.
|
|
28
|
+
* @param {object} config - The assessment's configuration object.
|
|
29
|
+
* @param {object} assessmentUI - The full assessment UI object (for accessing modal methods).
|
|
30
|
+
* @returns {object} An object with an `initialize` method to attach event listeners.
|
|
31
|
+
*/
|
|
32
|
+
export function createAssessmentActions(stateManager, uiManager, questionInstances, config, assessmentUI) {
|
|
33
|
+
// FAIL FAST validation of critical parameters
|
|
34
|
+
if (!config || !config.id) {
|
|
35
|
+
throw new Error('[AssessmentActions] config with id is required');
|
|
36
|
+
}
|
|
37
|
+
if (!assessmentUI) {
|
|
38
|
+
throw new Error(`[AssessmentActions:${config.id}] assessmentUI parameter is required`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { settings } = config;
|
|
42
|
+
// All parameters are immutable for this action instance's lifetime
|
|
43
|
+
|
|
44
|
+
async function _forceSaveCurrentResponse() {
|
|
45
|
+
const currentIndex = stateManager.getCurrentQuestionIndex();
|
|
46
|
+
const questionInstance = questionInstances[currentIndex];
|
|
47
|
+
if (questionInstance && typeof questionInstance.persistToSCORM === 'function') {
|
|
48
|
+
await questionInstance.persistToSCORM();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _manageFooterVisibility(viewName) {
|
|
53
|
+
if (viewName === 'question' || viewName === 'review') {
|
|
54
|
+
AppUI.hideFooter();
|
|
55
|
+
} else {
|
|
56
|
+
AppUI.showFooter();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleStart() {
|
|
61
|
+
const summary = stateManager.getSummary();
|
|
62
|
+
// Summary initialized by Factory on first render
|
|
63
|
+
const attemptNumber = (summary?.attempts || 0) + 1;
|
|
64
|
+
|
|
65
|
+
await stateManager.clearSession();
|
|
66
|
+
await stateManager.setCurrentView('question');
|
|
67
|
+
await stateManager.setCurrentQuestionIndex(0);
|
|
68
|
+
await stateManager.setStartTime(Date.now());
|
|
69
|
+
await stateManager.setSubmitted(false);
|
|
70
|
+
await stateManager.setAttemptNumber(attemptNumber);
|
|
71
|
+
|
|
72
|
+
_manageFooterVisibility('question');
|
|
73
|
+
uiManager.showView('question');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handlePrev() {
|
|
77
|
+
await _forceSaveCurrentResponse();
|
|
78
|
+
|
|
79
|
+
const currentIndex = stateManager.getCurrentQuestionIndex();
|
|
80
|
+
if (currentIndex > 0) {
|
|
81
|
+
await stateManager.setCurrentQuestionIndex(currentIndex - 1);
|
|
82
|
+
uiManager.showView('question');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleNext() {
|
|
87
|
+
await _forceSaveCurrentResponse();
|
|
88
|
+
|
|
89
|
+
const currentIndex = stateManager.getCurrentQuestionIndex();
|
|
90
|
+
const isLastQuestion = currentIndex === config.questions.length - 1;
|
|
91
|
+
|
|
92
|
+
if (isLastQuestion) {
|
|
93
|
+
if (settings.allowReview) {
|
|
94
|
+
await stateManager.updateSession({ reviewReached: true });
|
|
95
|
+
await stateManager.setCurrentView('review');
|
|
96
|
+
_manageFooterVisibility('review');
|
|
97
|
+
uiManager.showView('review');
|
|
98
|
+
} else {
|
|
99
|
+
// FIX: Route through handleSubmit to ensure unanswered checks are performed
|
|
100
|
+
// previously called submitAssessment() directly, bypassing validation
|
|
101
|
+
await handleSubmit();
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
await stateManager.setCurrentQuestionIndex(currentIndex + 1);
|
|
105
|
+
uiManager.showView('question');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleReviewQuestion(event) {
|
|
110
|
+
await _forceSaveCurrentResponse();
|
|
111
|
+
|
|
112
|
+
const index = parseInt(event.target.closest('[data-question-index]').dataset.questionIndex, 10);
|
|
113
|
+
if (isNaN(index)) {
|
|
114
|
+
const errorMessage = `[AssessmentActions:${config.id}] Invalid question index from review screen`;
|
|
115
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'handleReviewQuestion', assessmentId: config.id });
|
|
116
|
+
throw new Error(errorMessage);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await stateManager.setCurrentView('question');
|
|
120
|
+
await stateManager.setCurrentQuestionIndex(index);
|
|
121
|
+
_manageFooterVisibility('question');
|
|
122
|
+
uiManager.showView('question');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function handleBackToQuestions() {
|
|
126
|
+
await _forceSaveCurrentResponse();
|
|
127
|
+
await stateManager.setCurrentView('question');
|
|
128
|
+
_manageFooterVisibility('question');
|
|
129
|
+
uiManager.showView('question');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function handleJumpToReview() {
|
|
133
|
+
await _forceSaveCurrentResponse();
|
|
134
|
+
await stateManager.setCurrentView('review');
|
|
135
|
+
_manageFooterVisibility('review');
|
|
136
|
+
uiManager.showView('review');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function handleSubmit() {
|
|
140
|
+
await _forceSaveCurrentResponse();
|
|
141
|
+
|
|
142
|
+
// Check for unanswered questions before submission
|
|
143
|
+
// Use the same logic as review screen - delegate to interaction metadata
|
|
144
|
+
const session = stateManager.getSession();
|
|
145
|
+
const responses = session?.responses || {};
|
|
146
|
+
const unansweredIndices = [];
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < config.questions.length; i++) {
|
|
149
|
+
const _question = config.questions[i];
|
|
150
|
+
const response = responses[i];
|
|
151
|
+
|
|
152
|
+
// Get metadata from question instance
|
|
153
|
+
const metadata = questionInstances[i].metadata;
|
|
154
|
+
|
|
155
|
+
// Use interaction's isAnswered method - it knows best for its type
|
|
156
|
+
if (!metadata || !metadata.isAnswered(response)) {
|
|
157
|
+
unansweredIndices.push(i);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if unanswered questions exist and how to handle them
|
|
162
|
+
const allowUnanswered = settings.allowUnansweredSubmission === true; // Default false (strict mode)
|
|
163
|
+
|
|
164
|
+
if (unansweredIndices.length > 0) {
|
|
165
|
+
// Show modal: informational if blocked, confirmation if allowed
|
|
166
|
+
assessmentUI.showUnansweredModal(
|
|
167
|
+
unansweredIndices,
|
|
168
|
+
allowUnanswered, // Pass whether submission is allowed
|
|
169
|
+
async () => {
|
|
170
|
+
// User confirmed submission (only called if allowUnanswered is true)
|
|
171
|
+
await submitAssessment();
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Either no unanswered questions, or allowUnansweredSubmission is true - proceed
|
|
178
|
+
await submitAssessment();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleCheck() {
|
|
182
|
+
const currentIndex = stateManager.getCurrentQuestionIndex();
|
|
183
|
+
const questionInstance = questionInstances[currentIndex];
|
|
184
|
+
if (questionInstance && typeof questionInstance.checkAnswer === 'function') {
|
|
185
|
+
questionInstance.checkAnswer();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handleReset() {
|
|
190
|
+
const currentIndex = stateManager.getCurrentQuestionIndex();
|
|
191
|
+
const questionInstance = questionInstances[currentIndex];
|
|
192
|
+
if (questionInstance && typeof questionInstance.reset === 'function') {
|
|
193
|
+
questionInstance.reset();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function handleHint() {
|
|
198
|
+
const currentIndex = stateManager.getCurrentQuestionIndex();
|
|
199
|
+
const questionInstance = questionInstances[currentIndex];
|
|
200
|
+
if (questionInstance && typeof questionInstance.showHint === 'function') {
|
|
201
|
+
questionInstance.showHint();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function handleRetake() {
|
|
206
|
+
// Archive old responses BEFORE clearing session
|
|
207
|
+
const oldSession = stateManager.getSession();
|
|
208
|
+
if (oldSession && oldSession.responses && Object.keys(oldSession.responses).length > 0) {
|
|
209
|
+
await stateManager.archiveDiscardedResponses(config.questions, oldSession.responses);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Reset all question instances to clear their internal state
|
|
213
|
+
questionInstances.forEach(questionInstance => {
|
|
214
|
+
if (questionInstance && typeof questionInstance.reset === 'function') {
|
|
215
|
+
try {
|
|
216
|
+
questionInstance.reset();
|
|
217
|
+
} catch (_e) {
|
|
218
|
+
// Ignore error if container is null (not rendered yet)
|
|
219
|
+
// This happens when retaking from results screen without viewing questions
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Clear session state
|
|
225
|
+
await stateManager.clearSession();
|
|
226
|
+
await stateManager.setCurrentView('intro');
|
|
227
|
+
await stateManager.setCurrentQuestionIndex(0);
|
|
228
|
+
await stateManager.setStartTime(null);
|
|
229
|
+
await stateManager.setSubmitted(false);
|
|
230
|
+
await stateManager.updateSession({ reviewReached: false });
|
|
231
|
+
|
|
232
|
+
// Check if we need to re-randomize questions
|
|
233
|
+
const shouldRandomizeOnRetake = settings.randomizeOnRetake !== false;
|
|
234
|
+
// FIX: Also check randomizeQuestions setting for direct mode (no banks)
|
|
235
|
+
const isRandomized = config.questionBanks || settings.randomizeQuestions;
|
|
236
|
+
|
|
237
|
+
if (shouldRandomizeOnRetake && isRandomized) {
|
|
238
|
+
// Clear selection to force new randomization
|
|
239
|
+
await stateManager.setSelectedQuestions(null);
|
|
240
|
+
|
|
241
|
+
// Navigate back to this slide with refresh flag
|
|
242
|
+
// This will cause slide to create NEW assessment instance
|
|
243
|
+
const currentSlideId = getCurrentSlideId();
|
|
244
|
+
if (currentSlideId) {
|
|
245
|
+
goToSlide(currentSlideId, { refreshAssessment: true });
|
|
246
|
+
return; // Exit - new instance will be created
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Simple reset - same questions
|
|
251
|
+
_manageFooterVisibility('intro');
|
|
252
|
+
uiManager.showView('intro');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function handleRestartCourse() {
|
|
256
|
+
// Show a confirmation modal before restarting.
|
|
257
|
+
// The actual restart logic is handled by a global listener for 'confirm-restart'.
|
|
258
|
+
eventBus.emit('ui:showModal', 'restart');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function _prepareResultsDisplayData(results) {
|
|
262
|
+
const summary = stateManager.getSummary();
|
|
263
|
+
// Summary always exists after submit
|
|
264
|
+
const currentAttempts = summary?.attempts || 0;
|
|
265
|
+
const { attemptsBeforeRemedial, attemptsBeforeRestart, allowRetake } = settings;
|
|
266
|
+
|
|
267
|
+
// Determine action button configuration
|
|
268
|
+
let actionButton = null;
|
|
269
|
+
|
|
270
|
+
if (!results.passed && allowRetake) {
|
|
271
|
+
// Check if remedial content has already been viewed
|
|
272
|
+
// Use NavigationState directly to get visited slides
|
|
273
|
+
const visitedSlides = getVisitedSlides() || [];
|
|
274
|
+
|
|
275
|
+
const hasRemedialSlides = settings.remedialSlideIds && settings.remedialSlideIds.length > 0;
|
|
276
|
+
const remedialViewed = hasRemedialSlides && settings.remedialSlideIds.every(id => visitedSlides.includes(id));
|
|
277
|
+
|
|
278
|
+
if (attemptsBeforeRestart && currentAttempts >= attemptsBeforeRestart) {
|
|
279
|
+
actionButton = {
|
|
280
|
+
type: 'restart',
|
|
281
|
+
action: 'restart-course',
|
|
282
|
+
label: 'Restart Course',
|
|
283
|
+
message: `You have completed ${currentAttempts} attempts. To try again, you must restart the entire course. This will erase all progress.`,
|
|
284
|
+
messageType: 'warning'
|
|
285
|
+
};
|
|
286
|
+
} else if (attemptsBeforeRemedial && currentAttempts >= attemptsBeforeRemedial && !remedialViewed) {
|
|
287
|
+
actionButton = {
|
|
288
|
+
type: 'remedial',
|
|
289
|
+
action: hasRemedialSlides ? 'go-to-remedial' : 'retake',
|
|
290
|
+
label: hasRemedialSlides ? 'Review Content' : 'Retake Assessment',
|
|
291
|
+
message: hasRemedialSlides
|
|
292
|
+
? 'Please review the recommended content before attempting the assessment again. This will help strengthen your understanding of key concepts.'
|
|
293
|
+
: 'Please take time to review the course material before attempting the assessment again.',
|
|
294
|
+
messageType: 'info'
|
|
295
|
+
};
|
|
296
|
+
} else {
|
|
297
|
+
// Standard retake
|
|
298
|
+
let attemptsMessage = null;
|
|
299
|
+
|
|
300
|
+
// Check remedial warning first (if applicable and not passed)
|
|
301
|
+
if (attemptsBeforeRemedial) {
|
|
302
|
+
const remaining = attemptsBeforeRemedial - currentAttempts;
|
|
303
|
+
if (remaining > 0) {
|
|
304
|
+
attemptsMessage = remaining > 1
|
|
305
|
+
? `${remaining} more attempts before review is recommended.`
|
|
306
|
+
: 'Content review will be recommended after your next attempt.';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// If no remedial warning (either not configured, or we passed it and viewed content), check restart warning
|
|
311
|
+
if (!attemptsMessage && attemptsBeforeRestart) {
|
|
312
|
+
const remaining = attemptsBeforeRestart - currentAttempts;
|
|
313
|
+
if (remaining > 0) {
|
|
314
|
+
attemptsMessage = remaining > 1
|
|
315
|
+
? `${remaining} attempts remaining before course restart is required.`
|
|
316
|
+
: 'This is your final attempt before course restart is required.';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
actionButton = {
|
|
321
|
+
type: 'retake',
|
|
322
|
+
action: 'retake',
|
|
323
|
+
label: 'Retake Assessment',
|
|
324
|
+
attemptsMessage
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
...results,
|
|
331
|
+
actionButton,
|
|
332
|
+
currentAttempts
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function handleGoToRemedial() {
|
|
337
|
+
const remedialSlideIds = settings.remedialSlideIds || [];
|
|
338
|
+
if (remedialSlideIds.length === 0) {
|
|
339
|
+
const error = new Error('No remedial slides configured');
|
|
340
|
+
logger.error(error.message, { domain: 'assessment', operation: 'handleGoToRemedial', stack: error.stack, assessmentId: config.id });
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Navigate to first remedial slide
|
|
345
|
+
const firstRemedialSlide = remedialSlideIds[0];
|
|
346
|
+
|
|
347
|
+
// Validate slide exists before navigating
|
|
348
|
+
const slide = await getSlideById(firstRemedialSlide);
|
|
349
|
+
if (!slide) {
|
|
350
|
+
const errorMessage = `[AssessmentActions:${config.id}] Remedial slide '${firstRemedialSlide}' not found in course structure`;
|
|
351
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'handleGoToRemedial', assessmentId: config.id, remedialSlideIds });
|
|
352
|
+
AppUI.showNotification(`Error: Slide '${firstRemedialSlide}' not found`, 'error');
|
|
353
|
+
throw new Error(errorMessage);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
goToSlide(firstRemedialSlide, {
|
|
357
|
+
fromAssessment: config.id,
|
|
358
|
+
remedialReview: true
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function submitAssessment() {
|
|
363
|
+
const session = stateManager.getSession();
|
|
364
|
+
if (!session) {
|
|
365
|
+
const errorMessage = `[AssessmentActions:${config.id}] No session data found, cannot submit`;
|
|
366
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'submitAssessment', assessmentId: config.id });
|
|
367
|
+
throw new Error(errorMessage);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const attemptNumber = stateManager.getAttemptNumber();
|
|
371
|
+
const finalResults = calculateFinalResults(session.responses || {}, attemptNumber);
|
|
372
|
+
|
|
373
|
+
// Calculate time spent
|
|
374
|
+
const startTime = stateManager.getStartTime();
|
|
375
|
+
if (startTime) {
|
|
376
|
+
const endTime = Date.now();
|
|
377
|
+
const durationMs = endTime - startTime;
|
|
378
|
+
const minutes = Math.floor(durationMs / 60000);
|
|
379
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
380
|
+
finalResults.timeSpent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// OPTIMIZATION: Store only summary stats in suspend_data, not full details array
|
|
384
|
+
// The details array can be 2-4KB for large assessments, causing SCORM 409 errors
|
|
385
|
+
// Details are only needed for immediate display; on resume we show summary only
|
|
386
|
+
const resultsSummary = {
|
|
387
|
+
attemptNumber: finalResults.attemptNumber,
|
|
388
|
+
totalQuestions: finalResults.totalQuestions,
|
|
389
|
+
correctCount: finalResults.correctCount,
|
|
390
|
+
scorePercentage: finalResults.scorePercentage,
|
|
391
|
+
passed: finalResults.passed,
|
|
392
|
+
// Store only question IDs and correctness, not full details
|
|
393
|
+
questionResults: finalResults.details.map(d => ({
|
|
394
|
+
id: d.questionId,
|
|
395
|
+
correct: d.correct
|
|
396
|
+
}))
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
await stateManager.updateSummary({
|
|
400
|
+
lastResults: resultsSummary,
|
|
401
|
+
submitted: true,
|
|
402
|
+
attempts: attemptNumber,
|
|
403
|
+
[`attempt_${attemptNumber}`]: resultsSummary,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await stateManager.updateSession({
|
|
407
|
+
submitted: true
|
|
408
|
+
// Don't duplicate results in session - summary has it
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Prepare display data with action buttons and business logic
|
|
412
|
+
// Pass FULL results (with details) for immediate display only
|
|
413
|
+
const displayData = _prepareResultsDisplayData(finalResults);
|
|
414
|
+
|
|
415
|
+
await stateManager.setCurrentView('results');
|
|
416
|
+
_manageFooterVisibility('results');
|
|
417
|
+
uiManager.showView('results', displayData);
|
|
418
|
+
|
|
419
|
+
// Keep full results in memory for this session only (not persisted)
|
|
420
|
+
// This allows detailed review immediately after submission
|
|
421
|
+
|
|
422
|
+
// Automatically set linked objective if configured
|
|
423
|
+
if (config.assessmentObjective) {
|
|
424
|
+
try {
|
|
425
|
+
objectiveManager.setCompletionStatus(config.assessmentObjective, 'completed');
|
|
426
|
+
objectiveManager.setSuccessStatus(config.assessmentObjective, finalResults.passed ? 'passed' : 'failed');
|
|
427
|
+
} catch (error) {
|
|
428
|
+
const errorMessage = `Failed to update objective '${config.assessmentObjective}' after assessment submission`;
|
|
429
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'submitAssessment', stack: error.stack, assessmentId: config.id, objective: config.assessmentObjective });
|
|
430
|
+
throw new Error(errorMessage);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Record each question response to cmi.interactions for LMS reporting
|
|
435
|
+
// This is separate from suspend_data persistence - purely for analytics/audit
|
|
436
|
+
_recordAssessmentInteractionsToCMI(finalResults, attemptNumber);
|
|
437
|
+
|
|
438
|
+
// Emit event for ScoreManager and other systems BEFORE flush
|
|
439
|
+
// ScoreManager listens here and calls reportScore() synchronously
|
|
440
|
+
eventBus.emit('assessment:submitted', {
|
|
441
|
+
assessmentId: config.id,
|
|
442
|
+
results: finalResults
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Flush critical assessment data + score to LMS immediately
|
|
446
|
+
// Assessment submission is a critical action - don't rely on debounce
|
|
447
|
+
await globalStateManager.flush();
|
|
448
|
+
|
|
449
|
+
if (typeof config.onComplete === 'function') {
|
|
450
|
+
config.onComplete(finalResults);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function calculateFinalResults(responses, attemptNumber) {
|
|
455
|
+
const totalQuestions = config.questions.length;
|
|
456
|
+
let achievedScore = 0;
|
|
457
|
+
let totalPossibleScore = 0;
|
|
458
|
+
|
|
459
|
+
const details = config.questions.map((q, i) => {
|
|
460
|
+
const questionInstance = questionInstances[i];
|
|
461
|
+
const response = responses[i];
|
|
462
|
+
const weight = q.weight;
|
|
463
|
+
totalPossibleScore += weight;
|
|
464
|
+
|
|
465
|
+
// Handle missing responses - treat as incorrect
|
|
466
|
+
let evaluation;
|
|
467
|
+
if (response === undefined || response === null) {
|
|
468
|
+
// Unanswered question - mark as incorrect
|
|
469
|
+
evaluation = {
|
|
470
|
+
correct: false,
|
|
471
|
+
score: 0,
|
|
472
|
+
feedback: 'Question was not answered'
|
|
473
|
+
};
|
|
474
|
+
} else {
|
|
475
|
+
evaluation = questionInstance.evaluate(response);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!evaluation || typeof evaluation.correct !== 'boolean') {
|
|
479
|
+
const error = new Error(`Question ${i + 1} (${q.id}) evaluate() returned invalid result`);
|
|
480
|
+
logger.error(error.message, { domain: 'assessment', operation: 'calculateFinalResults', stack: error.stack, assessmentId: config.id, questionIndex: i, questionId: q.id });
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const isCorrect = evaluation.correct;
|
|
485
|
+
|
|
486
|
+
if (isCorrect) {
|
|
487
|
+
achievedScore += weight;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Include bank metadata if present
|
|
491
|
+
const detail = {
|
|
492
|
+
questionIndex: i,
|
|
493
|
+
questionId: q.id,
|
|
494
|
+
correct: isCorrect,
|
|
495
|
+
response: response,
|
|
496
|
+
weight: weight,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
if (q._meta) {
|
|
500
|
+
detail.bankId = q._meta.bankId;
|
|
501
|
+
detail.originalIndex = q._meta.originalIndex;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return detail;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const scorePercentage = (totalPossibleScore > 0) ? (achievedScore / totalPossibleScore) * 100 : 0;
|
|
508
|
+
|
|
509
|
+
// Check for LMS-provided masteryScore override (cmi5 launch data)
|
|
510
|
+
// masteryScore is 0-1 scaled in cmi5 spec, convert to percentage
|
|
511
|
+
const launchData = globalStateManager.getLaunchData();
|
|
512
|
+
const effectivePassingScore = (launchData?.masteryScore !== null && launchData?.masteryScore !== undefined)
|
|
513
|
+
? launchData.masteryScore * 100
|
|
514
|
+
: (settings.passingScore || 0);
|
|
515
|
+
|
|
516
|
+
const passed = scorePercentage >= effectivePassingScore;
|
|
517
|
+
|
|
518
|
+
const correctCount = details.filter(d => d.correct).length;
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
attemptNumber,
|
|
522
|
+
totalQuestions,
|
|
523
|
+
correctCount,
|
|
524
|
+
scorePercentage,
|
|
525
|
+
passed,
|
|
526
|
+
details,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Records assessment question interactions to CMI for LMS reporting.
|
|
532
|
+
* Called after assessment submission to append each question as a CMI interaction.
|
|
533
|
+
* Formats learner_response according to SCORM 2004 4th Edition requirements.
|
|
534
|
+
* @param {Object} finalResults - The calculated results from calculateFinalResults
|
|
535
|
+
* @param {number} attemptNumber - Current attempt number for ID uniqueness
|
|
536
|
+
*/
|
|
537
|
+
function _recordAssessmentInteractionsToCMI(finalResults, attemptNumber) {
|
|
538
|
+
if (!interactionManager || typeof interactionManager.record !== 'function') {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const questions = config.questions || [];
|
|
543
|
+
|
|
544
|
+
finalResults.details.forEach((detail, i) => {
|
|
545
|
+
const question = questions[i];
|
|
546
|
+
if (!question) return;
|
|
547
|
+
|
|
548
|
+
const questionInstance = questionInstances[i];
|
|
549
|
+
const scormType = questionInstance?.metadata?.scormType || 'other';
|
|
550
|
+
|
|
551
|
+
// Format response according to SCORM 2004 requirements
|
|
552
|
+
const formattedResponse = formatLearnerResponseForScorm(scormType, detail.response);
|
|
553
|
+
|
|
554
|
+
// Build unique ID: assessmentId_questionId_attempt-N
|
|
555
|
+
const interactionId = `${config.id}_${detail.questionId}_attempt-${attemptNumber}`;
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
interactionManager.record({
|
|
559
|
+
id: interactionId,
|
|
560
|
+
type: scormType,
|
|
561
|
+
learner_response: formattedResponse,
|
|
562
|
+
result: detail.correct ? 'correct' : 'incorrect',
|
|
563
|
+
objectives: config.assessmentObjective ? [config.assessmentObjective] : undefined,
|
|
564
|
+
});
|
|
565
|
+
} catch (err) {
|
|
566
|
+
logger.error(`Failed to record assessment interaction to CMI: ${err.message}`, {
|
|
567
|
+
domain: 'assessment', operation: 'recordInteractionsToCMI',
|
|
568
|
+
assessmentId: config.id, questionId: detail.questionId
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function initialize(container) {
|
|
575
|
+
container.addEventListener('click', async (event) => {
|
|
576
|
+
const target = event.target.closest('[data-action]');
|
|
577
|
+
if (!target) return;
|
|
578
|
+
|
|
579
|
+
const action = target.dataset.action;
|
|
580
|
+
const actions = {
|
|
581
|
+
'start': handleStart,
|
|
582
|
+
'prev': handlePrev,
|
|
583
|
+
'next': handleNext,
|
|
584
|
+
'review-question': handleReviewQuestion,
|
|
585
|
+
'back-to-questions': handleBackToQuestions,
|
|
586
|
+
'jump-to-review': handleJumpToReview,
|
|
587
|
+
'submit': handleSubmit,
|
|
588
|
+
'check': handleCheck,
|
|
589
|
+
'reset': handleReset,
|
|
590
|
+
'hint': handleHint,
|
|
591
|
+
'retake': handleRetake,
|
|
592
|
+
'restart-course': handleRestartCourse,
|
|
593
|
+
'go-to-remedial': handleGoToRemedial,
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
if (actions[action]) {
|
|
597
|
+
event.preventDefault();
|
|
598
|
+
target.disabled = true;
|
|
599
|
+
try {
|
|
600
|
+
await actions[action](event);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
const errorMessage = `[AssessmentActions:${config.id}] Error in action '${action}': ${error.message}`;
|
|
603
|
+
logger.error(errorMessage, { domain: 'assessment', operation: action, assessmentId: config.id, stack: error.stack });
|
|
604
|
+
throw error;
|
|
605
|
+
} finally {
|
|
606
|
+
target.disabled = false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
initialize,
|
|
614
|
+
};
|
|
615
|
+
}
|