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