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