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.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. 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
+ }