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,565 @@
1
+ /**
2
+ * stub-player/interactions-panel.js - Interactions panel component and handlers
3
+ *
4
+ * Shows standalone interaction definitions from slide files (NOT assessment questions).
5
+ * Provides viewing and inline editing of interaction properties.
6
+ */
7
+
8
+ import { escapeHtml, renderEditForm, saveItemEdits } from './edit-utils.js';
9
+
10
+ const PANEL_ICON_PATHS = {
11
+ interactions: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
12
+ list: '<path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/>',
13
+ target: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
14
+ slide: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/>',
15
+ navigate: '<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',
16
+ edit: '<path d="M13 21h8"/><path d="m15 5 4 4"/><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>',
17
+ close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'
18
+ };
19
+
20
+ function renderPanelIcon(name, className = 'panel-icon') {
21
+ const path = PANEL_ICON_PATHS[name];
22
+ if (!path) return '';
23
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${className}" aria-hidden="true">${path}</svg>`;
24
+ }
25
+
26
+
27
+ /**
28
+ * Generate the Interactions Panel HTML container
29
+ */
30
+ export function generateInteractionsPanel() {
31
+ return `
32
+ <div id="stub-player-interactions-panel">
33
+ <div id="stub-player-interactions-panel-header">
34
+ <h3>${renderPanelIcon('interactions')} Interactions</h3>
35
+ <button id="stub-player-interactions-panel-close">&times;</button>
36
+ </div>
37
+ <div id="stub-player-interactions-tabs">
38
+ <button class="active" data-tab="all">${renderPanelIcon('list', 'tab-icon')} <span>Interactions</span></button>
39
+ <button data-tab="assessments">${renderPanelIcon('target', 'tab-icon')} <span>Assessments</span></button>
40
+ <button data-tab="slide">${renderPanelIcon('slide', 'tab-icon')} <span>This Slide</span></button>
41
+ </div>
42
+ <div id="stub-player-interactions-body">
43
+ <div class="interactions-loading">Loading...</div>
44
+ </div>
45
+ </div>
46
+ `;
47
+ }
48
+
49
+ /**
50
+ * Create interactions panel handlers
51
+ * @param {Object} context - Shared context containing cmiData, navigateToSlide, etc.
52
+ * @returns {Object} Panel handler methods
53
+ */
54
+ export function createInteractionsPanelHandlers(context) {
55
+ const { getCmiData, navigateToSlide } = context;
56
+
57
+ let interactionsData = null;
58
+ let assessmentsData = null;
59
+ let interactionSchemas = {};
60
+ let currentTab = 'all';
61
+ let editingInteractionId = null;
62
+ let currentAssessmentId = null;
63
+
64
+ const interactionsPanel = document.getElementById('stub-player-interactions-panel');
65
+ const interactionsTabs = document.getElementById('stub-player-interactions-tabs');
66
+ const interactionsBody = document.getElementById('stub-player-interactions-body');
67
+ const closeBtn = document.getElementById('stub-player-interactions-panel-close');
68
+
69
+ // Setup tab handlers
70
+ function setupTabs() {
71
+ if (!interactionsTabs) return;
72
+ interactionsTabs.querySelectorAll('button').forEach(btn => {
73
+ btn.addEventListener('click', () => {
74
+ interactionsTabs.querySelectorAll('button').forEach(b => b.classList.remove('active'));
75
+ btn.classList.add('active');
76
+ setTab(btn.dataset.tab);
77
+ });
78
+ });
79
+ }
80
+
81
+ // Close button handler
82
+ if (closeBtn) {
83
+ closeBtn.addEventListener('click', () => {
84
+ interactionsPanel?.classList.remove('visible');
85
+ });
86
+ }
87
+
88
+ // Initialize tabs on creation
89
+ setupTabs();
90
+
91
+ async function loadInteractions() {
92
+ if (!interactionsBody) return;
93
+ interactionsBody.innerHTML = '<div class="interactions-loading">Loading...</div>';
94
+ try {
95
+ // Load manifest (contains slides & interactions) and assessments
96
+ const [manifestResponse, assessResponse, schemaResponse] = await Promise.all([
97
+ fetch('/_content-manifest.json'),
98
+ fetch('/__assessments'),
99
+ fetch('/__interaction-schemas')
100
+ ]);
101
+
102
+ if (schemaResponse.ok) {
103
+ const schemaData = await schemaResponse.json();
104
+ interactionSchemas = schemaData?.schemas || {};
105
+ }
106
+
107
+ if (manifestResponse.ok) {
108
+ const manifest = await manifestResponse.json();
109
+
110
+ // Flatten interactions from slides structure
111
+ const interactions = [];
112
+ if (manifest && manifest.slides) {
113
+ for (const [slideId, slideData] of Object.entries(manifest.slides)) {
114
+ if (slideData.interactions) {
115
+ for (const interaction of slideData.interactions) {
116
+ interactions.push({
117
+ ...interaction,
118
+ schema: interaction.schema || interactionSchemas[interaction.type] || null,
119
+ slideId // Ensure slideId is attached
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ interactionsData = { interactions };
127
+ }
128
+
129
+ if (assessResponse.ok) {
130
+ const data = await assessResponse.json();
131
+ assessmentsData = data.assessments || [];
132
+ }
133
+ render();
134
+ } catch (err) {
135
+ interactionsBody.innerHTML = '<div class="interactions-empty">Error: ' + err.message + '</div>';
136
+ }
137
+ }
138
+
139
+ function setTab(tab) {
140
+ currentTab = tab;
141
+ editingInteractionId = null;
142
+ render();
143
+ }
144
+
145
+ function render() {
146
+ if (!interactionsBody) return;
147
+
148
+ if (currentTab === 'slide') {
149
+ renderSlideTab();
150
+ } else if (currentTab === 'all') {
151
+ renderInteractionsTab();
152
+ } else if (currentTab === 'assessments') {
153
+ renderAssessmentsTab();
154
+ }
155
+ }
156
+
157
+ // =========================================================================
158
+ // THIS SLIDE TAB - Context-aware
159
+ // =========================================================================
160
+ function renderSlideTab() {
161
+ const cmiData = getCmiData();
162
+ const currentSlide = cmiData['cmi.location'] || '';
163
+
164
+ // Check if current slide is an assessment
165
+ const assessment = assessmentsData?.find(a => a.id === currentSlide);
166
+
167
+ if (assessment) {
168
+ // Show assessment details for this slide
169
+ renderSingleAssessment(assessment);
170
+ } else {
171
+ // Show interactions for this slide
172
+ const interactions = (interactionsData?.interactions || []).filter(i => i.slideId === currentSlide);
173
+
174
+ if (interactions.length === 0) {
175
+ interactionsBody.innerHTML = '<div class="interactions-empty">No interactions on current slide</div>';
176
+ return;
177
+ }
178
+
179
+ renderInteractionsList(interactions);
180
+ }
181
+ }
182
+
183
+ // =========================================================================
184
+ // INTERACTIONS TAB - All interactions
185
+ // =========================================================================
186
+ function renderInteractionsTab() {
187
+ const interactions = interactionsData?.interactions || [];
188
+
189
+ if (interactions.length === 0) {
190
+ interactionsBody.innerHTML = '<div class="interactions-empty">No standalone interactions found in course</div>';
191
+ return;
192
+ }
193
+
194
+ const grouped = new Map();
195
+ for (const interaction of interactions) {
196
+ const key = interaction.slideId || 'unknown-slide';
197
+ if (!grouped.has(key)) grouped.set(key, []);
198
+ grouped.get(key).push(interaction);
199
+ }
200
+
201
+ const slideIds = Array.from(grouped.keys()).sort((a, b) => a.localeCompare(b));
202
+ let html = '<div class="interaction-groups">';
203
+
204
+ for (const slideId of slideIds) {
205
+ const groupInteractions = grouped.get(slideId);
206
+ html += `
207
+ <section class="interaction-group">
208
+ <div class="interaction-group-header">
209
+ <div class="interaction-group-title-wrap">
210
+ ${renderPanelIcon('slide', 'group-icon')}
211
+ <h4 class="interaction-group-title">${escapeHtml(slideId)}</h4>
212
+ <span class="interaction-group-count">${groupInteractions.length}</span>
213
+ </div>
214
+ <button class="interaction-nav-btn with-label" data-slide="${escapeHtml(slideId)}" title="Go to slide">${renderPanelIcon('navigate', 'action-icon')} <span>Open</span></button>
215
+ </div>
216
+ <div class="interactions-list">
217
+ ${groupInteractions.map(interaction => renderInteractionCard(interaction)).join('')}
218
+ </div>
219
+ </section>
220
+ `;
221
+ }
222
+
223
+ html += '</div>';
224
+ interactionsBody.innerHTML = html;
225
+ attachInteractionHandlers();
226
+ }
227
+
228
+ function renderInteractionsList(interactions) {
229
+ let html = '<div class="interactions-list">';
230
+
231
+ for (const interaction of interactions) {
232
+ html += renderInteractionCard(interaction);
233
+ }
234
+
235
+ html += '</div>';
236
+ interactionsBody.innerHTML = html;
237
+ attachInteractionHandlers();
238
+ }
239
+
240
+ function attachInteractionHandlers() {
241
+ interactionsBody.querySelectorAll('.interaction-nav-btn').forEach(btn => {
242
+ btn.addEventListener('click', (e) => {
243
+ e.stopPropagation();
244
+ navigateToSlide(btn.dataset.slide);
245
+ });
246
+ });
247
+
248
+ interactionsBody.querySelectorAll('.interaction-edit-btn').forEach(btn => {
249
+ btn.addEventListener('click', (e) => {
250
+ e.stopPropagation();
251
+ const id = btn.dataset.id;
252
+ editingInteractionId = (editingInteractionId === id) ? null : id;
253
+ render();
254
+ });
255
+ });
256
+
257
+ interactionsBody.querySelectorAll('.edit-save-btn').forEach(btn => {
258
+ btn.addEventListener('click', async (e) => {
259
+ e.stopPropagation();
260
+ const card = btn.closest('.interaction-card');
261
+ const form = card.querySelector('.edit-form');
262
+ const interactionId = card.dataset.interactionId;
263
+ const slideId = card.dataset.slideId;
264
+ await saveItemEdits('/__edit-interaction', form, slideId, interactionId);
265
+ editingInteractionId = null;
266
+ await loadInteractions();
267
+ });
268
+ });
269
+
270
+ interactionsBody.querySelectorAll('.edit-cancel-btn').forEach(btn => {
271
+ btn.addEventListener('click', (e) => {
272
+ e.stopPropagation();
273
+ editingInteractionId = null;
274
+ render();
275
+ });
276
+ });
277
+ }
278
+
279
+ // =========================================================================
280
+ // ASSESSMENTS TAB - All assessments
281
+ // =========================================================================
282
+ function renderAssessmentsTab() {
283
+ if (!assessmentsData || assessmentsData.length === 0) {
284
+ interactionsBody.innerHTML = '<div class="interactions-empty">No assessments found</div>';
285
+ return;
286
+ }
287
+
288
+ // Ensure current selection is valid
289
+ if (!currentAssessmentId || !assessmentsData.find(a => a.id === currentAssessmentId)) {
290
+ currentAssessmentId = assessmentsData[0].id;
291
+ }
292
+
293
+ // Render assessment sub-tabs
294
+ let tabsHtml = '<div class="assessment-sub-tabs">';
295
+ for (const assessment of assessmentsData) {
296
+ const active = assessment.id === currentAssessmentId ? 'active' : '';
297
+ const title = assessment.title || assessment.id;
298
+ tabsHtml += `<button class="${active}" data-assessment-id="${escapeHtml(assessment.id)}">${escapeHtml(title)}</button>`;
299
+ }
300
+ tabsHtml += '</div>';
301
+
302
+ const assessment = assessmentsData.find(a => a.id === currentAssessmentId);
303
+ let bodyHtml = '';
304
+ if (assessment) {
305
+ bodyHtml = renderAssessmentDetails(assessment);
306
+ }
307
+
308
+ interactionsBody.innerHTML = tabsHtml + bodyHtml;
309
+
310
+ // Attach assessment sub-tab handlers
311
+ interactionsBody.querySelectorAll('.assessment-sub-tabs button').forEach(btn => {
312
+ btn.addEventListener('click', () => {
313
+ currentAssessmentId = btn.dataset.assessmentId;
314
+ editingInteractionId = null; // Clear editing state when switching tabs
315
+ renderAssessmentsTab();
316
+ });
317
+ });
318
+
319
+ // Attach all assessment handlers (nav, settings)
320
+ if (assessment) {
321
+ attachAssessmentHandlers(assessment);
322
+ attachInteractionHandlers(); // For question editing
323
+ }
324
+ }
325
+
326
+ function renderSingleAssessment(assessment) {
327
+ interactionsBody.innerHTML = renderAssessmentDetails(assessment);
328
+ attachAssessmentHandlers(assessment);
329
+ attachInteractionHandlers(); // For question editing
330
+ }
331
+
332
+ function attachAssessmentHandlers(assessment) {
333
+ const assessmentId = assessment.id;
334
+
335
+ // Attach navigation handler
336
+ interactionsBody.querySelectorAll('.interaction-nav-btn').forEach(btn => {
337
+ btn.addEventListener('click', () => {
338
+ navigateToSlide(btn.dataset.slide);
339
+ });
340
+ });
341
+
342
+ // Toggle click handlers (save immediately)
343
+ interactionsBody.querySelectorAll('.assessment-config-toggle[data-field]').forEach(toggle => {
344
+ toggle.addEventListener('click', async function () {
345
+ const isOn = this.classList.contains('on');
346
+ this.classList.toggle('on');
347
+ const field = this.dataset.field;
348
+ await saveAssessmentValue(assessmentId, field, !isOn);
349
+ });
350
+ });
351
+
352
+ // Input handlers (save with debounce)
353
+ interactionsBody.querySelectorAll('.assessment-config-input[data-field]').forEach(input => {
354
+ let timeout;
355
+ input.addEventListener('input', function () {
356
+ clearTimeout(timeout);
357
+ timeout = setTimeout(async () => {
358
+ const field = this.dataset.field;
359
+ const isArray = this.dataset.type === 'array';
360
+ let value;
361
+
362
+ if (isArray) {
363
+ const trimmed = this.value.trim();
364
+ value = trimmed ? trimmed.split(',').map(s => s.trim()).filter(Boolean) : [];
365
+ } else if (this.type === 'number') {
366
+ if (this.value === '' || this.value === 'null') {
367
+ value = null;
368
+ } else {
369
+ value = parseInt(this.value, 10);
370
+ }
371
+ } else {
372
+ value = this.value;
373
+ }
374
+
375
+ await saveAssessmentValue(assessmentId, field, value);
376
+ }, 500);
377
+ });
378
+ });
379
+ }
380
+
381
+ async function saveAssessmentValue(assessmentId, field, value) {
382
+ try {
383
+ const response = await fetch('/__edit-assessment', {
384
+ method: 'POST',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ body: JSON.stringify({ assessmentId, field, value })
387
+ });
388
+ if (!response.ok) {
389
+ const error = await response.json();
390
+ console.error('Assessment save error:', error);
391
+ }
392
+ } catch (err) {
393
+ console.error('Failed to save assessment setting:', err);
394
+ }
395
+ }
396
+
397
+ function renderAssessmentDetails(assessment) {
398
+ const settings = assessment.settings || assessment.config || {};
399
+ const questions = assessment.questions || [];
400
+
401
+ let html = `
402
+ <div class="assessment-info" data-assessment-id="${escapeHtml(assessment.id)}">
403
+ <h4>${escapeHtml(assessment.title || assessment.id)}</h4>
404
+ <div class="assessment-meta">
405
+ <span class="meta-item">ID: ${escapeHtml(assessment.id)}</span>
406
+ <button class="interaction-nav-btn with-label" data-slide="${escapeHtml(assessment.id)}" title="Go to start slide">${renderPanelIcon('navigate', 'action-icon')} <span>Go to Slide</span></button>
407
+ </div>
408
+ `;
409
+
410
+ // Settings section - always editable (inline pattern)
411
+ html += `<div class="config-section">
412
+ <div class="config-section-header">Settings</div>
413
+ <div class="config-row">
414
+ <span class="config-label">Passing Score</span>
415
+ <input type="number" class="config-input assessment-config-input assessment-config-score-input" data-field="passingScore" value="${settings.passingScore ?? 80}" min="0" max="100">
416
+ <span class="config-hint-inline">%</span>
417
+ </div>
418
+ <div class="config-row">
419
+ <span class="config-label">Allow Retake</span>
420
+ <div class="config-toggle assessment-config-toggle ${settings.allowRetake !== false ? 'on' : ''}" data-field="allowRetake"></div>
421
+ </div>
422
+ <div class="config-row">
423
+ <span class="config-label">Allow Review</span>
424
+ <div class="config-toggle assessment-config-toggle ${settings.allowReview !== false ? 'on' : ''}" data-field="allowReview"></div>
425
+ </div>
426
+ <div class="config-row">
427
+ <span class="config-label">Randomize Questions</span>
428
+ <div class="config-toggle assessment-config-toggle ${settings.randomizeQuestions ? 'on' : ''}" data-field="randomizeQuestions"></div>
429
+ </div>
430
+ <div class="config-row">
431
+ <span class="config-label">Randomize on Retake</span>
432
+ <div class="config-toggle assessment-config-toggle ${settings.randomizeOnRetake !== false ? 'on' : ''}" data-field="randomizeOnRetake"></div>
433
+ </div>
434
+ <div class="config-row">
435
+ <span class="config-label">Show Progress</span>
436
+ <div class="config-toggle assessment-config-toggle ${settings.showProgress !== false ? 'on' : ''}" data-field="showProgress"></div>
437
+ </div>
438
+ <div class="config-row">
439
+ <span class="config-label">Attempts Before Remedial</span>
440
+ <input type="number" class="config-input assessment-config-input assessment-config-attempts-input" data-field="attemptsBeforeRemedial" value="${settings.attemptsBeforeRemedial ?? ''}" min="1" placeholder="unlimited">
441
+ </div>
442
+ <div class="config-row">
443
+ <span class="config-label">Attempts Before Restart</span>
444
+ <input type="number" class="config-input assessment-config-input assessment-config-attempts-input" data-field="attemptsBeforeRestart" value="${settings.attemptsBeforeRestart ?? ''}" min="1" placeholder="unlimited">
445
+ </div>
446
+ <div class="config-row">
447
+ <span class="config-label">Remedial Slides</span>
448
+ <input type="text" class="config-input assessment-config-input assessment-config-remedial-input" data-field="remedialSlideIds" data-type="array" value="${settings.remedialSlideIds ? settings.remedialSlideIds.join(', ') : ''}" placeholder="slide-1, slide-2">
449
+ </div>
450
+ </div>`;
451
+
452
+ // Questions List - reuse interaction card components
453
+ if (questions.length > 0) {
454
+ html += `
455
+ <section class="interaction-group assessment-interactions-section">
456
+ <div class="interaction-group-header">
457
+ <div class="interaction-group-title-wrap">
458
+ ${renderPanelIcon('list', 'group-icon')}
459
+ <h4 class="interaction-group-title">Questions</h4>
460
+ <span class="interaction-group-count">${questions.length}</span>
461
+ </div>
462
+ </div>
463
+ <div class="interactions-list assessment-interactions-list">
464
+ `;
465
+
466
+ for (const q of questions) {
467
+ // Preserve full question payload so schema editors can bind correctly.
468
+ const interaction = {
469
+ ...q,
470
+ schema: q.schema || interactionSchemas[q.type] || null,
471
+ slideId: assessment.id,
472
+ };
473
+ html += renderInteractionCard(interaction, q.weight);
474
+ }
475
+
476
+ html += '</div></section>';
477
+ } else {
478
+ html += '<div class="interactions-empty">No questions defined</div>';
479
+ }
480
+
481
+ html += '</div>'; // End assessment-info
482
+ return html;
483
+ }
484
+
485
+ // Shared helper for rendering a single interaction/question card
486
+ function renderInteractionCard(interaction, weight = null) {
487
+ const isEditing = editingInteractionId === interaction.id;
488
+
489
+ let html = `<div class="interaction-card${isEditing ? ' editing' : ''}" data-interaction-id="${interaction.id}" data-slide-id="${interaction.slideId}">
490
+ <div class="interaction-header">
491
+ <div class="interaction-header-left">
492
+ <span class="interaction-type">${interaction.type}</span>
493
+ <span class="interaction-id">${interaction.id}</span>
494
+ ${weight !== null ? `<span class="interaction-weight">Weight: ${weight}</span>` : ''}
495
+ </div>
496
+ <div class="interaction-header-right">
497
+ <button class="interaction-edit-btn icon-btn" data-id="${interaction.id}" title="${isEditing ? 'Cancel edit' : 'Edit interaction'}">${isEditing ? renderPanelIcon('close', 'action-icon') : renderPanelIcon('edit', 'action-icon')}</button>
498
+ <button class="interaction-nav-btn icon-btn" data-slide="${interaction.slideId}" title="Go to slide">${renderPanelIcon('navigate', 'action-icon')}</button>
499
+ </div>
500
+ </div>`;
501
+
502
+ if (isEditing) {
503
+ const schema = interaction.schema;
504
+ if (!schema) {
505
+ console.warn(
506
+ '[InteractionsPanel] Schema not found for interaction type "' + interaction.type + '". ' +
507
+ 'Falling back to legacy editor for interaction "' + interaction.id + '" on slide "' + interaction.slideId + '".'
508
+ );
509
+ }
510
+ html += renderEditForm(interaction, schema || null);
511
+ } else {
512
+ if (interaction.label) {
513
+ html += `<div class="interaction-label">${escapeHtml(interaction.label)}</div>`;
514
+ }
515
+
516
+ if (interaction.prompt) {
517
+ html += `<div class="interaction-prompt">${escapeHtml(interaction.prompt)}</div>`;
518
+ }
519
+
520
+ if (interaction.correctAnswer !== undefined && interaction.correctAnswer !== null) {
521
+ html += `<div class="interaction-detail">
522
+ <span class="detail-label">Correct Answer:</span>
523
+ <span class="detail-value">${escapeHtml(String(interaction.correctAnswer))}</span>
524
+ </div>`;
525
+ }
526
+
527
+ if (interaction.choices && interaction.choices.length > 0) {
528
+ html += '<div class="interaction-choices">';
529
+ html += '<span class="detail-label">Choices:</span><ul>';
530
+ for (const c of interaction.choices) {
531
+ const marker = c.correct ? ' ✓' : '';
532
+ html += `<li>${escapeHtml(c.value)}: ${escapeHtml(c.text)}${marker}</li>`;
533
+ }
534
+ html += '</ul></div>';
535
+ }
536
+
537
+ if (interaction.pairs && interaction.pairs.length > 0) {
538
+ html += '<div class="interaction-pairs">';
539
+ html += '<span class="detail-label">Pairs:</span><ul>';
540
+ for (const p of interaction.pairs) {
541
+ html += `<li>${escapeHtml(p.text)} → ${escapeHtml(p.match)}</li>`;
542
+ }
543
+ html += '</ul></div>';
544
+ }
545
+ }
546
+
547
+ html += '</div>';
548
+ return html;
549
+ }
550
+
551
+ function onLocationChange() {
552
+ // Re-render if on "This Slide" tab
553
+ if (currentTab === 'slide' && (interactionsData || assessmentsData)) {
554
+ render();
555
+ }
556
+ }
557
+
558
+ return {
559
+ loadInteractions,
560
+ render,
561
+ setTab,
562
+ onLocationChange,
563
+ get data() { return interactionsData; }
564
+ };
565
+ }