@examplary/pci-runtime 0.0.1 → 0.0.2

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/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Examplary PCI runtime
2
+
3
+ A PCI (Portable Custom Interaction) runtime for Examplary question type components. This package provides the necessary runtime environment to load Examplary custom question types as QTI 3.0 PCI components.
4
+
5
+ ## Usage
6
+
7
+ Define a PCI QTI item:
8
+
9
+ ```xml
10
+ <qti-assessment-item
11
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
12
+ xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
13
+ xsi:schemalocation="http://www.imsglobal.org/xsd/imsqtiasi_v3p0 https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0_v1p0.xsd"
14
+ identifier="question-1"
15
+ title="Sample Question"
16
+ xml:lang="en-US"
17
+ adaptive="false"
18
+ time-dependent="false"
19
+ tool-name="Examplary"
20
+ tool-version="1.0.0"
21
+ >
22
+ <qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="string" />
23
+ <qti-response-processing template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct" />
24
+
25
+ <qti-item-body>
26
+ <p>Hello, world!</p>
27
+
28
+ <qti-portable-custom-interaction
29
+ response-identifier="RESPONSE"
30
+ module="my-question-type"
31
+ custom-interaction-type-identifier="urn:fdc:examplary.ai:pci:com.example.my-question-type"
32
+ data-question="{... JSON data ...}"
33
+ class="examplary-pci-runtime"
34
+ >
35
+ <qti-interaction-markup></qti-interaction-markup>
36
+ </qti-portable-custom-interaction>
37
+ </qti-item-body>
38
+ </qti-assessment-item>
39
+ ```
40
+
41
+ Make sure to set up a module mapping file (usually called `modules/module_resolution.js` with identifier `pci_module_resolution`) to map the `module` attribute to the actual JavaScript module that implements the question type:
42
+
43
+ ```js
44
+ {
45
+ "waitSeconds": 60,
46
+ "paths": {
47
+ "examplaryPciRuntime": "modules/runtime.js",
48
+ "my-question-type": "modules/my-question-type.js"
49
+ }
50
+ }
51
+ ```
52
+
53
+ Where `modules/runtime.js` is a copy of the [runtime code](https://unpkg.com/@examplary/pci-runtime), and `modules/my-question-type.js` is your custom question type implementation:
54
+
55
+ ```js
56
+ define(['qtiCustomInteractionContext', 'examplaryPciRuntime'], function (qtiCustomInteractionContext, examplaryPciRuntime) {
57
+ return examplaryPciRuntime.register({
58
+ qtiCustomInteractionContext,
59
+ id: 'com.example.my-question-type',
60
+ language: 'en',
61
+ translations: {
62
+ 'placeholder': 'Type your answer here...'
63
+ },
64
+ component: '(assessment component compiled JS here)',
65
+ stylesheet: '(assessment component compiled CSS here, optional)'
66
+ });
67
+ });
68
+ ```
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@examplary/pci-runtime",
3
3
  "type": "module",
4
4
  "packageManager": "yarn@4.8.1",
5
- "version": "0.0.1",
5
+ "version": "0.0.2",
6
6
  "description": "PCI runtime for Examplary question type components.",
7
7
  "scripts": {
8
8
  "build": "node ./scripts/build.js",
@@ -25,6 +25,7 @@
25
25
  "email": "hi@examplary.ai"
26
26
  },
27
27
  "dependencies": {
28
+ "@citolab/tspci": "^2.6.5",
28
29
  "@examplary/ui": "workspace:^",
29
30
  "zustand": "^5.0.9"
30
31
  }
@@ -1,4 +1,4 @@
1
- define(['qtiCustomInteractionContext'], function(qtiCustomInteractionContext) {
1
+ define(function() {
2
2
  "use strict";
3
3
  var __export__ = (() => {
4
4
  var __create = Object.create;
@@ -1662,14 +1662,14 @@ var __export__ = (() => {
1662
1662
  return x5 === y3 && (0 !== x5 || 1 / x5 === 1 / y3) || x5 !== x5 && y3 !== y3;
1663
1663
  }
1664
1664
  "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
1665
- var React72 = require_react(), shim = require_shim(), objectIs = "function" === typeof Object.is ? Object.is : is2, useSyncExternalStore3 = shim.useSyncExternalStore, useRef32 = React72.useRef, useEffect37 = React72.useEffect, useMemo18 = React72.useMemo, useDebugValue3 = React72.useDebugValue;
1665
+ var React72 = require_react(), shim = require_shim(), objectIs = "function" === typeof Object.is ? Object.is : is2, useSyncExternalStore3 = shim.useSyncExternalStore, useRef32 = React72.useRef, useEffect37 = React72.useEffect, useMemo19 = React72.useMemo, useDebugValue3 = React72.useDebugValue;
1666
1666
  exports.useSyncExternalStoreWithSelector = function(subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
1667
1667
  var instRef = useRef32(null);
1668
1668
  if (null === instRef.current) {
1669
1669
  var inst = { hasValue: false, value: null };
1670
1670
  instRef.current = inst;
1671
1671
  } else inst = instRef.current;
1672
- instRef = useMemo18(
1672
+ instRef = useMemo19(
1673
1673
  function() {
1674
1674
  function memoizedSelector(nextSnapshot) {
1675
1675
  if (!hasMemo) {
@@ -19831,7 +19831,7 @@ var __export__ = (() => {
19831
19831
  // src/runtime.tsx
19832
19832
  var runtime_exports = {};
19833
19833
  __export(runtime_exports, {
19834
- default: () => runtime_default
19834
+ register: () => register
19835
19835
  });
19836
19836
  var import_react52 = __toESM(require_react(), 1);
19837
19837
 
@@ -96010,70 +96010,89 @@ import "https://unpkg.com/@cortex-js/compute-engine?module"`), '["Error", "compu
96010
96010
 
96011
96011
  // src/runtime.tsx
96012
96012
  var import_client5 = __toESM(require_client(), 1);
96013
- var AssessmentComponent = null;
96014
- if (typeof EXAMPLARY_QUESTION_TYPE_ID === "undefined" || typeof EXAMPLARY_QUESTION_TYPE_COMPONENT === "undefined") {
96015
- throw new Error(
96016
- "Missing required global variables for Examplary PCI runtime."
96017
- );
96018
- }
96019
- var runtime = {
96020
- typeIdentifier: `urn:fdc:examplary.ai:pci:${EXAMPLARY_QUESTION_TYPE_ID}`,
96021
- _baseElement: null,
96022
- _store: null,
96023
- _state: {},
96024
- _config: {},
96025
- _question: {},
96026
- _onReadyCalled: false,
96027
- /**
96028
- * Create a new instance of this portable custom interaction.
96029
- * Will be called by the qtiCustomInteractionContext.
96030
- */
96031
- getInstance: function(dom, configuration, state) {
96032
- const newInstance = Object.assign({}, this);
96033
- newInstance._init = newInstance._init.bind(newInstance);
96034
- newInstance._init(dom, configuration, state);
96035
- return newInstance;
96036
- },
96037
- /**
96038
- * Initialize this PCI instance with the provided DOM element,
96039
- */
96040
- _init: function(dom, configuration, state) {
96041
- this._baseElement = dom;
96042
- const uid = "tap-" + Math.floor(Math.random() * 1e5);
96043
- dom.setAttribute("data-uid", uid);
96044
- this._config = configuration;
96045
- const json = configuration.properties.examplarySettings || "{}";
96046
- this._question = JSON.parse(json);
96047
- console.log("[PCI-Examplary] init", { state, configuration });
96048
- if (state) {
96049
- this._state = JSON.parse(state);
96050
- }
96051
- this._store = create((set2) => ({
96052
- answer: this._state?.answer || null,
96053
- saveAnswer: (answer) => set2({
96054
- answer
96055
- })
96056
- }));
96057
- this.oncompleted = this.oncompleted.bind(this);
96058
- this._loadComponent();
96059
- return this;
96060
- },
96061
- _loadComponent: async function() {
96062
- if (!AssessmentComponent) {
96063
- AssessmentComponent = await evalComponent(
96064
- EXAMPLARY_QUESTION_TYPE_COMPONENT
96065
- );
96013
+ var register = ({
96014
+ qtiCustomInteractionContext,
96015
+ id: id2,
96016
+ component,
96017
+ language,
96018
+ translations,
96019
+ stylesheet
96020
+ }) => {
96021
+ if (!id2 || !component) {
96022
+ throw new Error(
96023
+ "Missing required parameters 'id' or 'component' for Examplary PCI runtime."
96024
+ );
96025
+ }
96026
+ let AssessmentComponent = null;
96027
+ class ExamplaryPci {
96028
+ typeIdentifier;
96029
+ dom = null;
96030
+ shadowRoot = null;
96031
+ config = null;
96032
+ _question = null;
96033
+ _state = null;
96034
+ _onReadyCalled = false;
96035
+ debounceTimeout = null;
96036
+ store = null;
96037
+ /**
96038
+ * Set correct type identifier and register PCI,
96039
+ * if context is provided.
96040
+ */
96041
+ constructor() {
96042
+ this.typeIdentifier = `urn:fdc:examplary.ai:pci:${id2}`;
96043
+ if (qtiCustomInteractionContext) {
96044
+ qtiCustomInteractionContext.register(this);
96045
+ }
96066
96046
  }
96067
- (() => {
96068
- if ("_context" in AssessmentComponent) {
96069
- const context = AssessmentComponent._context;
96070
- for (const dep in context) {
96071
- globalThis[dep] = context[dep];
96047
+ /**
96048
+ * Create a new instance of this portable custom interaction.
96049
+ * Will be called by the qtiCustomInteractionContext.
96050
+ */
96051
+ getInstance(dom, config, state) {
96052
+ const json = config.properties.examplarySettings || "{}";
96053
+ const question = JSON.parse(json);
96054
+ this.config = config;
96055
+ this._state = state ? JSON.parse(state) : null;
96056
+ this.store = create()((set2) => ({
96057
+ answer: this._state?.answer || null,
96058
+ question,
96059
+ saveAnswer: (answer) => {
96060
+ set2({ answer });
96061
+ this.interactionChanged();
96062
+ }
96063
+ }));
96064
+ if (this.config.boundTo && Object.keys(this.config.boundTo).length > 0) {
96065
+ const responseIdentifier = Object.keys(this.config.boundTo)[0];
96066
+ const response = this.config.boundTo[responseIdentifier];
96067
+ if (response && response.base !== null) {
96068
+ this.setResponse(response);
96072
96069
  }
96073
96070
  }
96074
- const stylesheet = typeof EXAMPLARY_QUESTION_TYPE_STYLESHEET === "undefined" ? "" : EXAMPLARY_QUESTION_TYPE_STYLESHEET;
96075
- const shadowRoot = this._baseElement.attachShadow({ mode: "open" });
96076
- shadowRoot.innerHTML = `
96071
+ if (!dom) {
96072
+ throw new Error("No DOM Element provided");
96073
+ }
96074
+ this.dom = dom;
96075
+ this.shadowRoot = dom.shadowRoot || dom.attachShadow({ mode: "open" });
96076
+ this.render();
96077
+ return this;
96078
+ }
96079
+ /**
96080
+ * Render the PCI component inside the provided DOM element.
96081
+ */
96082
+ async render() {
96083
+ if (!AssessmentComponent) {
96084
+ AssessmentComponent = await evalComponent(
96085
+ component
96086
+ );
96087
+ }
96088
+ (() => {
96089
+ if ("_context" in AssessmentComponent) {
96090
+ const context = AssessmentComponent._context;
96091
+ for (const dep in context) {
96092
+ globalThis[dep] = context[dep];
96093
+ }
96094
+ }
96095
+ this.shadowRoot.innerHTML = `
96077
96096
  <style>
96078
96097
 
96079
96098
  @layer modules_theme, theme, modules_base, base, modules_components, components, modules_utilities, utilities;
@@ -100304,106 +100323,160 @@ body {
100304
100323
  </style>
100305
100324
  <div class="examplary-component-root"></div>
100306
100325
  `;
100307
- const root = shadowRoot.querySelector(".examplary-component-root");
100308
- const AssessmentCoreComponent = () => {
100309
- const answer = this._store((state) => state.answer);
100310
- const saveAnswer = this._store((state) => state.saveAnswer);
100311
- (0, import_react52.useEffect)(() => {
100312
- if (!this._onReadyCalled) {
100313
- this._onReadyCalled = true;
100314
- if (this._config.onready) {
100315
- this._config.onready(this, this.getState());
100316
- }
100317
- }
100318
- }, []);
100319
- return (
100320
- // @ts-expect-error api
100321
- /* @__PURE__ */ React.createElement(
100322
- AssessmentComponent,
100323
- {
100324
- question: this._question,
100325
- isPreview: false,
100326
- answer,
100327
- saveAnswer,
100328
- api: null,
100329
- t: (key) => EXAMPLARY_QUESTION_TYPE_TRANSLATIONS[key] || key,
100330
- i18n: { language: EXAMPLARY_QUESTION_TYPE_LANGUAGE || "en" }
100331
- }
100332
- )
100326
+ const root = this.shadowRoot.querySelector(
100327
+ ".examplary-component-root"
100333
100328
  );
100329
+ const AssessmentCoreComponent = () => {
100330
+ const answer = this.store((state) => state.answer);
100331
+ const saveAnswer = this.store((state) => state.saveAnswer);
100332
+ (0, import_react52.useEffect)(() => {
100333
+ if (this.config.onready) {
100334
+ if (!this._onReadyCalled) {
100335
+ this._onReadyCalled = true;
100336
+ this.config.onready(this, this.getState());
100337
+ }
100338
+ }
100339
+ }, []);
100340
+ const translate = (0, import_react52.useCallback)((key) => {
100341
+ return translations?.[key] || key;
100342
+ }, []);
100343
+ const i18n = (0, import_react52.useMemo)(() => ({ language: language || "en" }), []);
100344
+ return (
100345
+ // @ts-expect-error api
100346
+ /* @__PURE__ */ React.createElement(
100347
+ AssessmentComponent,
100348
+ {
100349
+ question: this._question,
100350
+ isPreview: false,
100351
+ answer,
100352
+ saveAnswer,
100353
+ api: null,
100354
+ t: translate,
100355
+ i18n
100356
+ }
100357
+ )
100358
+ );
100359
+ };
100360
+ (0, import_client5.createRoot)(root).render(/* @__PURE__ */ React.createElement(AssessmentCoreComponent, null));
100361
+ })();
100362
+ }
100363
+ /**
100364
+ * Get the output response for this PCI instance.
100365
+ *
100366
+ * This will be requested by the Delivery System when
100367
+ * it needs to collect the responses.
100368
+ */
100369
+ getResponse() {
100370
+ if (!this.store?.getState().answer) {
100371
+ return void 0;
100372
+ }
100373
+ let answerValue = this.store?.getState().answer?.value || "";
100374
+ if (Array.isArray(answerValue)) {
100375
+ answerValue = answerValue.join(",");
100376
+ }
100377
+ return {
100378
+ base: {
100379
+ string: answerValue
100380
+ }
100334
100381
  };
100335
- (0, import_client5.createRoot)(root).render(/* @__PURE__ */ React.createElement(AssessmentCoreComponent, null));
100336
- })();
100337
- },
100338
- /**
100339
- * Get the output response for this PCI instance.
100340
- */
100341
- getResponse: function() {
100342
- console.log(`[PCI-Examplary] getResponse`);
100343
- if (!this._store?.getState().answer) {
100344
- return void 0;
100345
100382
  }
100346
- let answerValue = this._store?.getState().answer?.value || "";
100347
- if (Array.isArray(answerValue)) {
100348
- answerValue = answerValue.join(",");
100383
+ /**
100384
+ * Returns whether the current response is valid.
100385
+ */
100386
+ checkValidity() {
100387
+ return !!this.store?.getState().answer;
100349
100388
  }
100350
- return {
100351
- base: {
100352
- string: answerValue
100389
+ /**
100390
+ * The current state of this PCI. May be passed to getInstance
100391
+ * to later restore this PCI instance.
100392
+ */
100393
+ getState() {
100394
+ return JSON.stringify({
100395
+ answer: this.store?.getState().answer || null
100396
+ });
100397
+ }
100398
+ /**
100399
+ * Restore a previous state of the PCI instance.
100400
+ */
100401
+ setState(value) {
100402
+ if (value) {
100403
+ this._state = JSON.parse(value);
100404
+ this.store?.setState({
100405
+ answer: this._state.answer || null
100406
+ });
100353
100407
  }
100408
+ }
100409
+ /**
100410
+ * Restore the response value for this PCI instance.
100411
+ */
100412
+ setResponse(value) {
100413
+ const answerValue = value?.base?.string || value?.base?.integer || value?.base?.float || value?.base?.boolean || null;
100414
+ this.store?.setState((state) => ({
100415
+ answer: {
100416
+ ...state.answer,
100417
+ value: answerValue
100418
+ }
100419
+ }));
100420
+ }
100421
+ /**
100422
+ * Notify the Delivery System that the response has changed.
100423
+ */
100424
+ interactionChanged = () => {
100425
+ if (this.debounceTimeout) {
100426
+ clearTimeout(this.debounceTimeout);
100427
+ }
100428
+ this.debounceTimeout = setTimeout(() => {
100429
+ const event = {
100430
+ interaction: this,
100431
+ responseIdentifier: this.config.responseIdentifier,
100432
+ valid: this.checkValidity(),
100433
+ value: this.getResponse()
100434
+ };
100435
+ const interactionChangedEvent = new CustomEvent(
100436
+ "qti-interaction-changed",
100437
+ { detail: event }
100438
+ );
100439
+ this.dom?.dispatchEvent(interactionChangedEvent);
100440
+ }, 1e3);
100354
100441
  };
100355
- },
100356
- checkValidity: function() {
100357
- console.log(`[PCI-Examplary] checkValidity`);
100358
- if (!this._store?.getState().answer) {
100359
- return false;
100442
+ /**
100443
+ * This will be provided as the oncompleted callback to cleanup
100444
+ * before this PCI is unloaded.
100445
+ *
100446
+ * We don't have any special cleanup to do here.
100447
+ */
100448
+ oncompleted() {
100360
100449
  }
100361
- return true;
100362
- },
100363
- /**
100364
- * The current state of this PCI. May be passed to getInstance
100365
- * to later restore this PCI instance.
100366
- */
100367
- getState: function() {
100368
- console.log(`[PCI-Examplary] getState`);
100369
- return JSON.stringify({
100370
- answer: this._store?.getState().answer || null
100371
- });
100372
- },
100373
- /**
100374
- * Restore a previous state of the PCI instance.
100375
- */
100376
- setState: function(value) {
100377
- console.log(`[PCI-Examplary] setState`);
100378
- if (value) {
100379
- this._state = JSON.parse(value);
100380
- this._store?.setState({
100381
- answer: this._state.answer || null
100382
- });
100450
+ // ------------------------------------------------
100451
+ // TAO compatibility methods
100452
+ // ------------------------------------------------
100453
+ /**
100454
+ * Called when setting correct response in tao
100455
+ */
100456
+ off() {
100383
100457
  }
100384
- },
100385
- setResponse: function(value) {
100386
- console.log(`[PCI-Examplary] setResponse`, value);
100387
- const answerValue = value?.base?.string || value?.base?.integer || value?.base?.float || value?.base?.boolean || null;
100388
- this._store?.setState((state) => ({
100389
- answer: {
100390
- ...state.answer,
100391
- value: answerValue
100392
- }
100393
- }));
100394
- },
100395
- /**
100396
- * This will be provided as the oncompleted callback to cleanup
100397
- * before this PCI is unloaded.
100398
- */
100399
- oncompleted: function() {
100400
- console.log(`[PCI-Examplary] oncompleted`);
100458
+ /**
100459
+ * Called by TAO when binding events?
100460
+ */
100461
+ on(_5) {
100462
+ }
100463
+ /**
100464
+ * Called by TAO for a changed property.
100465
+ */
100466
+ trigger = (event, value) => {
100467
+ this.config.properties[event] = value;
100468
+ this.render();
100469
+ };
100470
+ /**
100471
+ * Reset the response in TAO
100472
+ */
100473
+ resetResponse = () => {
100474
+ this.render();
100475
+ this.store?.setState({ answer: null });
100476
+ };
100401
100477
  }
100478
+ return new ExamplaryPci();
100402
100479
  };
100403
- if (qtiCustomInteractionContext) {
100404
- qtiCustomInteractionContext.register(runtime);
100405
- }
100406
- var runtime_default = runtime;
100407
100480
  return __toCommonJS(runtime_exports);
100408
100481
  })();
100409
100482
  /*! Bundled license information: