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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssessmentUI - Manages all DOM interactions for an assessment.
|
|
3
|
+
*
|
|
4
|
+
* This module is responsible for rendering all views of the assessment,
|
|
5
|
+
* including the intro screen, questions, review screen, and results. It uses
|
|
6
|
+
* the ViewManager to efficiently switch between these views without re-rendering
|
|
7
|
+
* the entire DOM.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createViewManager } from '../utilities/view-manager.js';
|
|
11
|
+
import { iconManager } from '../utilities/icons.js';
|
|
12
|
+
import * as Modal from '../components/ui-components/modal.js';
|
|
13
|
+
import { logger } from '../utilities/logger.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shows a modal when there are unanswered questions and submission is attempted.
|
|
17
|
+
* Modal behavior adapts based on whether submission is allowed:
|
|
18
|
+
* - If blocked: Shows informational message with only "Go Back" button
|
|
19
|
+
* - If allowed: Shows confirmation with "Go Back" and "Submit Anyway" buttons
|
|
20
|
+
*
|
|
21
|
+
* @param {Array<number>} unansweredIndices - Array of 0-based question indices that are unanswered
|
|
22
|
+
* @param {boolean} allowSubmission - Whether submission with unanswered questions is allowed
|
|
23
|
+
* @param {Function} onConfirm - Callback to execute if user confirms submission (only called if allowSubmission is true)
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
function showUnansweredModal(unansweredIndices, allowSubmission, onConfirm) {
|
|
27
|
+
const count = unansweredIndices.length;
|
|
28
|
+
const questionText = count === 1 ? 'question' : 'questions';
|
|
29
|
+
const questionNumbers = unansweredIndices.map(i => i + 1).join(', ');
|
|
30
|
+
|
|
31
|
+
let clickHandler = null;
|
|
32
|
+
|
|
33
|
+
// Adapt modal content based on whether submission is allowed
|
|
34
|
+
const modalConfig = allowSubmission ? {
|
|
35
|
+
// Confirmation mode: User can choose to submit with unanswered questions
|
|
36
|
+
title: 'Unanswered Questions',
|
|
37
|
+
body: `
|
|
38
|
+
<p class="mb-3">You have <strong>${count} unanswered ${questionText}</strong> (${questionNumbers}).</p>
|
|
39
|
+
<p class="mb-3">Unanswered questions will be marked as incorrect.</p>
|
|
40
|
+
<p class="font-bold">Do you want to submit anyway?</p>
|
|
41
|
+
`,
|
|
42
|
+
footer: `
|
|
43
|
+
<button class="btn btn-secondary" data-action="dismiss-unanswered-modal" data-testid="modal-unanswered-cancel">Go Back</button>
|
|
44
|
+
<button class="btn btn-primary" data-action="confirm-unanswered-submit" data-testid="modal-unanswered-confirm">Submit Anyway</button>
|
|
45
|
+
`
|
|
46
|
+
} : {
|
|
47
|
+
// Blocked mode: Submission not allowed, must answer all questions
|
|
48
|
+
title: 'All Questions Required',
|
|
49
|
+
body: `
|
|
50
|
+
<p class="mb-3">You must answer all questions before submitting.</p>
|
|
51
|
+
<p class="mb-3">You have <strong>${count} unanswered ${questionText}</strong> (${questionNumbers}).</p>
|
|
52
|
+
<p class="font-bold">Please go back and complete all questions.</p>
|
|
53
|
+
`,
|
|
54
|
+
footer: `
|
|
55
|
+
<button class="btn btn-primary" data-action="dismiss-unanswered-modal" data-testid="modal-unanswered-dismiss">Go Back</button>
|
|
56
|
+
`
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Show modal using singleton API
|
|
60
|
+
Modal.show({
|
|
61
|
+
...modalConfig,
|
|
62
|
+
config: {
|
|
63
|
+
closeOnBackdrop: true,
|
|
64
|
+
closeOnEscape: true,
|
|
65
|
+
},
|
|
66
|
+
onOpen: () => {
|
|
67
|
+
// Set up click handler after modal is rendered
|
|
68
|
+
const modalElement = document.getElementById('global-modal');
|
|
69
|
+
if (!modalElement) return;
|
|
70
|
+
|
|
71
|
+
clickHandler = (event) => {
|
|
72
|
+
const target = event.target.closest('[data-action]');
|
|
73
|
+
if (!target) return;
|
|
74
|
+
|
|
75
|
+
const action = target.dataset.action;
|
|
76
|
+
|
|
77
|
+
if (action === 'confirm-unanswered-submit' && allowSubmission) {
|
|
78
|
+
Modal.hide();
|
|
79
|
+
onConfirm();
|
|
80
|
+
} else if (action === 'dismiss-unanswered-modal') {
|
|
81
|
+
Modal.hide();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
modalElement.addEventListener('click', clickHandler);
|
|
86
|
+
},
|
|
87
|
+
onClose: () => {
|
|
88
|
+
// Cleanup listener when modal closes
|
|
89
|
+
const modalElement = document.getElementById('global-modal');
|
|
90
|
+
if (modalElement && clickHandler) {
|
|
91
|
+
modalElement.removeEventListener('click', clickHandler);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createAssessmentUI(config, stateManager, questionInstances) {
|
|
98
|
+
// FAIL FAST validation of critical parameters
|
|
99
|
+
if (!config || !config.id) {
|
|
100
|
+
throw new Error('[AssessmentUI] config with id is required');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { settings, review, resultsDisplay } = config;
|
|
104
|
+
|
|
105
|
+
function renderIntroScreen() {
|
|
106
|
+
const summary = stateManager.getSummary();
|
|
107
|
+
// Summary should always exist after Factory initialization
|
|
108
|
+
const currentAttempts = summary?.attempts || 0;
|
|
109
|
+
const attemptsBeforeRemedial = settings.attemptsBeforeRemedial;
|
|
110
|
+
const attemptsBeforeRestart = settings.attemptsBeforeRestart;
|
|
111
|
+
|
|
112
|
+
let attemptsInfo = '';
|
|
113
|
+
if (currentAttempts > 0) {
|
|
114
|
+
if (attemptsBeforeRestart && currentAttempts >= attemptsBeforeRestart) {
|
|
115
|
+
// Already at restart threshold - shouldn't reach here normally
|
|
116
|
+
attemptsInfo = ` | <strong>Attempts:</strong> ${currentAttempts} (course retake is required)`;
|
|
117
|
+
} else if (attemptsBeforeRemedial && currentAttempts >= attemptsBeforeRemedial) {
|
|
118
|
+
// In remedial phase
|
|
119
|
+
if (attemptsBeforeRestart) {
|
|
120
|
+
const remainingBeforeRestart = attemptsBeforeRestart - currentAttempts;
|
|
121
|
+
attemptsInfo = ` | <strong>Attempts:</strong> ${currentAttempts} (${remainingBeforeRestart} before a course retake will be required)`;
|
|
122
|
+
} else {
|
|
123
|
+
attemptsInfo = ` | <strong>Attempts:</strong> ${currentAttempts}`;
|
|
124
|
+
}
|
|
125
|
+
} else if (attemptsBeforeRemedial) {
|
|
126
|
+
// Before remedial threshold
|
|
127
|
+
const remainingBeforeRemedial = attemptsBeforeRemedial - currentAttempts;
|
|
128
|
+
attemptsInfo = ` | <strong>Attempts:</strong> ${currentAttempts} (${remainingBeforeRemedial} before additional review will be required)`;
|
|
129
|
+
} else if (attemptsBeforeRestart) {
|
|
130
|
+
// Only restart configured, no remedial
|
|
131
|
+
const remainingBeforeRestart = attemptsBeforeRestart - currentAttempts;
|
|
132
|
+
attemptsInfo = ` | <strong>Attempts:</strong> ${currentAttempts} (${remainingBeforeRestart} before a course retake will be required)`;
|
|
133
|
+
} else {
|
|
134
|
+
// No limits configured
|
|
135
|
+
attemptsInfo = ` | <strong>Attempts:</strong> ${currentAttempts}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const introEl = document.createElement('div');
|
|
140
|
+
introEl.className = 'content-medium stack-lg pt-6';
|
|
141
|
+
// Optional icon displayed before title
|
|
142
|
+
const iconHtml = config.icon
|
|
143
|
+
? `<span class="mr-2" style="display: inline-flex; align-items: center;" aria-hidden="true">${iconManager.getIcon(config.icon, { size: 'xl' })}</span>`
|
|
144
|
+
: '';
|
|
145
|
+
// Optional HTML description displayed below title
|
|
146
|
+
const descriptionHtml = config.description
|
|
147
|
+
? `<div class="text-muted">${config.description}</div>`
|
|
148
|
+
: '';
|
|
149
|
+
|
|
150
|
+
introEl.innerHTML = `
|
|
151
|
+
<div class="card no-hover text-center stack-md">
|
|
152
|
+
<h1 class="text-2xl font-bold flex items-center justify-center">${iconHtml}${config.title || 'Assessment'}</h1>
|
|
153
|
+
${descriptionHtml}
|
|
154
|
+
<div class="bg-gray-50 border rounded p-4 text-sm">
|
|
155
|
+
<p class="m-0"><strong>Questions:</strong> ${config.questions.length} | <strong>Passing Score:</strong> ${settings.passingScore}%${attemptsInfo}</p>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="flex justify-center">
|
|
158
|
+
<button data-action="start" class="btn btn-primary btn-lg" data-testid="assessment-start">Start Assessment</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
return introEl;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderQuestion() {
|
|
166
|
+
const questionIndex = stateManager.getCurrentQuestionIndex();
|
|
167
|
+
const questionInstance = questionInstances[questionIndex];
|
|
168
|
+
|
|
169
|
+
if (!questionInstance) {
|
|
170
|
+
const errorMessage = `[AssessmentUI:${config.id}] Question instance not found for index ${questionIndex} (assessment has ${questionInstances.length} questions)`;
|
|
171
|
+
logger.error(errorMessage, { domain: 'assessment', operation: 'renderQuestion', assessmentId: config.id, questionIndex });
|
|
172
|
+
throw new Error(errorMessage);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const questionEl = document.createElement('div');
|
|
176
|
+
questionEl.className = 'content-medium';
|
|
177
|
+
|
|
178
|
+
// Create card wrapper for consistent styling
|
|
179
|
+
const cardWrapper = document.createElement('div');
|
|
180
|
+
cardWrapper.className = 'card no-hover stack-md';
|
|
181
|
+
|
|
182
|
+
// Add title
|
|
183
|
+
const titleEl = document.createElement('h1');
|
|
184
|
+
titleEl.className = 'text-xl font-bold m-0';
|
|
185
|
+
titleEl.textContent = config.title || 'Assessment';
|
|
186
|
+
cardWrapper.appendChild(titleEl);
|
|
187
|
+
|
|
188
|
+
// Add progress indicator
|
|
189
|
+
if (settings.showProgress) {
|
|
190
|
+
const progressEl = document.createElement('p');
|
|
191
|
+
progressEl.className = 'text-muted text-sm m-0';
|
|
192
|
+
progressEl.textContent = `Question ${questionIndex + 1} of ${config.questions.length}`;
|
|
193
|
+
cardWrapper.appendChild(progressEl);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Create a container for the question content
|
|
197
|
+
const questionContent = document.createElement('div');
|
|
198
|
+
questionInstance.render(questionContent);
|
|
199
|
+
|
|
200
|
+
// Restore saved response after DOM is created
|
|
201
|
+
questionInstance.restoreFromSCORM();
|
|
202
|
+
cardWrapper.appendChild(questionContent);
|
|
203
|
+
|
|
204
|
+
// Add navigation
|
|
205
|
+
const navEl = createNavigation();
|
|
206
|
+
navEl.className = 'flex justify-between items-center mt-6 pt-4 border-top';
|
|
207
|
+
cardWrapper.appendChild(navEl);
|
|
208
|
+
|
|
209
|
+
questionEl.appendChild(cardWrapper);
|
|
210
|
+
|
|
211
|
+
return questionEl;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createNavigation() {
|
|
215
|
+
const navEl = document.createElement('div');
|
|
216
|
+
// Class name set in renderQuestion to avoid duplication/conflict
|
|
217
|
+
|
|
218
|
+
const currentIndex = stateManager.getCurrentQuestionIndex();
|
|
219
|
+
const isFirstQuestion = currentIndex === 0;
|
|
220
|
+
const isLastQuestion = currentIndex === config.questions.length - 1;
|
|
221
|
+
|
|
222
|
+
const session = stateManager.getSession();
|
|
223
|
+
const reviewReached = session?.reviewReached || false;
|
|
224
|
+
|
|
225
|
+
const prevButton = `<button class="btn btn-secondary" data-action="prev" data-testid="assessment-nav-prev" ${isFirstQuestion ? 'disabled' : ''}>Previous</button>`;
|
|
226
|
+
const nextButtonLabel = isLastQuestion ? (settings.allowReview ? 'Review' : 'Submit') : 'Next';
|
|
227
|
+
const nextButton = `<button class="btn btn-primary" data-action="next" data-testid="assessment-nav-next">${nextButtonLabel}</button>`;
|
|
228
|
+
|
|
229
|
+
// Show Jump to Review button once user has reached review screen
|
|
230
|
+
const jumpToReviewButton = reviewReached && settings.allowReview
|
|
231
|
+
? '<button class="btn btn-secondary" data-action="jump-to-review" data-testid="assessment-jump-to-review">Jump to Review</button>'
|
|
232
|
+
: '';
|
|
233
|
+
|
|
234
|
+
let progressIndicator = '';
|
|
235
|
+
if (settings.showProgress) {
|
|
236
|
+
// Simplified progress for nav bar since it's also at top
|
|
237
|
+
progressIndicator = `<span class="text-sm text-muted">Question ${currentIndex + 1} / ${config.questions.length}</span>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
navEl.innerHTML = `${prevButton}${progressIndicator}<div class="flex gap-2">${jumpToReviewButton}${nextButton}</div>`;
|
|
241
|
+
return navEl;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderReviewScreen() {
|
|
245
|
+
const reviewEl = document.createElement('div');
|
|
246
|
+
reviewEl.className = 'content-medium';
|
|
247
|
+
|
|
248
|
+
const session = stateManager.getSession();
|
|
249
|
+
if (!session) {
|
|
250
|
+
throw new Error(`Assessment '${config.id}' has no session - state corrupted`);
|
|
251
|
+
}
|
|
252
|
+
const responses = session.responses || {};
|
|
253
|
+
|
|
254
|
+
// Use metadata's isAnswered method to properly check each interaction type
|
|
255
|
+
const allAnswered = config.questions.every((q, index) => {
|
|
256
|
+
const metadata = questionInstances[index].metadata;
|
|
257
|
+
return metadata.isAnswered(responses[index]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const questionsHtml = config.questions.map((q, index) => {
|
|
261
|
+
const response = responses[index];
|
|
262
|
+
const metadata = questionInstances[index].metadata;
|
|
263
|
+
const isAnswered = metadata.isAnswered(response);
|
|
264
|
+
const statusClass = isAnswered ? 'bg-success text-white' : 'bg-gray-200 text-muted';
|
|
265
|
+
const statusText = isAnswered ? 'Answered' : 'Not Answered';
|
|
266
|
+
|
|
267
|
+
// Use prompt property (standard across all interaction types)
|
|
268
|
+
const questionText = q.prompt || q.questionText || 'Question';
|
|
269
|
+
const displayText = questionText.length > 60 ? questionText.substring(0, 60) + '...' : questionText;
|
|
270
|
+
|
|
271
|
+
return `
|
|
272
|
+
<li class="flex justify-between items-center p-3 bg-gray-50 rounded">
|
|
273
|
+
<div class="flex items-center gap-3 flex-1">
|
|
274
|
+
<span class="font-bold text-muted">Q${index + 1}</span>
|
|
275
|
+
<span class="text-sm">${displayText}</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="flex items-center gap-3">
|
|
278
|
+
<span class="text-xs px-2 py-1 rounded ${statusClass}">${statusText}</span>
|
|
279
|
+
<button class="btn btn-sm btn-outline-secondary" data-action="review-question" data-question-index="${index}" data-testid="assessment-review-question-${index}">Edit</button>
|
|
280
|
+
</div>
|
|
281
|
+
</li>
|
|
282
|
+
`;
|
|
283
|
+
}).join('');
|
|
284
|
+
|
|
285
|
+
reviewEl.innerHTML = `
|
|
286
|
+
<div class="card no-hover stack-md">
|
|
287
|
+
<div>
|
|
288
|
+
<h2 class="text-xl font-bold m-0">Review Your Answers</h2>
|
|
289
|
+
<p class="text-muted m-0">Please review your answers before submitting the assessment.</p>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<ul class="list-none stack-sm m-0 p-0">${questionsHtml}</ul>
|
|
293
|
+
|
|
294
|
+
${!allAnswered && review.requireAllAnswered ? '<div class="callout callout-danger">You must answer all questions before submitting.</div>' : ''}
|
|
295
|
+
|
|
296
|
+
<div class="flex justify-between mt-4 pt-4 border-top">
|
|
297
|
+
<button class="btn btn-secondary" data-action="back-to-questions" data-testid="assessment-back-to-questions">Back to Questions</button>
|
|
298
|
+
<button class="btn btn-primary" data-action="submit" data-testid="assessment-submit" ${!allAnswered && review.requireAllAnswered ? 'disabled' : ''}>Submit Assessment</button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
`;
|
|
302
|
+
return reviewEl;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderResultsScreen(results) {
|
|
306
|
+
if (!results) {
|
|
307
|
+
const el = document.createElement('div');
|
|
308
|
+
el.textContent = 'No results to display.';
|
|
309
|
+
return el;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const resultsEl = document.createElement('div');
|
|
313
|
+
resultsEl.className = 'content-medium';
|
|
314
|
+
|
|
315
|
+
const { totalQuestions, correctCount, scorePercentage, passed, details } = results;
|
|
316
|
+
|
|
317
|
+
let detailsHtml = '';
|
|
318
|
+
if (resultsDisplay.showQuestions && details) {
|
|
319
|
+
detailsHtml = details.map((detail, index) => {
|
|
320
|
+
if (!detail) return '';
|
|
321
|
+
const qConfig = config.questions[index];
|
|
322
|
+
const isCorrect = detail.correct;
|
|
323
|
+
const correctnessClass = isCorrect ? 'text-success' : 'text-error';
|
|
324
|
+
const correctnessIcon = isCorrect ? '✔' : '✖';
|
|
325
|
+
const bgClass = isCorrect ? 'bg-green-50' : 'bg-red-50';
|
|
326
|
+
|
|
327
|
+
// Use prompt property (standard across all interaction types)
|
|
328
|
+
const questionText = qConfig.prompt || qConfig.questionText || `Question ${index + 1}`;
|
|
329
|
+
|
|
330
|
+
// Get metadata from question instance
|
|
331
|
+
const metadata = questionInstances[index].metadata;
|
|
332
|
+
|
|
333
|
+
let responseHtml = '';
|
|
334
|
+
if (resultsDisplay.showUserResponses && detail.response !== null && detail.response !== undefined) {
|
|
335
|
+
responseHtml += `<div class="mt-2 p-3 bg-gray-50 rounded"><p class="text-xs font-bold text-muted uppercase mb-2 mt-0">Your Answer</p><div class="text-sm">${metadata.formatUserResponse(qConfig, detail.response)}</div></div>`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Show correct answer based on whether student got it right or wrong
|
|
339
|
+
const shouldShowCorrect = (isCorrect && resultsDisplay.showCorrectAnswers) ||
|
|
340
|
+
(!isCorrect && resultsDisplay.showIncorrectAnswers);
|
|
341
|
+
|
|
342
|
+
if (shouldShowCorrect) {
|
|
343
|
+
const correctAnswer = metadata.getCorrectAnswer(qConfig);
|
|
344
|
+
responseHtml += `<div class="mt-2 p-3 bg-green-50 rounded"><p class="text-xs font-bold text-success uppercase mb-2 mt-0">Correct Answer</p><div class="text-sm">${metadata.formatCorrectAnswer(qConfig, correctAnswer)}</div></div>`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return `
|
|
348
|
+
<li class="p-4 rounded ${bgClass}">
|
|
349
|
+
<div class="flex gap-3 items-start">
|
|
350
|
+
<span class="font-bold text-muted">Q${index + 1}</span>
|
|
351
|
+
<div class="flex-1 stack-sm">
|
|
352
|
+
<div class="flex justify-between">
|
|
353
|
+
<span class="font-semibold">${questionText}</span>
|
|
354
|
+
${resultsDisplay.showCorrectness ? `<span class="${correctnessClass} font-bold">${correctnessIcon}</span>` : ''}
|
|
355
|
+
</div>
|
|
356
|
+
${responseHtml}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</li>
|
|
360
|
+
`;
|
|
361
|
+
}).join('');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Render action button (provided by Actions layer with all business logic)
|
|
365
|
+
let actionButtonHtml = '';
|
|
366
|
+
if (results.actionButton) {
|
|
367
|
+
const btn = results.actionButton;
|
|
368
|
+
|
|
369
|
+
let messageHtml = '';
|
|
370
|
+
if (btn.message) {
|
|
371
|
+
const calloutClass = `callout callout-${btn.messageType || 'info'}`;
|
|
372
|
+
const title = btn.type === 'restart' ? 'Maximum Attempts Reached' : 'Review Recommended';
|
|
373
|
+
messageHtml = `
|
|
374
|
+
<div class="${calloutClass}">
|
|
375
|
+
<p class="font-bold">${title}</p>
|
|
376
|
+
<p>${btn.message}</p>
|
|
377
|
+
</div>
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let attemptsHtml = '';
|
|
382
|
+
if (btn.attemptsMessage) {
|
|
383
|
+
attemptsHtml = `<p class="text-muted mt-2 text-sm text-center">${btn.attemptsMessage}</p>`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
actionButtonHtml = `
|
|
387
|
+
<div class="stack-md mt-6 pt-4 border-top">
|
|
388
|
+
${messageHtml}
|
|
389
|
+
<div class="flex justify-center">
|
|
390
|
+
<button data-action="${btn.action}" class="btn btn-primary btn-lg" data-testid="assessment-${btn.action}">
|
|
391
|
+
${btn.label}
|
|
392
|
+
</button>
|
|
393
|
+
</div>
|
|
394
|
+
${attemptsHtml}
|
|
395
|
+
</div>
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
resultsEl.innerHTML = `
|
|
400
|
+
<div class="card no-hover stack-lg">
|
|
401
|
+
<div class="text-center stack-sm">
|
|
402
|
+
<h2 class="text-2xl font-bold m-0">Assessment Results</h2>
|
|
403
|
+
<p class="text-xl ${passed ? 'text-success' : 'text-error'} font-bold m-0">${passed ? 'Passed' : 'Failed'}</p>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div class="cols-3 gap-4">
|
|
407
|
+
<div class="p-3 bg-gray-50 rounded text-center border">
|
|
408
|
+
<div class="text-xs font-bold text-muted uppercase">Score</div>
|
|
409
|
+
<div class="text-xl font-bold">${scorePercentage.toFixed(0)}%</div>
|
|
410
|
+
</div>
|
|
411
|
+
<div class="p-3 bg-gray-50 rounded text-center border">
|
|
412
|
+
<div class="text-xs font-bold text-muted uppercase">Correct</div>
|
|
413
|
+
<div class="text-xl font-bold">${correctCount} / ${totalQuestions}</div>
|
|
414
|
+
</div>
|
|
415
|
+
<div class="p-3 bg-gray-50 rounded text-center border">
|
|
416
|
+
<div class="text-xs font-bold text-muted uppercase">Time</div>
|
|
417
|
+
<div class="text-xl font-bold">${results.timeSpent || '--:--'}</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
${detailsHtml ? `<ul class="list-none stack-sm m-0 p-0">${detailsHtml}</ul>` : ''}
|
|
422
|
+
${actionButtonHtml}
|
|
423
|
+
</div>
|
|
424
|
+
`;
|
|
425
|
+
return resultsEl;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function initialize(container) {
|
|
429
|
+
const viewManager = createViewManager(container, 'assessment');
|
|
430
|
+
viewManager.registerView('intro', {
|
|
431
|
+
render: renderIntroScreen
|
|
432
|
+
});
|
|
433
|
+
viewManager.registerView('question', {
|
|
434
|
+
render: renderQuestion
|
|
435
|
+
});
|
|
436
|
+
viewManager.registerView('review', {
|
|
437
|
+
render: renderReviewScreen
|
|
438
|
+
});
|
|
439
|
+
viewManager.registerView('results', {
|
|
440
|
+
render: renderResultsScreen
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return viewManager;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
initialize,
|
|
448
|
+
showUnansweredModal: (unansweredIndices, allowSubmission, onConfirm) =>
|
|
449
|
+
showUnansweredModal(unansweredIndices, allowSubmission, onConfirm),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file api-engagement.js
|
|
3
|
+
* Engagement tracking, flag management, and audio control methods
|
|
4
|
+
* for the CourseCodeAutomation API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import engagementManager from '../engagement/engagement-manager.js';
|
|
8
|
+
import flagManager from '../managers/flag-manager.js';
|
|
9
|
+
import audioManager from '../managers/audio-manager.js';
|
|
10
|
+
import * as NavigationState from '../navigation/NavigationState.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates engagement/flag/audio API methods bound to the shared logTrace function.
|
|
14
|
+
* @param {Function} logTrace - Shared trace logger
|
|
15
|
+
* @returns {Object} Engagement, flag, and audio API methods
|
|
16
|
+
*/
|
|
17
|
+
export function createEngagementMethods(logTrace) {
|
|
18
|
+
return {
|
|
19
|
+
// ===== Engagement Methods =====
|
|
20
|
+
|
|
21
|
+
getEngagementState() {
|
|
22
|
+
const slideId = NavigationState.getCurrentSlideId();
|
|
23
|
+
if (!slideId) {
|
|
24
|
+
throw new Error('CourseCodeAutomation: No active slide');
|
|
25
|
+
}
|
|
26
|
+
const state = engagementManager.getSlideState(slideId);
|
|
27
|
+
logTrace('getEngagementState', { slideId, state });
|
|
28
|
+
return state;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
getEngagementProgress() {
|
|
32
|
+
const slideId = NavigationState.getCurrentSlideId();
|
|
33
|
+
if (!slideId) {
|
|
34
|
+
throw new Error('CourseCodeAutomation: No active slide');
|
|
35
|
+
}
|
|
36
|
+
const progress = engagementManager.getProgress(slideId);
|
|
37
|
+
logTrace('getEngagementProgress', { slideId, progress });
|
|
38
|
+
return progress;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
markTabViewed(tabId) {
|
|
42
|
+
const slideId = NavigationState.getCurrentSlideId();
|
|
43
|
+
if (!slideId) {
|
|
44
|
+
throw new Error('CourseCodeAutomation: No active slide');
|
|
45
|
+
}
|
|
46
|
+
engagementManager.trackTabView(slideId, tabId);
|
|
47
|
+
logTrace('markTabViewed', { slideId, tabId });
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
markFlipCardViewed(cardId) {
|
|
51
|
+
const slideId = NavigationState.getCurrentSlideId();
|
|
52
|
+
if (!slideId) {
|
|
53
|
+
throw new Error('CourseCodeAutomation: No active slide');
|
|
54
|
+
}
|
|
55
|
+
engagementManager.trackFlipCardView(slideId, cardId);
|
|
56
|
+
logTrace('markFlipCardViewed', { slideId, cardId });
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
setScrollDepth(percentage) {
|
|
60
|
+
const slideId = NavigationState.getCurrentSlideId();
|
|
61
|
+
if (!slideId) {
|
|
62
|
+
throw new Error('CourseCodeAutomation: No active slide');
|
|
63
|
+
}
|
|
64
|
+
if (typeof percentage !== 'number' || percentage < 0 || percentage > 100) {
|
|
65
|
+
throw new Error('CourseCodeAutomation: Scroll depth must be a number between 0 and 100');
|
|
66
|
+
}
|
|
67
|
+
engagementManager.trackScrollDepth(slideId, percentage);
|
|
68
|
+
logTrace('setScrollDepth', { slideId, percentage });
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
resetEngagement() {
|
|
72
|
+
const slideId = NavigationState.getCurrentSlideId();
|
|
73
|
+
if (!slideId) {
|
|
74
|
+
throw new Error('CourseCodeAutomation: No active slide');
|
|
75
|
+
}
|
|
76
|
+
engagementManager.resetSlide(slideId);
|
|
77
|
+
logTrace('resetEngagement', { slideId });
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// ===== Flag Management =====
|
|
81
|
+
|
|
82
|
+
getFlag(key) {
|
|
83
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
84
|
+
throw new Error('CourseCodeAutomation: getFlag requires a non-empty string key');
|
|
85
|
+
}
|
|
86
|
+
const value = flagManager.getFlag(key);
|
|
87
|
+
logTrace('getFlag', { key, value });
|
|
88
|
+
return value;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
setFlag(key, value) {
|
|
92
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
93
|
+
throw new Error('CourseCodeAutomation: setFlag requires a non-empty string key');
|
|
94
|
+
}
|
|
95
|
+
flagManager.setFlag(key, value);
|
|
96
|
+
logTrace('setFlag', { key, value });
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
getAllFlags() {
|
|
100
|
+
const flags = flagManager.getAllFlags();
|
|
101
|
+
logTrace('getAllFlags', { count: Object.keys(flags).length });
|
|
102
|
+
return flags;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
removeFlag(key) {
|
|
106
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
107
|
+
throw new Error('CourseCodeAutomation: removeFlag requires a non-empty string key');
|
|
108
|
+
}
|
|
109
|
+
flagManager.removeFlag(key);
|
|
110
|
+
logTrace('removeFlag', { key });
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// ===== Audio Methods =====
|
|
114
|
+
|
|
115
|
+
getAudioState() {
|
|
116
|
+
if (!audioManager.isReady()) {
|
|
117
|
+
return { initialized: false };
|
|
118
|
+
}
|
|
119
|
+
const state = audioManager.getState();
|
|
120
|
+
logTrace('getAudioState', { hasAudio: !!state.currentSrc, contextType: state.contextType });
|
|
121
|
+
return state;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
hasAudio() {
|
|
125
|
+
const has = audioManager.isReady() && audioManager.hasAudio();
|
|
126
|
+
logTrace('hasAudio', { result: has });
|
|
127
|
+
return has;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
simulateAudioComplete() {
|
|
131
|
+
if (!audioManager.isReady()) {
|
|
132
|
+
throw new Error('CourseCodeAutomation: AudioManager not initialized');
|
|
133
|
+
}
|
|
134
|
+
if (!audioManager.hasAudio()) {
|
|
135
|
+
throw new Error('CourseCodeAutomation: No audio loaded');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const state = audioManager.getState();
|
|
139
|
+
if (!state.contextId) {
|
|
140
|
+
throw new Error('CourseCodeAutomation: No audio context');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
144
|
+
|
|
145
|
+
if (state.duration > 0) {
|
|
146
|
+
const targetPosition = state.duration * (state.completionThreshold || 0.95);
|
|
147
|
+
audioManager.seek(targetPosition);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
switch (state.contextType) {
|
|
151
|
+
case 'slide':
|
|
152
|
+
if (currentSlideId) {
|
|
153
|
+
engagementManager.trackSlideAudioComplete(currentSlideId);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case 'modal': {
|
|
157
|
+
const modalId = state.contextId.replace('modal-', '');
|
|
158
|
+
if (currentSlideId && modalId) {
|
|
159
|
+
engagementManager.trackModalAudioComplete(currentSlideId, modalId);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case 'tab':
|
|
164
|
+
case 'standalone':
|
|
165
|
+
if (currentSlideId) {
|
|
166
|
+
engagementManager.trackStandaloneAudioComplete(currentSlideId, state.contextId);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
logTrace('simulateAudioComplete', {
|
|
172
|
+
contextId: state.contextId,
|
|
173
|
+
contextType: state.contextType,
|
|
174
|
+
slideId: currentSlideId
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
isAudioCompletedForContext(contextId) {
|
|
179
|
+
if (!audioManager.isReady()) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const completed = audioManager.isAudioCompleted(contextId);
|
|
183
|
+
logTrace('isAudioCompletedForContext', { contextId, completed });
|
|
184
|
+
return completed;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
getAudioProgress() {
|
|
188
|
+
if (!audioManager.isReady()) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
const progress = audioManager.getProgressPercentage();
|
|
192
|
+
logTrace('getAudioProgress', { progress });
|
|
193
|
+
return progress;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|