coursecode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- package/template/vite.config.js +339 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stub-player/content-viewer.js - Content viewer panel component
|
|
3
|
+
*
|
|
4
|
+
* Generates the content viewer panel HTML and handles client-side interactions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate content viewer panel HTML
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {boolean} options.isLive - Whether this is live mode (shows refresh button)
|
|
11
|
+
* Note: Content is populated dynamically by JS, this just creates the container
|
|
12
|
+
*/
|
|
13
|
+
export function generateContentViewer({ isLive }) {
|
|
14
|
+
return `
|
|
15
|
+
<div id="stub-player-content-panel">
|
|
16
|
+
<div id="stub-player-content-resize-handle"></div>
|
|
17
|
+
<div id="stub-player-content-panel-header">
|
|
18
|
+
<h3>📄 Course Review</h3>
|
|
19
|
+
<div class="header-buttons">
|
|
20
|
+
<label class="checkbox-label" title="Show narration text">
|
|
21
|
+
<input type="checkbox" id="stub-player-show-narration" checked> Narration
|
|
22
|
+
</label>
|
|
23
|
+
${isLive ? '<button id="stub-player-content-refresh" title="Refresh content from source">🔄 Refresh</button>' : ''}
|
|
24
|
+
<button id="stub-player-content-print" title="Print content">🖨️ Print</button>
|
|
25
|
+
<button id="stub-player-content-new-window" title="Open in new window">↗ New Window</button>
|
|
26
|
+
<button id="stub-player-content-panel-close">Close</button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div id="stub-player-content-body">
|
|
30
|
+
<div class="content-loading">Loading content...</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Initialize Content Viewer Handlers
|
|
38
|
+
*/
|
|
39
|
+
export function createContentViewerHandlers(context) {
|
|
40
|
+
// isLive comes from context when passed by app.js (detected from window.location)
|
|
41
|
+
|
|
42
|
+
// Panel elements
|
|
43
|
+
const contentPanel = document.getElementById('stub-player-content-panel');
|
|
44
|
+
const contentBody = document.getElementById('stub-player-content-body');
|
|
45
|
+
const closeBtn = document.getElementById('stub-player-content-panel-close');
|
|
46
|
+
const refreshBtn = document.getElementById('stub-player-content-refresh');
|
|
47
|
+
const printBtn = document.getElementById('stub-player-content-print');
|
|
48
|
+
const newWindowBtn = document.getElementById('stub-player-content-new-window');
|
|
49
|
+
const showNarrationCheck = document.getElementById('stub-player-show-narration');
|
|
50
|
+
const resizeHandle = document.getElementById('stub-player-content-resize-handle');
|
|
51
|
+
|
|
52
|
+
// Close handler
|
|
53
|
+
if (closeBtn) {
|
|
54
|
+
closeBtn.addEventListener('click', () => {
|
|
55
|
+
contentPanel.classList.remove('visible');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Refresh handler
|
|
60
|
+
if (refreshBtn) {
|
|
61
|
+
refreshBtn.addEventListener('click', () => {
|
|
62
|
+
loadContent();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Print handler
|
|
67
|
+
if (printBtn) {
|
|
68
|
+
printBtn.addEventListener('click', () => {
|
|
69
|
+
const printContent = contentBody.innerHTML;
|
|
70
|
+
const printWin = window.open('', '_blank');
|
|
71
|
+
printWin.document.write(`
|
|
72
|
+
<html>
|
|
73
|
+
<head>
|
|
74
|
+
<title>Course Content Review</title>
|
|
75
|
+
<link rel="stylesheet" href="/__stub-player/styles.css">
|
|
76
|
+
<style>
|
|
77
|
+
body { padding: 20px; font-family: sans-serif; }
|
|
78
|
+
@media print {
|
|
79
|
+
button { display: none; }
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
${printContent}
|
|
85
|
+
<script>
|
|
86
|
+
window.onload = function() { window.print(); window.close(); }
|
|
87
|
+
</script>
|
|
88
|
+
</body>
|
|
89
|
+
</html>
|
|
90
|
+
`);
|
|
91
|
+
printWin.document.close();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// New Window handler
|
|
96
|
+
if (newWindowBtn) {
|
|
97
|
+
newWindowBtn.addEventListener('click', () => {
|
|
98
|
+
window.open('/__content-view', '_blank');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Narration toggle
|
|
103
|
+
if (showNarrationCheck) {
|
|
104
|
+
showNarrationCheck.addEventListener('change', (e) => {
|
|
105
|
+
if (contentBody) {
|
|
106
|
+
contentBody.classList.toggle('hide-narration', !e.target.checked);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Resize Handler
|
|
112
|
+
if (resizeHandle) {
|
|
113
|
+
let startX, startWidth;
|
|
114
|
+
|
|
115
|
+
resizeHandle.addEventListener('mousedown', (e) => {
|
|
116
|
+
isResizing = true;
|
|
117
|
+
startX = e.clientX;
|
|
118
|
+
startWidth = parseInt(document.defaultView.getComputedStyle(contentPanel).width, 10);
|
|
119
|
+
document.documentElement.addEventListener('mousemove', doDrag, false);
|
|
120
|
+
document.documentElement.addEventListener('mouseup', stopDrag, false);
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
function doDrag(e) {
|
|
125
|
+
const width = startWidth - (e.clientX - startX); // Dragging left increases width
|
|
126
|
+
if (width > 300 && width < window.innerWidth - 100) {
|
|
127
|
+
contentPanel.style.width = width + 'px';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function stopDrag() {
|
|
132
|
+
isResizing = false;
|
|
133
|
+
document.documentElement.removeEventListener('mousemove', doDrag, false);
|
|
134
|
+
document.documentElement.removeEventListener('mouseup', stopDrag, false);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Core load function
|
|
139
|
+
async function loadContent() {
|
|
140
|
+
if (!contentBody) return;
|
|
141
|
+
|
|
142
|
+
// Use preloaded content if available (for static exports)
|
|
143
|
+
if (context.initialContent) {
|
|
144
|
+
contentBody.innerHTML = context.initialContent;
|
|
145
|
+
context.initialContent = null; // Clear so subsequent refreshes (if any) might fetch?
|
|
146
|
+
// Actually in static export refresh doesn't make sense unless refreshing from... what?
|
|
147
|
+
// If live, we probably prefer fetch.
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const showNarration = showNarrationCheck ? showNarrationCheck.checked : true;
|
|
152
|
+
|
|
153
|
+
contentBody.innerHTML = '<div class="content-loading">Loading content...</div>';
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetch('/__content?includeNarration=' + showNarration);
|
|
157
|
+
if (response.ok) {
|
|
158
|
+
const html = await response.text();
|
|
159
|
+
contentBody.innerHTML = html;
|
|
160
|
+
// Re-apply narration visibility class
|
|
161
|
+
contentBody.classList.toggle('hide-narration', !showNarration);
|
|
162
|
+
} else {
|
|
163
|
+
contentBody.innerHTML = '<div class="content-error">Failed to load content</div>';
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
contentBody.innerHTML = '<div class="content-error">Error: ' + err.message + '</div>';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
loadContent
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stub-player/debug-panel.js - Debug panel component
|
|
3
|
+
*
|
|
4
|
+
* Generates the debug panel HTML with tabs for state, API log, xAPI, and errors.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate debug panel HTML
|
|
9
|
+
*/
|
|
10
|
+
export function generateDebugPanel() {
|
|
11
|
+
return `
|
|
12
|
+
<div id="stub-player-debug-panel">
|
|
13
|
+
<div id="stub-player-debug-panel-header">
|
|
14
|
+
<h3>LMS Debug <span id="stub-player-format-badge" class="format-badge"></span></h3>
|
|
15
|
+
<button id="stub-player-debug-panel-close">×</button>
|
|
16
|
+
</div>
|
|
17
|
+
<div id="stub-player-debug-tabs">
|
|
18
|
+
<button class="active" data-tab="state">State</button>
|
|
19
|
+
<button data-tab="api-log">API Log</button>
|
|
20
|
+
<button id="stub-player-xapi-tab" data-tab="xapi">xAPI <span id="stub-player-xapi-badge"></span></button>
|
|
21
|
+
<button data-tab="errors">Errors <span id="stub-player-error-badge"></span></button>
|
|
22
|
+
</div>
|
|
23
|
+
<div id="stub-player-debug-content">
|
|
24
|
+
<div id="tab-state">
|
|
25
|
+
<div class="debug-section" id="section-resume">
|
|
26
|
+
<div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
27
|
+
<span class="toggle-icon">▼</span>
|
|
28
|
+
<h4>Resume State</h4>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="debug-section-content">
|
|
31
|
+
<table class="debug-table" id="stub-player-resume-data"></table>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="debug-section" id="section-score">
|
|
35
|
+
<div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
36
|
+
<span class="toggle-icon">▼</span>
|
|
37
|
+
<h4>Score</h4>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="debug-section-content">
|
|
40
|
+
<table class="debug-table" id="stub-player-score-data"></table>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="debug-section collapsed" id="section-objectives">
|
|
44
|
+
<div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
45
|
+
<span class="toggle-icon">▼</span>
|
|
46
|
+
<h4>Objectives</h4>
|
|
47
|
+
<span class="count-badge" id="stub-player-objectives-count">0</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="debug-section-content">
|
|
50
|
+
<div id="stub-player-objectives-data"></div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="debug-section collapsed" id="section-interactions">
|
|
54
|
+
<div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
55
|
+
<span class="toggle-icon">▼</span>
|
|
56
|
+
<h4>Interactions</h4>
|
|
57
|
+
<span class="count-badge" id="stub-player-interactions-count">0</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="debug-section-content">
|
|
60
|
+
<div id="stub-player-interactions-data"></div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="debug-section" id="section-suspend">
|
|
64
|
+
<div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
65
|
+
<span class="toggle-icon">▼</span>
|
|
66
|
+
<h4>Suspend Data</h4>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="debug-section-content">
|
|
69
|
+
<div class="suspend-info" id="stub-player-suspend-info"></div>
|
|
70
|
+
<pre id="stub-player-suspend-data" style="background:#252542;padding:8px;border-radius:4px;overflow-x:auto;font-size:10px;max-height:200px;overflow-y:auto;"></pre>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="debug-section-actions">
|
|
74
|
+
<button id="stub-player-resume-test-btn" class="debug-action-btn" title="Simulate session end and test resume">🔄 Test Resume</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div id="tab-api-log" style="display:none;">
|
|
78
|
+
<div id="stub-player-api-log"></div>
|
|
79
|
+
</div>
|
|
80
|
+
<div id="tab-xapi" style="display:none;">
|
|
81
|
+
<div id="stub-player-xapi-log">
|
|
82
|
+
<div id="stub-player-no-statements">No xAPI statements yet. Navigate through the course to see statements.</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div id="tab-errors" style="display:none;">
|
|
86
|
+
<div id="stub-player-error-actions" style="display:none;"><button id="stub-player-copy-all-errors" class="debug-action-btn" title="Copy all errors to clipboard">📋 Copy All</button></div>
|
|
87
|
+
<div id="stub-player-error-log">
|
|
88
|
+
<div id="stub-player-no-errors">✓ No errors or warnings</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize Client-Side Handlers
|
|
98
|
+
*/
|
|
99
|
+
import { cmiData, apiLog, errorLog, xapiLog, saveState, logApiCall, logError, setUiCallbacks } from './lms-api.js';
|
|
100
|
+
|
|
101
|
+
function escapeHtmlInline(str) {
|
|
102
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createDebugPanelHandlers() {
|
|
106
|
+
// Tab switching
|
|
107
|
+
const tabs = document.querySelectorAll('#stub-player-debug-tabs button');
|
|
108
|
+
tabs.forEach(tab => {
|
|
109
|
+
tab.addEventListener('click', () => {
|
|
110
|
+
tabs.forEach(t => t.classList.remove('active'));
|
|
111
|
+
tab.classList.add('active');
|
|
112
|
+
|
|
113
|
+
const target = tab.dataset.tab;
|
|
114
|
+
['state', 'api-log', 'xapi', 'errors'].forEach(id => {
|
|
115
|
+
document.getElementById('tab-' + id).style.display = id === target ? 'block' : 'none';
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Event delegation for xAPI entry expansion
|
|
121
|
+
const xapiContainer = document.getElementById('stub-player-xapi-log');
|
|
122
|
+
if (xapiContainer) {
|
|
123
|
+
xapiContainer.addEventListener('click', function (e) {
|
|
124
|
+
const entry = e.target.closest('.xapi-entry');
|
|
125
|
+
if (entry) entry.classList.toggle('expanded');
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Register callbacks with LMS API to update UI on changes
|
|
130
|
+
setUiCallbacks({
|
|
131
|
+
onStateChange: (type, data) => {
|
|
132
|
+
if (type === 'format') updateFormatBadge(data);
|
|
133
|
+
else updateDebugDisplay();
|
|
134
|
+
},
|
|
135
|
+
onApiLog: updateApiLogDisplay,
|
|
136
|
+
onErrorLog: updateErrorDisplay,
|
|
137
|
+
onXapiLog: updateXapiDisplay
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Test Resume Button
|
|
141
|
+
document.getElementById('stub-player-resume-test-btn')?.addEventListener('click', () => {
|
|
142
|
+
// Ensure exit is set to suspend
|
|
143
|
+
if (!cmiData['cmi.exit'] || cmiData['cmi.exit'] === '') {
|
|
144
|
+
logError('Resume Warning', 'cmi.exit was empty - setting to "suspend".', 'Course should set cmi.exit before Terminate', true);
|
|
145
|
+
cmiData['cmi.exit'] = 'suspend';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Clear session time and set entry to resume
|
|
149
|
+
cmiData['cmi.session_time'] = 'PT0H0M0S';
|
|
150
|
+
cmiData['cmi.entry'] = 'resume';
|
|
151
|
+
|
|
152
|
+
saveState();
|
|
153
|
+
|
|
154
|
+
logApiCall('LMS Commit', 'Session ended', JSON.stringify({
|
|
155
|
+
location: cmiData['cmi.location'],
|
|
156
|
+
completion: cmiData['cmi.completion_status'],
|
|
157
|
+
exit: cmiData['cmi.exit']
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
// Reload page to simulate new session
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
const frame = document.getElementById('stub-player-course-frame');
|
|
163
|
+
frame.src = 'about:blank';
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
window.location.reload();
|
|
166
|
+
}, 100);
|
|
167
|
+
}, 200);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Close button handler
|
|
171
|
+
document.getElementById('stub-player-debug-panel-close')?.addEventListener('click', () => {
|
|
172
|
+
document.getElementById('stub-player-debug-panel')?.classList.remove('visible');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Initial render
|
|
176
|
+
updateDebugDisplay();
|
|
177
|
+
updateApiLogDisplay();
|
|
178
|
+
updateErrorDisplay();
|
|
179
|
+
updateXapiDisplay();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function updateFormatBadge(format) {
|
|
183
|
+
const badge = document.getElementById('stub-player-format-badge');
|
|
184
|
+
const xapiTab = document.getElementById('stub-player-xapi-tab');
|
|
185
|
+
if (badge) {
|
|
186
|
+
badge.className = 'format-badge' + (format === 'scorm12' ? ' scorm12' : format === 'cmi5' ? ' cmi5' : format === 'lti' ? ' lti' : '');
|
|
187
|
+
badge.textContent = { scorm2004: 'SCORM 2004', 'scorm12': 'SCORM 1.2', cmi5: 'cmi5', lti: 'LTI' }[format] || format;
|
|
188
|
+
}
|
|
189
|
+
if (xapiTab) {
|
|
190
|
+
xapiTab.classList.toggle('visible', format === 'cmi5');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getValueClass(key, value) {
|
|
195
|
+
if (!value || value === '-') return 'value-empty';
|
|
196
|
+
if (key === 'cmi.entry') return value === 'resume' ? 'value-resume' : 'value-ab-initio';
|
|
197
|
+
if (key === 'cmi.completion_status') return value === 'completed' ? 'value-completed' : value === 'incomplete' ? 'value-incomplete' : 'value-unknown';
|
|
198
|
+
if (key === 'cmi.success_status') return value === 'passed' ? 'value-passed' : value === 'failed' ? 'value-failed' : 'value-unknown';
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatBytes(bytes) {
|
|
203
|
+
if (bytes < 1024) return bytes + ' B';
|
|
204
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function updateDebugDisplay() {
|
|
208
|
+
const resumeTable = document.getElementById('stub-player-resume-data');
|
|
209
|
+
const scoreTable = document.getElementById('stub-player-score-data');
|
|
210
|
+
const objectivesData = document.getElementById('stub-player-objectives-data');
|
|
211
|
+
const interactionsData = document.getElementById('stub-player-interactions-data');
|
|
212
|
+
const suspendPre = document.getElementById('stub-player-suspend-data');
|
|
213
|
+
const suspendInfo = document.getElementById('stub-player-suspend-info');
|
|
214
|
+
|
|
215
|
+
if (!resumeTable) return;
|
|
216
|
+
|
|
217
|
+
// Resume State section
|
|
218
|
+
const resumeKeys = [
|
|
219
|
+
{ key: 'cmi.entry', label: 'entry' },
|
|
220
|
+
{ key: 'cmi.exit', label: 'exit' },
|
|
221
|
+
{ key: 'cmi.location', label: 'location' },
|
|
222
|
+
{ key: 'cmi.completion_status', label: 'completion' },
|
|
223
|
+
{ key: 'cmi.success_status', label: 'success' },
|
|
224
|
+
{ key: 'cmi.session_time', label: 'session_time' },
|
|
225
|
+
{ key: 'cmi.total_time', label: 'total_time' }
|
|
226
|
+
];
|
|
227
|
+
resumeTable.innerHTML = resumeKeys.map(({ key, label }) => {
|
|
228
|
+
const value = cmiData[key] || '-';
|
|
229
|
+
const cls = getValueClass(key, value);
|
|
230
|
+
return '<tr><td>' + label + '</td><td class="' + cls + '">' + (value || '<span class="value-empty">(empty)</span>') + '</td></tr>';
|
|
231
|
+
}).join('');
|
|
232
|
+
|
|
233
|
+
// Score section
|
|
234
|
+
const scoreKeys = [
|
|
235
|
+
{ key: 'cmi.score.raw', label: 'raw' },
|
|
236
|
+
{ key: 'cmi.score.scaled', label: 'scaled' },
|
|
237
|
+
{ key: 'cmi.score.min', label: 'min' },
|
|
238
|
+
{ key: 'cmi.score.max', label: 'max' }
|
|
239
|
+
];
|
|
240
|
+
scoreTable.innerHTML = scoreKeys.map(({ key, label }) => {
|
|
241
|
+
const value = cmiData[key];
|
|
242
|
+
return '<tr><td>' + label + '</td><td>' + (value !== '' && value !== undefined ? value : '<span class="value-empty">-</span>') + '</td></tr>';
|
|
243
|
+
}).join('');
|
|
244
|
+
|
|
245
|
+
// Objectives section
|
|
246
|
+
const objectives = cmiData._objectives || {};
|
|
247
|
+
const objList = Object.values(objectives);
|
|
248
|
+
document.getElementById('stub-player-objectives-count').textContent = objList.length;
|
|
249
|
+
if (objList.length === 0) {
|
|
250
|
+
objectivesData.innerHTML = '<span class="value-empty">No objectives recorded</span>';
|
|
251
|
+
} else {
|
|
252
|
+
objectivesData.innerHTML = '<table class="data-table"><thead><tr><th>ID</th><th>Completion</th><th>Success</th><th>Score</th></tr></thead><tbody>' +
|
|
253
|
+
objList.map(obj => {
|
|
254
|
+
const completion = obj.completion_status || '-';
|
|
255
|
+
const success = obj.success_status || '-';
|
|
256
|
+
const score = obj.score_scaled !== undefined ? obj.score_scaled : '-';
|
|
257
|
+
return '<tr><td>' + (obj.id || '-') + '</td><td>' + completion + '</td><td>' + success + '</td><td>' + score + '</td></tr>';
|
|
258
|
+
}).join('') + '</tbody></table>';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Interactions section
|
|
262
|
+
const interactions = cmiData._interactions || [];
|
|
263
|
+
document.getElementById('stub-player-interactions-count').textContent = interactions.length;
|
|
264
|
+
if (interactions.length === 0) {
|
|
265
|
+
interactionsData.innerHTML = '<span class="value-empty">No interactions recorded</span>';
|
|
266
|
+
} else {
|
|
267
|
+
interactionsData.innerHTML = '<table class="data-table"><thead><tr><th>ID</th><th>Type</th><th>Response</th><th>Result</th></tr></thead><tbody>' +
|
|
268
|
+
interactions.map(int => {
|
|
269
|
+
const result = int.result || '-';
|
|
270
|
+
const resultClass = result === 'correct' ? 'result-correct' : result === 'incorrect' || result === 'wrong' ? 'result-incorrect' : '';
|
|
271
|
+
return '<tr><td title="' + escapeHtmlInline(int.id || '') + '">' + escapeHtmlInline((int.id || '-').substring(0, 20)) + '</td><td>' +
|
|
272
|
+
(int.type || '-') + '</td><td title="' + escapeHtmlInline(int.learner_response || int.student_response || '') + '">' +
|
|
273
|
+
escapeHtmlInline(((int.learner_response || int.student_response || '-') + '').substring(0, 15)) + '</td><td class="' + resultClass + '">' + result + '</td></tr>';
|
|
274
|
+
}).join('') + '</tbody></table>';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Suspend Data section
|
|
278
|
+
try {
|
|
279
|
+
const suspend = cmiData['cmi.suspend_data'];
|
|
280
|
+
if (suspend) {
|
|
281
|
+
const rawBytes = suspend.length;
|
|
282
|
+
let decoded;
|
|
283
|
+
try { decoded = JSON.parse(suspend); } catch { decoded = null; }
|
|
284
|
+
|
|
285
|
+
if (decoded) {
|
|
286
|
+
const prettyJson = JSON.stringify(decoded, null, 2);
|
|
287
|
+
suspendInfo.innerHTML = 'Size: <span class="size">' + formatBytes(rawBytes) + '</span>';
|
|
288
|
+
suspendPre.textContent = prettyJson;
|
|
289
|
+
} else {
|
|
290
|
+
suspendInfo.innerHTML = 'Size: <span class="size">' + formatBytes(rawBytes) + '</span> <span style="color:#f59e0b">(raw/non-JSON)</span>';
|
|
291
|
+
suspendPre.textContent = suspend;
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
suspendInfo.innerHTML = '';
|
|
295
|
+
suspendPre.textContent = '(empty)';
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
suspendInfo.innerHTML = '';
|
|
299
|
+
suspendPre.textContent = cmiData['cmi.suspend_data'] || '(empty)';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function updateApiLogDisplay() {
|
|
304
|
+
const logEl = document.getElementById('stub-player-api-log');
|
|
305
|
+
if (!logEl) return;
|
|
306
|
+
logEl.innerHTML = apiLog.map(entry =>
|
|
307
|
+
'<div class="log-entry' + (entry.isError ? ' error' : '') + '">' +
|
|
308
|
+
'<span class="timestamp">' + entry.timestamp + '</span>' +
|
|
309
|
+
'<span class="method">' + entry.method + '</span>' +
|
|
310
|
+
(entry.args ? '<span class="args">' + escapeHtmlInline(entry.args) + '</span>' : '') +
|
|
311
|
+
'<span class="result">→ ' + escapeHtmlInline(entry.result) + '</span>' +
|
|
312
|
+
'</div>'
|
|
313
|
+
).join('');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function updateErrorDisplay() {
|
|
317
|
+
const logEl = document.getElementById('stub-player-error-log');
|
|
318
|
+
const badge = document.getElementById('stub-player-error-badge');
|
|
319
|
+
const inlineBadge = document.getElementById('stub-player-header-error-badge-inline');
|
|
320
|
+
if (!logEl) return;
|
|
321
|
+
|
|
322
|
+
const errors = errorLog.filter(e => !e.isWarning);
|
|
323
|
+
const warnings = errorLog.filter(e => e.isWarning);
|
|
324
|
+
const totalCount = errorLog.length;
|
|
325
|
+
if (inlineBadge) {
|
|
326
|
+
if (totalCount > 0) {
|
|
327
|
+
inlineBadge.textContent = totalCount > 99 ? '99+' : totalCount;
|
|
328
|
+
inlineBadge.classList.add('visible');
|
|
329
|
+
inlineBadge.classList.toggle('has-warnings', errors.length === 0 && warnings.length > 0);
|
|
330
|
+
} else {
|
|
331
|
+
inlineBadge.classList.remove('visible');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Show/hide Copy All button
|
|
336
|
+
const actionsEl = document.getElementById('stub-player-error-actions');
|
|
337
|
+
if (actionsEl) actionsEl.style.display = errorLog.length > 0 ? 'flex' : 'none';
|
|
338
|
+
|
|
339
|
+
if (errorLog.length === 0) {
|
|
340
|
+
logEl.innerHTML = '<div id="stub-player-no-errors">✓ No errors or warnings</div>';
|
|
341
|
+
badge.innerHTML = '';
|
|
342
|
+
} else {
|
|
343
|
+
logEl.innerHTML = errorLog.map((entry, index) => {
|
|
344
|
+
return '<div class="error-entry' + (entry.isWarning ? ' warning' : '') + '">' +
|
|
345
|
+
'<button class="error-copy-btn" data-error-index="' + index + '" title="Copy to clipboard">📋</button>' +
|
|
346
|
+
'<div class="error-type">' + escapeHtmlInline(entry.type) + '</div>' +
|
|
347
|
+
'<div class="error-message">' + escapeHtmlInline(entry.message) + '</div>' +
|
|
348
|
+
(entry.hint ? '<div class="error-hint">' + escapeHtmlInline(entry.hint) + '</div>' : '') +
|
|
349
|
+
'</div>';
|
|
350
|
+
}).join('');
|
|
351
|
+
|
|
352
|
+
// Add click handlers for individual copy buttons
|
|
353
|
+
logEl.querySelectorAll('.error-copy-btn').forEach(btn => {
|
|
354
|
+
btn.addEventListener('click', (e) => {
|
|
355
|
+
e.stopPropagation();
|
|
356
|
+
const idx = parseInt(btn.dataset.errorIndex, 10);
|
|
357
|
+
const entry = errorLog[idx];
|
|
358
|
+
if (entry) {
|
|
359
|
+
const text = `${entry.type}: ${entry.message}${entry.hint ? '\nHint: ' + entry.hint : ''}`;
|
|
360
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
361
|
+
btn.textContent = '✓';
|
|
362
|
+
setTimeout(() => { btn.textContent = '📋'; }, 1500);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Copy All button handler
|
|
369
|
+
const copyAllBtn = document.getElementById('stub-player-copy-all-errors');
|
|
370
|
+
if (copyAllBtn) {
|
|
371
|
+
copyAllBtn.onclick = () => {
|
|
372
|
+
const allText = errorLog.map(entry => {
|
|
373
|
+
const prefix = entry.isWarning ? 'WARNING' : 'ERROR';
|
|
374
|
+
return `[${prefix}] ${entry.type}: ${entry.message}${entry.hint ? '\nHint: ' + entry.hint : ''}`;
|
|
375
|
+
}).join('\n\n');
|
|
376
|
+
navigator.clipboard.writeText(allText).then(() => {
|
|
377
|
+
copyAllBtn.textContent = '✓ Copied';
|
|
378
|
+
setTimeout(() => { copyAllBtn.textContent = '📋 Copy All'; }, 1500);
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let badgeHtml = '';
|
|
384
|
+
if (errors.length > 0) badgeHtml += '<span class="error-count-badge">' + errors.length + '</span>';
|
|
385
|
+
if (warnings.length > 0) badgeHtml += '<span class="warning-count-badge">' + warnings.length + '</span>';
|
|
386
|
+
badge.innerHTML = badgeHtml;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function updateXapiDisplay() {
|
|
391
|
+
const logEl = document.getElementById('stub-player-xapi-log');
|
|
392
|
+
const badge = document.getElementById('stub-player-xapi-badge');
|
|
393
|
+
if (!logEl) return;
|
|
394
|
+
|
|
395
|
+
if (xapiLog.length === 0) {
|
|
396
|
+
logEl.innerHTML = '<div id="stub-player-no-statements">No xAPI statements yet. Navigate through the course to see statements.</div>';
|
|
397
|
+
if (badge) badge.innerHTML = '';
|
|
398
|
+
} else {
|
|
399
|
+
logEl.innerHTML = xapiLog.map(entry => {
|
|
400
|
+
const verb = entry.type || 'unknown';
|
|
401
|
+
const data = entry.data || {};
|
|
402
|
+
const objectId = data.id || data.objectiveId || data.assessmentId || '-';
|
|
403
|
+
const details = JSON.stringify(data, null, 2);
|
|
404
|
+
|
|
405
|
+
return '<div class="xapi-entry verb-' + verb + '" data-expandable>' +
|
|
406
|
+
'<div class="xapi-header">' +
|
|
407
|
+
'<span class="xapi-timestamp">' + escapeHtmlInline(entry.receivedAt) + '</span>' +
|
|
408
|
+
'<span class="xapi-verb">' + escapeHtmlInline(verb) + '</span>' +
|
|
409
|
+
'<span class="xapi-type">' + escapeHtmlInline(entry.type || '') + '</span>' +
|
|
410
|
+
'</div>' +
|
|
411
|
+
'<div class="xapi-object">' + escapeHtmlInline(objectId) + '</div>' +
|
|
412
|
+
'<div class="xapi-details">' + escapeHtmlInline(details) + '</div>' +
|
|
413
|
+
'</div>';
|
|
414
|
+
}).join('');
|
|
415
|
+
|
|
416
|
+
if (badge) {
|
|
417
|
+
badge.innerHTML = '<span class="xapi-count-badge">' + xapiLog.length + '</span>';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|