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,313 @@
1
+ /**
2
+ * Error Reporter - Optional external error reporting via webhook
3
+ *
4
+ * Sends framework errors to a configured endpoint (e.g., Cloudflare Worker)
5
+ * for email notifications. Disabled by default; enable in course-config.js.
6
+ *
7
+ * Flood protection:
8
+ * - Per-error dedup: same error key won't be sent twice within 60 seconds
9
+ * - Batching: errors arriving within a 2-second window are combined into one request
10
+ * - Global rate cap: max 10 reports per rolling 60-second window
11
+ * - Re-entrancy guard: reporter's own log messages don't trigger new reports
12
+ *
13
+ * Configuration in course-config.js:
14
+ * environment: {
15
+ * errorReporting: {
16
+ * endpoint: 'https://your-worker.workers.dev/errors',
17
+ * // Optional: API key for endpoint authentication
18
+ * apiKey: 'your-shared-api-key',
19
+ * // Optional: include course/learner context
20
+ * includeContext: true
21
+ * }
22
+ * }
23
+ */
24
+
25
+ import { eventBus } from '../core/event-bus.js';
26
+ import { logger } from './logger.js';
27
+
28
+ // ── Per-error dedup ─────────────────────────────────────────────────
29
+ const recentErrors = new Map();
30
+ const DEBOUNCE_MS = 60000; // Don't send same error more than once per minute
31
+
32
+ // ── Batching ────────────────────────────────────────────────────────
33
+ const BATCH_WINDOW_MS = 2000; // Collect errors for 2 seconds before sending
34
+ let pendingBatch = []; // Accumulated payloads waiting to flush
35
+ let batchTimer = null; // setTimeout handle for the current window
36
+
37
+ // ── Global rate cap ─────────────────────────────────────────────────
38
+ const MAX_SENDS_PER_WINDOW = 10;
39
+ const RATE_WINDOW_MS = 60000;
40
+ const sendTimestamps = []; // Timestamps of recent sends
41
+
42
+ // ── Re-entrancy guard ───────────────────────────────────────────────
43
+ let _isReporting = false;
44
+
45
+ /**
46
+ * Generate a unique key for an error to detect duplicates
47
+ */
48
+ function getErrorKey(errorData) {
49
+ const domain = errorData.domain || 'unknown';
50
+ const operation = errorData.operation || 'unknown';
51
+ const message = errorData.message || String(errorData);
52
+ return `${domain}:${operation}:${message}`;
53
+ }
54
+
55
+ /**
56
+ * Check if this error was recently reported
57
+ */
58
+ function wasRecentlyReported(errorKey) {
59
+ const lastReported = recentErrors.get(errorKey);
60
+ if (!lastReported) return false;
61
+ return (Date.now() - lastReported) < DEBOUNCE_MS;
62
+ }
63
+
64
+ /**
65
+ * Mark an error as recently reported
66
+ */
67
+ function markAsReported(errorKey) {
68
+ recentErrors.set(errorKey, Date.now());
69
+
70
+ // Clean up old entries periodically
71
+ if (recentErrors.size > 100) {
72
+ const now = Date.now();
73
+ for (const [key, timestamp] of recentErrors.entries()) {
74
+ if (now - timestamp > DEBOUNCE_MS) {
75
+ recentErrors.delete(key);
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Check whether we've hit the global rate cap
83
+ */
84
+ function isRateLimited() {
85
+ const now = Date.now();
86
+ // Prune timestamps outside the rolling window
87
+ while (sendTimestamps.length > 0 && now - sendTimestamps[0] > RATE_WINDOW_MS) {
88
+ sendTimestamps.shift();
89
+ }
90
+ return sendTimestamps.length >= MAX_SENDS_PER_WINDOW;
91
+ }
92
+
93
+ /**
94
+ * Queue an error for batched sending.
95
+ * Errors are collected for BATCH_WINDOW_MS then flushed in a single request.
96
+ */
97
+ function enqueueError(errorData, config, courseConfig) {
98
+ // Normalize error data — some emitters pass strings instead of objects
99
+ const normalizedError = typeof errorData === 'string'
100
+ ? { domain: 'unknown', operation: 'unknown', message: errorData }
101
+ : errorData;
102
+
103
+ // Skip user-facing errors (expected behavior, not system errors)
104
+ if (normalizedError.userFacing === true) {
105
+ logger.debug('[ErrorReporter] Skipping user-facing error (not a system error):', normalizedError.message);
106
+ return;
107
+ }
108
+
109
+ const errorKey = getErrorKey(normalizedError);
110
+
111
+ // Skip if recently reported (even across batches)
112
+ if (wasRecentlyReported(errorKey)) {
113
+ logger.debug('[ErrorReporter] Skipping duplicate error:', errorKey);
114
+ return;
115
+ }
116
+
117
+ // Global rate cap check
118
+ if (isRateLimited()) {
119
+ logger.debug('[ErrorReporter] Rate limited, dropping error:', errorKey);
120
+ return;
121
+ }
122
+
123
+ // Build payload entry
124
+ const payload = {
125
+ domain: normalizedError.domain || 'unknown',
126
+ operation: normalizedError.operation || 'unknown',
127
+ message: normalizedError.message || String(errorData),
128
+ stack: normalizedError.stack,
129
+ context: normalizedError.context
130
+ };
131
+
132
+ // Mark as reported immediately so duplicates within the same batch are dropped
133
+ markAsReported(errorKey);
134
+
135
+ pendingBatch.push(payload);
136
+
137
+ // Store config/courseConfig for the flush (same for all errors in a session)
138
+ if (!batchTimer) {
139
+ batchTimer = setTimeout(() => flushBatch(config, courseConfig), BATCH_WINDOW_MS);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Flush the pending batch as a single request to the endpoint.
145
+ */
146
+ async function flushBatch(config, courseConfig) {
147
+ const batch = pendingBatch;
148
+ pendingBatch = [];
149
+ batchTimer = null;
150
+
151
+ if (batch.length === 0) return;
152
+
153
+ // Build the request payload
154
+ const request = {
155
+ // If single error, send flat for backward compatibility; otherwise send array
156
+ ...(batch.length === 1
157
+ ? batch[0]
158
+ : { errors: batch, message: `${batch.length} errors`, domain: batch[0].domain, operation: batch[0].operation }),
159
+
160
+ // Metadata
161
+ timestamp: new Date().toISOString(),
162
+ url: window.location.href,
163
+ userAgent: navigator.userAgent
164
+ };
165
+
166
+ // Optionally include course context
167
+ if (config.includeContext !== false) {
168
+ request.course = {
169
+ title: courseConfig.metadata?.title,
170
+ version: courseConfig.metadata?.version,
171
+ id: courseConfig.metadata?.id
172
+ };
173
+ }
174
+
175
+ // Guard re-entrancy: our own logger calls must not trigger new reports
176
+ _isReporting = true;
177
+ try {
178
+ const headers = { 'Content-Type': 'application/json' };
179
+ if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;
180
+
181
+ const response = await fetch(config.endpoint, {
182
+ method: 'POST',
183
+ headers,
184
+ body: JSON.stringify(request)
185
+ });
186
+
187
+ if (response.ok) {
188
+ sendTimestamps.push(Date.now());
189
+ logger.debug(`[ErrorReporter] Batch of ${batch.length} error(s) reported successfully`);
190
+ } else {
191
+ logger.warn('[ErrorReporter] Failed to report error:', response.status);
192
+ }
193
+ } catch (e) {
194
+ // Silent fail — don't break the course or cause infinite loops
195
+ logger.debug('[ErrorReporter] Network error reporting:', e.message);
196
+ } finally {
197
+ _isReporting = false;
198
+ }
199
+ }
200
+
201
+ // Store config globally for user reports
202
+ let _config = null;
203
+ let _courseConfig = null;
204
+
205
+ /**
206
+ * Check if error reporting is configured and user reports are enabled
207
+ * @returns {boolean}
208
+ */
209
+ export function isUserReportingEnabled() {
210
+ return !!(_config?.endpoint && _config?.enableUserReports !== false);
211
+ }
212
+
213
+ /**
214
+ * Submit a user-initiated issue report
215
+ * @param {string} description - User's description of the issue
216
+ * @param {Object} options - Additional options
217
+ * @returns {Promise<{success: boolean, message: string}>}
218
+ */
219
+ export async function submitUserReport(description, options = {}) {
220
+ if (!_config?.endpoint) {
221
+ return { success: false, message: 'Error reporting is not configured.' };
222
+ }
223
+
224
+ const payload = {
225
+ type: 'user_report',
226
+ description: description.trim(),
227
+
228
+ // Metadata
229
+ timestamp: new Date().toISOString(),
230
+ url: window.location.href,
231
+ userAgent: navigator.userAgent
232
+ };
233
+
234
+ // Include course context
235
+ if (_config.includeContext !== false && _courseConfig) {
236
+ payload.course = {
237
+ title: _courseConfig.metadata?.title,
238
+ version: _courseConfig.metadata?.version,
239
+ id: _courseConfig.metadata?.id
240
+ };
241
+ }
242
+
243
+ // Include current slide info if available
244
+ if (options.currentSlide) {
245
+ payload.currentSlide = options.currentSlide;
246
+ }
247
+
248
+ // Include recent logs if requested and available
249
+ if (options.includeLogs && typeof getRecentLogs === 'function') {
250
+ payload.recentLogs = getRecentLogs();
251
+ }
252
+
253
+ try {
254
+ const headers = { 'Content-Type': 'application/json' };
255
+ if (_config.apiKey) headers['Authorization'] = `Bearer ${_config.apiKey}`;
256
+
257
+ const response = await fetch(_config.endpoint, {
258
+ method: 'POST',
259
+ headers,
260
+ body: JSON.stringify(payload)
261
+ });
262
+
263
+ if (response.ok) {
264
+ logger.debug('[ErrorReporter] User report submitted successfully');
265
+ return { success: true, message: 'Your report has been submitted. Thank you!' };
266
+ } else {
267
+ logger.warn('[ErrorReporter] Failed to submit user report:', response.status);
268
+ return { success: false, message: 'Failed to submit report. Please try again later.' };
269
+ }
270
+ } catch (e) {
271
+ logger.warn('[ErrorReporter] Network error submitting user report:', e.message);
272
+ return { success: false, message: 'Network error. Please check your connection and try again.' };
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Initialize error reporting if configured
278
+ *
279
+ * @param {Object} courseConfig - The course configuration object
280
+ */
281
+ export function initErrorReporter(courseConfig) {
282
+ const config = courseConfig.environment?.errorReporting;
283
+
284
+ // Store for user reports
285
+ _config = config;
286
+ _courseConfig = courseConfig;
287
+
288
+ // Never send reports during local dev — the preview server and dev command
289
+ // inject VITE_COURSECODE_LOCAL into the Vite build env, which is auto-exposed
290
+ // to client code. Production builds via `coursecode build` don't set this.
291
+ if (import.meta.env.VITE_COURSECODE_LOCAL) {
292
+ logger.debug('[ErrorReporter] Disabled in local dev mode');
293
+ return;
294
+ }
295
+
296
+ // Disabled if not configured or no endpoint
297
+ if (!config?.endpoint) {
298
+ logger.debug('[ErrorReporter] Not configured, skipping initialization');
299
+ return;
300
+ }
301
+
302
+ logger.info('[ErrorReporter] Initialized with endpoint:', config.endpoint);
303
+
304
+ // Subscribe to unified logger events
305
+ eventBus.on('log:error', (errorData) => {
306
+ if (_isReporting) return; // prevent re-entrancy from our own logger calls
307
+ enqueueError(errorData, config, courseConfig);
308
+ });
309
+ eventBus.on('log:warn', (errorData) => {
310
+ if (_isReporting) return; // prevent re-entrancy from our own logger calls
311
+ enqueueError(errorData, config, courseConfig);
312
+ });
313
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Hotspot Coordinate Helper Utility
3
+ * Development tool to help authors determine hotspot coordinates
4
+ *
5
+ * Usage:
6
+ * import { enableHotspotHelper } from '../../../framework/js/utilities/hotspot-helper.js';
7
+ *
8
+ * // In your slide render function:
9
+ * const img = document.querySelector('img');
10
+ * enableHotspotHelper(img);
11
+ *
12
+ * // Click on the image to see coordinates logged to console
13
+ */
14
+
15
+ import { logger } from './logger.js';
16
+
17
+ /**
18
+ * Enable coordinate helper on an image element
19
+ * Logs pixel and percentage coordinates on click
20
+ * @param {HTMLImageElement} imageElement - Image element to attach helper to
21
+ * @param {Object} options - Configuration options
22
+ * @param {boolean} options.showOverlay - Show visual overlay (default: true)
23
+ * @param {boolean} options.showPixels - Log pixel coordinates (default: true)
24
+ * @param {boolean} options.showPercent - Log percentage coordinates (default: true)
25
+ * @param {string} options.outputFormat - 'json' or 'code' (default: 'both')
26
+ */
27
+ export function enableHotspotHelper(imageElement, options = {}) {
28
+ const {
29
+ showOverlay = true,
30
+ showPixels = true,
31
+ showPercent = true,
32
+ outputFormat = 'both'
33
+ } = options;
34
+
35
+ if (!imageElement) {
36
+ logger.error('[HotspotHelper] No image element provided');
37
+ return;
38
+ }
39
+
40
+ // Wait for image to load
41
+ const initialize = () => {
42
+ const container = imageElement.parentElement;
43
+ let overlay = null;
44
+
45
+ if (showOverlay) {
46
+ // Create visual overlay
47
+ overlay = document.createElement('div');
48
+ overlay.style.cssText = `
49
+ position: absolute;
50
+ top: 0;
51
+ left: 0;
52
+ width: 100%;
53
+ height: 100%;
54
+ pointer-events: none;
55
+ z-index: 9999;
56
+ `;
57
+ container.style.position = 'relative';
58
+ container.appendChild(overlay);
59
+ }
60
+
61
+ const markers = [];
62
+
63
+ imageElement.addEventListener('click', (e) => {
64
+ const rect = imageElement.getBoundingClientRect();
65
+ const naturalWidth = imageElement.naturalWidth || imageElement.width;
66
+ const naturalHeight = imageElement.naturalHeight || imageElement.height;
67
+
68
+ const clickX = e.clientX - rect.left;
69
+ const clickY = e.clientY - rect.top;
70
+
71
+ const pixelX = Math.round((clickX / rect.width) * naturalWidth);
72
+ const pixelY = Math.round((clickY / rect.height) * naturalHeight);
73
+
74
+ const percentX = ((clickX / rect.width) * 100).toFixed(2);
75
+ const percentY = ((clickY / rect.height) * 100).toFixed(2);
76
+
77
+ logger.debug('\n=== Hotspot Coordinate Helper ===');
78
+ logger.debug(`Image: ${naturalWidth}x${naturalHeight}px`);
79
+
80
+ if (showPixels) {
81
+ logger.debug('\nPixel coordinates:');
82
+ logger.debug(` x: ${pixelX}, y: ${pixelY}`);
83
+ }
84
+
85
+ if (showPercent) {
86
+ logger.debug('\nPercentage coordinates:');
87
+ logger.debug(` x: ${percentX}%, y: ${percentY}%`);
88
+ }
89
+
90
+ if (outputFormat === 'json' || outputFormat === 'both') {
91
+ logger.debug('\nJSON format (circle):');
92
+ logger.debug(JSON.stringify({
93
+ id: 'hotspot-1',
94
+ shape: 'circle',
95
+ position: { cx: pixelX, cy: pixelY, r: 25 },
96
+ correct: true,
97
+ label: 'Hotspot Label'
98
+ }, null, 2));
99
+
100
+ logger.debug('\nJSON format (rectangle):');
101
+ logger.debug(JSON.stringify({
102
+ id: 'hotspot-1',
103
+ shape: 'rectangle',
104
+ position: { x: pixelX, y: pixelY, width: 50, height: 40 },
105
+ correct: true,
106
+ label: 'Hotspot Label'
107
+ }, null, 2));
108
+ }
109
+
110
+ if (outputFormat === 'code' || outputFormat === 'both') {
111
+ logger.debug('\nCode snippet (circle):');
112
+ logger.debug(`{
113
+ id: 'hotspot-1',
114
+ shape: 'circle',
115
+ position: { cx: ${pixelX}, cy: ${pixelY}, r: 25 },
116
+ correct: true,
117
+ label: 'Hotspot Label'
118
+ }`);
119
+
120
+ logger.debug('\nCode snippet (rectangle):');
121
+ logger.debug(`{
122
+ id: 'hotspot-1',
123
+ shape: 'rectangle',
124
+ position: { x: ${pixelX}, y: ${pixelY}, width: 50, height: 40 },
125
+ correct: true,
126
+ label: 'Hotspot Label'
127
+ }`);
128
+ }
129
+
130
+ // Add visual marker
131
+ if (overlay) {
132
+ const marker = document.createElement('div');
133
+ marker.style.cssText = `
134
+ position: absolute;
135
+ left: ${clickX}px;
136
+ top: ${clickY}px;
137
+ width: 10px;
138
+ height: 10px;
139
+ background: rgba(255, 0, 0, 0.7);
140
+ border: 2px solid white;
141
+ border-radius: 50%;
142
+ transform: translate(-50%, -50%);
143
+ pointer-events: none;
144
+ `;
145
+
146
+ const label = document.createElement('div');
147
+ label.style.cssText = `
148
+ position: absolute;
149
+ left: ${clickX + 10}px;
150
+ top: ${clickY - 10}px;
151
+ background: rgba(0, 0, 0, 0.8);
152
+ color: white;
153
+ padding: 2px 6px;
154
+ font-size: 11px;
155
+ border-radius: 3px;
156
+ white-space: nowrap;
157
+ pointer-events: none;
158
+ `;
159
+ label.textContent = `${pixelX}, ${pixelY}`;
160
+
161
+ overlay.appendChild(marker);
162
+ overlay.appendChild(label);
163
+ markers.push({ marker, label });
164
+ }
165
+
166
+ logger.debug('=================================\n');
167
+ });
168
+
169
+ // Add clear button
170
+ if (showOverlay) {
171
+ const clearButton = document.createElement('button');
172
+ clearButton.textContent = 'Clear Markers';
173
+ clearButton.style.cssText = `
174
+ position: absolute;
175
+ top: 10px;
176
+ right: 10px;
177
+ z-index: 10000;
178
+ padding: 5px 10px;
179
+ background: rgba(255, 0, 0, 0.8);
180
+ color: white;
181
+ border: none;
182
+ border-radius: 4px;
183
+ cursor: pointer;
184
+ font-size: 12px;
185
+ `;
186
+ clearButton.addEventListener('click', () => {
187
+ markers.forEach(({ marker, label }) => {
188
+ marker.remove();
189
+ label.remove();
190
+ });
191
+ markers.length = 0;
192
+ });
193
+ container.appendChild(clearButton);
194
+ }
195
+
196
+ logger.debug('[HotspotHelper] Enabled on image. Click to see coordinates.');
197
+ };
198
+
199
+ if (imageElement.complete) {
200
+ initialize();
201
+ } else {
202
+ imageElement.addEventListener('load', initialize);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Create an interactive hotspot designer
208
+ * Shows a UI for drawing hotspots on an image
209
+ * @param {HTMLImageElement} imageElement - Image element
210
+ * @param {Function} onSave - Callback when hotspots are saved
211
+ */
212
+ export function createHotspotDesigner(imageElement, onSave) {
213
+ if (!imageElement) {
214
+ logger.error('[HotspotDesigner] No image element provided');
215
+ return;
216
+ }
217
+
218
+ const container = imageElement.parentElement;
219
+ container.style.position = 'relative';
220
+
221
+ const hotspots = [];
222
+ let _currentShape = 'circle';
223
+ const _isDrawing = false;
224
+ const _startPoint = null;
225
+
226
+ // Create UI
227
+ const ui = document.createElement('div');
228
+ ui.className = 'absolute p-3';
229
+ ui.style.cssText = `
230
+ top: 10px;
231
+ left: 10px;
232
+ background: white;
233
+ padding: 10px;
234
+ border-radius: 4px;
235
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
236
+ z-index: 10000;
237
+ `;
238
+
239
+ ui.innerHTML = `
240
+ <div class="mb-2 font-semibold">Hotspot Designer</div>
241
+ <div class="mb-2">
242
+ <label>
243
+ <input type="radio" name="shape" value="circle" checked> Circle
244
+ </label>
245
+ <label class="ml-2">
246
+ <input type="radio" name="shape" value="rectangle"> Rectangle
247
+ </label>
248
+ </div>
249
+ <div class="mb-2">
250
+ <label>
251
+ <input type="checkbox" id="correct-checkbox" checked> Correct Answer
252
+ </label>
253
+ </div>
254
+ <button id="save-hotspots" class="btn btn-success w-full" style="padding: 5px;">
255
+ Save Hotspots
256
+ </button>
257
+ <button id="clear-all" class="btn btn-reset w-full mt-2" style="padding: 5px;">
258
+ Clear All
259
+ </button>
260
+ `;
261
+
262
+ container.appendChild(ui);
263
+
264
+ // Shape selection
265
+ ui.querySelectorAll('input[name="shape"]').forEach(input => {
266
+ input.addEventListener('change', (e) => {
267
+ currentShape = e.target.value;
268
+ });
269
+ });
270
+
271
+ // Save button
272
+ ui.querySelector('#save-hotspots').addEventListener('click', () => {
273
+ if (onSave) {
274
+ onSave(hotspots);
275
+ }
276
+ logger.debug('Hotspots configuration:');
277
+ logger.debug(JSON.stringify(hotspots, null, 2));
278
+ });
279
+
280
+ // Clear button
281
+ ui.querySelector('#clear-all').addEventListener('click', () => {
282
+ hotspots.length = 0;
283
+ // Clear visual markers
284
+ });
285
+
286
+ logger.debug('[HotspotDesigner] Enabled. Click and drag to draw hotspots.');
287
+ }
288
+
289
+ /**
290
+ * Convert percentage-based config to pixel-based config
291
+ * @param {Object} config - Hotspot config with percentage values
292
+ * @param {number} imageWidth - Image width in pixels
293
+ * @param {number} imageHeight - Image height in pixels
294
+ * @returns {Object} Config with pixel values
295
+ */
296
+ export function percentToPixels(config, imageWidth, imageHeight) {
297
+ const converted = { ...config };
298
+
299
+ if (config.position) {
300
+ const pos = { ...config.position };
301
+
302
+ Object.keys(pos).forEach(key => {
303
+ if (typeof pos[key] === 'string' && pos[key].endsWith('%')) {
304
+ const percent = parseFloat(pos[key]);
305
+ const dimension = key === 'cx' || key === 'x' || key === 'width' ? imageWidth : imageHeight;
306
+ pos[key] = Math.round((percent / 100) * dimension);
307
+ }
308
+ });
309
+
310
+ converted.position = pos;
311
+ }
312
+
313
+ return converted;
314
+ }
315
+
316
+ /**
317
+ * Convert pixel-based config to percentage-based config
318
+ * @param {Object} config - Hotspot config with pixel values
319
+ * @param {number} imageWidth - Image width in pixels
320
+ * @param {number} imageHeight - Image height in pixels
321
+ * @returns {Object} Config with percentage values
322
+ */
323
+ export function pixelsToPercent(config, imageWidth, imageHeight) {
324
+ const converted = { ...config };
325
+
326
+ if (config.position) {
327
+ const pos = { ...config.position };
328
+
329
+ Object.keys(pos).forEach(key => {
330
+ if (typeof pos[key] === 'number') {
331
+ const dimension = key === 'cx' || key === 'x' || key === 'width' ? imageWidth : imageHeight;
332
+ const percent = ((pos[key] / dimension) * 100).toFixed(2);
333
+ pos[key] = `${percent}%`;
334
+ }
335
+ });
336
+
337
+ converted.position = pos;
338
+ }
339
+
340
+ return converted;
341
+ }