@pie-players/pie-assessment-toolkit 0.3.67 → 0.3.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +81 -9
  2. package/dist/attempt/AssessmentSession.d.ts +7 -27
  3. package/dist/components/ItemToolBar.custom-element.js +1 -1
  4. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  5. package/dist/components/SectionToolBar.custom-element.js +1 -1
  6. package/dist/components/chunks/ItemToolBar-pe5szfyx.js +46 -0
  7. package/dist/components/chunks/ItemToolBar-rd7te9r0.js +51 -0
  8. package/dist/components/item-toolbar-element.js +1 -0
  9. package/dist/components/pie-assessment-toolkit-element.js +1 -0
  10. package/dist/components/section-toolbar-element.js +1 -0
  11. package/dist/context/assessment-toolkit-context.d.ts +28 -0
  12. package/dist/index.d.ts +7 -3
  13. package/dist/index.js +4 -2
  14. package/dist/policy/core/compose-decision.js +14 -0
  15. package/dist/policy/core/decision-types.d.ts +1 -1
  16. package/dist/policy/sources/PnpPolicySource.d.ts +8 -0
  17. package/dist/policy/sources/PnpPolicySource.js +19 -10
  18. package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
  19. package/dist/runtime/SectionRuntimeEngine.js +66 -1
  20. package/dist/runtime/core/engine-resolver.d.ts +25 -1
  21. package/dist/runtime/registration-events.d.ts +52 -0
  22. package/dist/runtime/registration-events.js +2 -0
  23. package/dist/services/AccessibilityCatalogResolver.js +21 -5
  24. package/dist/services/I18nService.d.ts +28 -100
  25. package/dist/services/I18nService.js +43 -233
  26. package/dist/services/TTSService.d.ts +12 -0
  27. package/dist/services/TTSService.js +18 -17
  28. package/dist/services/ToolRegistry.d.ts +120 -2
  29. package/dist/services/ToolRegistry.js +64 -0
  30. package/dist/services/ToolkitCoordinator.d.ts +39 -37
  31. package/dist/services/ToolkitCoordinator.js +40 -0
  32. package/dist/services/audio-handoff.d.ts +39 -0
  33. package/dist/services/audio-handoff.js +58 -0
  34. package/dist/services/catalog-media.d.ts +35 -4
  35. package/dist/services/catalog-media.js +92 -3
  36. package/dist/services/framework-error.d.ts +15 -1
  37. package/dist/services/interfaces.d.ts +28 -0
  38. package/dist/services/pnp-standard-features.d.ts +1 -1
  39. package/dist/services/section-controller-types.d.ts +218 -7
  40. package/dist/services/selection-action.d.ts +49 -0
  41. package/dist/services/selection-action.js +10 -0
  42. package/dist/services/spoken-audio-cards.js +5 -1
  43. package/dist/services/tool-context.d.ts +6 -5
  44. package/dist/services/tool-context.js +205 -155
  45. package/dist/services/tool-icons.d.ts +18 -0
  46. package/dist/services/tool-icons.js +31 -0
  47. package/dist/services/tool-providers/CortexToolProvider.d.ts +18 -0
  48. package/dist/services/tool-providers/CortexToolProvider.js +32 -0
  49. package/dist/services/tool-providers/DesmosToolProvider.d.ts +13 -101
  50. package/dist/services/tool-providers/DesmosToolProvider.js +14 -145
  51. package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +21 -0
  52. package/dist/services/tool-providers/GeoGebraToolProvider.js +32 -0
  53. package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +35 -0
  54. package/dist/services/tool-providers/LazyCalculatorToolProvider.js +95 -0
  55. package/dist/services/tool-providers/index.d.ts +4 -0
  56. package/dist/services/tool-providers/index.js +2 -0
  57. package/dist/services/tool-request.d.ts +106 -0
  58. package/dist/services/tool-request.js +127 -0
  59. package/dist/services/toolbar-items.d.ts +6 -0
  60. package/dist/tools/client.d.ts +0 -2
  61. package/dist/tools/client.js +0 -4
  62. package/dist/tools/internal.d.ts +7 -1
  63. package/dist/tools/internal.js +8 -1
  64. package/dist/tools/tool-surface-host.d.ts +57 -0
  65. package/dist/tools/tool-surface-host.js +610 -0
  66. package/dist/tools/types.d.ts +1 -66
  67. package/package.json +22 -12
  68. package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
  69. package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
  70. package/dist/tools/calculators/desmos-provider.d.ts +0 -46
  71. package/dist/tools/calculators/desmos-provider.js +0 -393
  72. package/dist/tools/library-loader.d.ts +0 -62
  73. package/dist/tools/library-loader.js +0 -261
@@ -40,185 +40,226 @@ export function isRubricContext(context) {
40
40
  export function isElementContext(context) {
41
41
  return context.level === "element";
42
42
  }
43
+ const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").trim();
43
44
  /**
44
- * Helper to extract text content from an item or element for analysis
45
+ * A config's `models` as a list, whether it was authored as an array or as a
46
+ * record keyed by element id. Both forms are in the wild.
45
47
  */
46
- export function extractTextContent(context) {
48
+ function normalizeModels(modelsRaw) {
49
+ if (Array.isArray(modelsRaw))
50
+ return modelsRaw;
51
+ if (modelsRaw && typeof modelsRaw === "object") {
52
+ return Object.values(modelsRaw);
53
+ }
54
+ return [];
55
+ }
56
+ /**
57
+ * Push every string a model carries, one level into its arrays of objects.
58
+ *
59
+ * The depth is deliberate rather than a full walk: math and prose live in a
60
+ * model's own fields (`prompt`, `label`) and in its choice/row arrays, which is
61
+ * one level down. Recursing further would pull in ids, keys and config flags.
62
+ */
63
+ function collectModelText(model, push) {
64
+ if (!model || typeof model !== "object")
65
+ return;
66
+ for (const value of Object.values(model)) {
67
+ if (typeof value === "string")
68
+ push(value);
69
+ if (Array.isArray(value)) {
70
+ for (const entry of value) {
71
+ if (entry && typeof entry === "object") {
72
+ for (const nested of Object.values(entry)) {
73
+ if (typeof nested === "string")
74
+ push(nested);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ /** Push the markup of every element snippet in a config's `elements` map. */
82
+ function collectElementsText(elements, push) {
83
+ if (!elements || typeof elements !== "object")
84
+ return;
85
+ for (const elementMarkup of Object.values(elements)) {
86
+ if (typeof elementMarkup === "string")
87
+ push(elementMarkup);
88
+ }
89
+ }
90
+ /**
91
+ * The authored content a context carries, for the content heuristics below.
92
+ *
93
+ * Each level differs only in which fields it reads: an element reads its own
94
+ * markup snippet and the one model bearing its id, an item and a passage read
95
+ * their whole config and every model. The traversal itself is shared, so a new
96
+ * place content can hide is added once.
97
+ *
98
+ * `transform` decides what the caller gets. {@link extractTextContent} strips
99
+ * tags, which is right for prose keyword matching and wrong for structural
100
+ * matching: a MathML item's only math signal *is* the `<math>` tag, and stripping
101
+ * first left `hasMathContent`'s MathML pattern unreachable.
102
+ */
103
+ function extractContent(context, transform) {
104
+ const textChunks = [];
105
+ const push = (text) => {
106
+ textChunks.push(transform(text));
107
+ };
108
+ const joined = () => textChunks.filter(Boolean).join(" ").trim();
47
109
  if (isElementContext(context)) {
48
110
  const config = context.item.config;
49
111
  if (!config)
50
112
  return "";
51
- const textChunks = [];
52
- const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").trim();
53
- // Try to find element markup by element id.
54
113
  const elementMarkup = config.elements?.[context.elementId];
55
- if (elementMarkup) {
56
- if (typeof elementMarkup === "string") {
57
- textChunks.push(stripHtml(elementMarkup));
58
- }
59
- }
60
- // Also inspect model data keyed by this element id.
61
- // In many items, math appears in model.prompt/labels rather than elements[elementId].
62
- const modelsRaw = config.models;
63
- const models = Array.isArray(modelsRaw)
64
- ? modelsRaw
65
- : modelsRaw && typeof modelsRaw === "object"
66
- ? Object.values(modelsRaw)
67
- : [];
68
- const model = models.find((m) => m && typeof m === "object" && m.id === context.elementId);
69
- if (model) {
70
- for (const value of Object.values(model)) {
71
- if (typeof value === "string") {
72
- textChunks.push(stripHtml(value));
73
- }
74
- if (Array.isArray(value)) {
75
- for (const entry of value) {
76
- if (entry && typeof entry === "object") {
77
- for (const nested of Object.values(entry)) {
78
- if (typeof nested === "string") {
79
- textChunks.push(stripHtml(nested));
80
- }
81
- }
82
- }
83
- }
84
- }
85
- }
86
- }
87
- return textChunks.filter(Boolean).join(" ").trim();
114
+ if (typeof elementMarkup === "string")
115
+ push(elementMarkup);
116
+ // Model data keyed by this element id: in many items the math is in
117
+ // `model.prompt`/labels rather than in `elements[elementId]`.
118
+ const model = normalizeModels(config.models).find((candidate) => !!candidate &&
119
+ typeof candidate === "object" &&
120
+ candidate.id === context.elementId);
121
+ collectModelText(model, push);
122
+ return joined();
88
123
  }
89
124
  if (isItemContext(context)) {
90
- const item = context.item;
91
- if (!item?.config)
125
+ const config = context.item?.config;
126
+ if (!config)
92
127
  return "";
93
- const config = item.config;
94
- const textChunks = [];
95
- const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").trim();
96
- // Primary item markup
97
- if (typeof config.markup === "string") {
98
- textChunks.push(stripHtml(config.markup));
128
+ if (typeof config.markup === "string")
129
+ push(config.markup);
130
+ collectElementsText(config.elements, push);
131
+ for (const model of normalizeModels(config.models)) {
132
+ collectModelText(model, push);
99
133
  }
100
- // Element markup snippets
101
- const elements = config.elements;
102
- if (elements && typeof elements === "object") {
103
- for (const elementMarkup of Object.values(elements)) {
104
- if (typeof elementMarkup === "string") {
105
- textChunks.push(stripHtml(elementMarkup));
106
- }
107
- }
108
- }
109
- // Model-level text (prompts, labels, etc.)
110
- const modelsRaw = config.models;
111
- const models = Array.isArray(modelsRaw)
112
- ? modelsRaw
113
- : modelsRaw && typeof modelsRaw === "object"
114
- ? Object.values(modelsRaw)
115
- : [];
116
- for (const model of models) {
117
- if (!model || typeof model !== "object")
118
- continue;
119
- for (const value of Object.values(model)) {
120
- if (typeof value === "string") {
121
- textChunks.push(stripHtml(value));
122
- }
123
- if (Array.isArray(value)) {
124
- for (const entry of value) {
125
- if (entry && typeof entry === "object") {
126
- for (const nested of Object.values(entry)) {
127
- if (typeof nested === "string") {
128
- textChunks.push(stripHtml(nested));
129
- }
130
- }
131
- }
132
- }
133
- }
134
- }
135
- }
136
- return textChunks.filter(Boolean).join(" ").trim();
134
+ return joined();
137
135
  }
138
136
  if (isPassageContext(context)) {
139
- const passage = context.passage;
140
- if (!passage?.config)
137
+ const config = context.passage?.config;
138
+ if (!config)
141
139
  return "";
142
- const config = passage.config;
143
- const textChunks = [];
144
- const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").trim();
145
- // Primary passage markup/content
146
- if (typeof config.markup === "string") {
147
- textChunks.push(stripHtml(config.markup));
148
- }
149
- if (typeof config.content === "string") {
150
- textChunks.push(stripHtml(config.content));
151
- }
152
- if (typeof config.prompt === "string") {
153
- textChunks.push(stripHtml(config.prompt));
154
- }
155
- // Element markup snippets
156
- const elements = config.elements;
157
- if (elements && typeof elements === "object") {
158
- for (const elementMarkup of Object.values(elements)) {
159
- if (typeof elementMarkup === "string") {
160
- textChunks.push(stripHtml(elementMarkup));
161
- }
162
- }
140
+ for (const field of ["markup", "content", "prompt"]) {
141
+ const value = config[field];
142
+ if (typeof value === "string")
143
+ push(value);
163
144
  }
164
- // Model-level text (prompts, labels, etc.)
165
- const modelsRaw = config.models;
166
- const models = Array.isArray(modelsRaw)
167
- ? modelsRaw
168
- : modelsRaw && typeof modelsRaw === "object"
169
- ? Object.values(modelsRaw)
170
- : [];
171
- for (const model of models) {
172
- if (!model || typeof model !== "object")
173
- continue;
174
- for (const value of Object.values(model)) {
175
- if (typeof value === "string") {
176
- textChunks.push(stripHtml(value));
177
- }
178
- if (Array.isArray(value)) {
179
- for (const entry of value) {
180
- if (entry && typeof entry === "object") {
181
- for (const nested of Object.values(entry)) {
182
- if (typeof nested === "string") {
183
- textChunks.push(stripHtml(nested));
184
- }
185
- }
186
- }
187
- }
188
- }
189
- }
145
+ collectElementsText(config.elements, push);
146
+ for (const model of normalizeModels(config.models)) {
147
+ collectModelText(model, push);
190
148
  }
191
- return textChunks.filter(Boolean).join(" ").trim();
149
+ return joined();
192
150
  }
193
151
  if (isRubricContext(context)) {
152
+ // No model walk: a rubric block is authored prose, so its text is one
153
+ // string — the embedded passage's markup when it has one, else its content.
194
154
  const rubric = context.rubricBlock;
195
- // If rubric has embedded passage, extract from passage config
196
155
  if (rubric.passage?.config) {
197
- const markup = rubric.passage.config.markup || "";
198
- return markup.replace(/<[^>]*>/g, " ").trim();
156
+ return transform(rubric.passage.config.markup || "");
199
157
  }
200
- // Otherwise, use simple content string
201
- const content = rubric.content || "";
202
- return content.replace(/<[^>]*>/g, " ").trim();
158
+ return transform(rubric.content || "");
203
159
  }
204
160
  return "";
205
161
  }
162
+ /** The plain text a context carries, tags removed. */
163
+ export function extractTextContent(context) {
164
+ return extractContent(context, stripHtml);
165
+ }
166
+ /**
167
+ * The authored markup a context carries, tags intact.
168
+ *
169
+ * For indicators that live in the markup rather than in the prose — `<math>`
170
+ * above all, whose whole signal is the element name.
171
+ */
172
+ export function extractMarkupContent(context) {
173
+ return extractContent(context, (value) => value);
174
+ }
206
175
  /**
207
176
  * Helper to check if context contains mathematical content
208
177
  * (Basic heuristic - can be overridden by tools)
209
178
  */
179
+ /**
180
+ * Chemical element symbols.
181
+ *
182
+ * A real set rather than `[A-Z][a-z]?`: that shape matches "It", "In", "He" and
183
+ * "A", which is why the science gate used to answer `true` for any prose that
184
+ * began a sentence.
185
+ */
186
+ const ELEMENT_SYMBOLS = new Set([
187
+ "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si",
188
+ "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co",
189
+ "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr",
190
+ "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I",
191
+ "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy",
192
+ "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au",
193
+ "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U",
194
+ "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr",
195
+ ]);
196
+ /** A word that could be a formula: capitalised groups with optional counts. */
197
+ const FORMULA_CANDIDATE = /\b[A-Z][A-Za-z]*\d*(?:[A-Z][A-Za-z]*\d*)*\b/g;
198
+ const FORMULA_GROUP = /([A-Z][a-z]?)(\d*)/g;
199
+ /**
200
+ * Whether the text contains something only a chemical formula looks like.
201
+ *
202
+ * A token qualifies when every one of its groups is a real element symbol *and*
203
+ * it either names two or more of them or carries a count — `NaCl`, `CO2`, `H2O`,
204
+ * `C6H12O6`. A lone symbol never qualifies: "In", "He", "As" and "At" are
205
+ * ordinary English words, and a single-letter "I" or "A" more so.
206
+ */
207
+ function hasChemicalFormula(text) {
208
+ for (const candidate of text.match(FORMULA_CANDIDATE) ?? []) {
209
+ FORMULA_GROUP.lastIndex = 0;
210
+ let groups = 0;
211
+ let hasCount = false;
212
+ let consumed = 0;
213
+ let valid = true;
214
+ let match = FORMULA_GROUP.exec(candidate);
215
+ while (match !== null) {
216
+ if (match[0] === "")
217
+ break;
218
+ if (!ELEMENT_SYMBOLS.has(match[1])) {
219
+ valid = false;
220
+ break;
221
+ }
222
+ groups += 1;
223
+ if (match[2])
224
+ hasCount = true;
225
+ consumed += match[0].length;
226
+ match = FORMULA_GROUP.exec(candidate);
227
+ }
228
+ // Every character has to belong to a group, or the token was only
229
+ // formula-shaped at its start ("Hello" -> "He" + "llo").
230
+ if (valid && consumed === candidate.length && (groups > 1 || hasCount)) {
231
+ return true;
232
+ }
233
+ }
234
+ return false;
235
+ }
210
236
  export function hasMathContent(context) {
211
- const text = extractTextContent(context);
212
- // Look for common math indicators
213
- const mathIndicators = [
237
+ // Structural signals live in the markup: stripping tags first is what left the
238
+ // MathML pattern unable to match anything at all.
239
+ const markup = extractMarkupContent(context);
240
+ const structuralIndicators = [
214
241
  /<math[>\s]/i, // MathML
215
242
  /\\\[([^\]]+)\\\]/, // LaTeX display math
216
243
  /\$\$[^$]+\$\$/, // LaTeX display math ($$...$$)
217
244
  /\\\(/, // LaTeX inline math
218
- /[+\-*/=<>≤≥∑∫√π]/, // Math symbols
219
- /\d+\s*[+\-*/=]\s*\d+/, // Simple arithmetic
220
245
  ];
221
- return mathIndicators.some((pattern) => pattern.test(text));
246
+ if (structuralIndicators.some((pattern) => pattern.test(markup)))
247
+ return true;
248
+ const text = extractTextContent(context);
249
+ // No bare-operator pattern. `/[+\-*/=<>≤≥∑∫√π]/` matched any hyphen or slash,
250
+ // so "well-known" and "and/or" made every item mathematical and this predicate
251
+ // answered `true` for essentially all content — a gate that does not gate. An
252
+ // operator counts only with operands around it, or when the character has no
253
+ // prose reading at all.
254
+ const textIndicators = [
255
+ /[≤≥≠±×÷∑∫√∞π]/, // Symbols with no prose reading
256
+ /\d+\s*[+\-*/×÷=]\s*\d+/, // Simple arithmetic
257
+ /\d\s*[<>]\s*\d/, // Numeric comparison
258
+ /\b\d+\s*\/\s*\d+\b/, // Fractions
259
+ /\b\d+(?:\.\d+)?\s*%/, // Percentages
260
+ /\^\s*\d/, // Exponents
261
+ ];
262
+ return textIndicators.some((pattern) => pattern.test(text));
222
263
  }
223
264
  /**
224
265
  * Helper to check if context contains choice-based interactions
@@ -259,9 +300,14 @@ export function hasChoiceInteraction(context) {
259
300
  if (!m || typeof m !== "object")
260
301
  return false;
261
302
  const type = m.element || "";
262
- if (interactionTypes.includes(type))
263
- return true;
264
- // Fallback for configs that don't provide canonical element names.
303
+ // A model that names its element has answered the question, whichever way
304
+ // the answer falls. `choices` is carried by interactions that are not
305
+ // choice interactions at all `placement-ordering`, `categorize` and
306
+ // `drag-in-the-blank` each hold their draggables there — so reading it on
307
+ // a named model showed the answer eliminator on items where the tool does
308
+ // nothing. The heuristic remains for configs that name no element.
309
+ if (type)
310
+ return interactionTypes.includes(type);
265
311
  return Array.isArray(m.choices) && m.choices.length > 0;
266
312
  });
267
313
  }
@@ -281,14 +327,18 @@ export function hasReadableText(context) {
281
327
  */
282
328
  export function hasScienceContent(context) {
283
329
  const text = extractTextContent(context);
284
- // Look for common science indicators
330
+ // The element-symbol pattern used to be `/\b[A-Z][a-z]?\d*\b/`, which matches
331
+ // any one- or two-letter capitalised word: "It", "In", "A", "No". Every item
332
+ // beginning a sentence with one read as science.
333
+ if (hasChemicalFormula(text))
334
+ return true;
335
+ if (/[A-Z][a-z]?[\u2080-\u2089]/.test(text))
336
+ return true; // Subscripted: H₂O, CO₂
285
337
  const scienceIndicators = [
286
- /chemistry|chemical|element|atom|molecule|compound/i,
287
- /periodic\s+table/i,
288
- /H₂O|CO₂|NaCl|O₂|N₂/i, // Chemical formulas
289
- /\b[A-Z][a-z]?\d*\b/, // Element symbols (H, He, Li, etc.)
290
- /biology|organism|cell|DNA|RNA|protein/i,
291
- /physics|force|energy|velocity|acceleration/i,
338
+ /chemistry|chemical|molecule|compound|periodic\s+table/i,
339
+ /\bchemical\s+element\b|\belement\s+symbol\b/i,
340
+ /biology|organism|\bDNA\b|\bRNA\b|protein|photosynthesis|ecosystem/i,
341
+ /physics|\bforce\b|\benergy\b|velocity|acceleration|momentum/i,
292
342
  ];
293
343
  return scienceIndicators.some((pattern) => pattern.test(text));
294
344
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Built-in icon markup for toolbar buttons, keyed by the names registrations use.
3
+ *
4
+ * A registration names an icon rather than shipping markup, so a host swapping the
5
+ * button chrome gets one consistent set. A name with no entry here renders no icon
6
+ * at all, and an icon-only button with no icon is a blank square — so a registration
7
+ * naming an icon and this map are one change, not two.
8
+ *
9
+ * Names are generic shapes ("book-open", "beaker"), never capability names. Core
10
+ * naming a capability is what the composition layer exists to prevent.
11
+ *
12
+ * Exported through `tools/internal` as well: a registration composing a selection
13
+ * gateway renders its own buttons and has to draw the same icon the toolbar button
14
+ * draws, or the learner sees two unrelated affordances for one tool.
15
+ */
16
+ export declare const TOOL_FALLBACK_ICONS: Readonly<Record<string, string>>;
17
+ /** Icon markup for `name`, or `null` when nothing built in matches. */
18
+ export declare function resolveFallbackToolIcon(name: string): string | null;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Built-in icon markup for toolbar buttons, keyed by the names registrations use.
3
+ *
4
+ * A registration names an icon rather than shipping markup, so a host swapping the
5
+ * button chrome gets one consistent set. A name with no entry here renders no icon
6
+ * at all, and an icon-only button with no icon is a blank square — so a registration
7
+ * naming an icon and this map are one change, not two.
8
+ *
9
+ * Names are generic shapes ("book-open", "beaker"), never capability names. Core
10
+ * naming a capability is what the composition layer exists to prevent.
11
+ *
12
+ * Exported through `tools/internal` as well: a registration composing a selection
13
+ * gateway renders its own buttons and has to draw the same icon the toolbar button
14
+ * draws, or the learner sees two unrelated affordances for one tool.
15
+ */
16
+ export const TOOL_FALLBACK_ICONS = {
17
+ calculator: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 2h10a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm0 2v4h10V4H7Zm0 6v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Z" fill="currentColor"/></svg>',
18
+ "volume-up": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 3.23v2.06A7.002 7.002 0 0 1 19 12a7 7 0 0 1-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77Zm-2 17.75V3L7 8H3v8h4l5 5Zm4.5-9a4.5 4.5 0 0 0-2.5-4.03v8.05A4.5 4.5 0 0 0 16.5 12Z" fill="currentColor"/></svg>',
19
+ swatch: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9c0-.55-.45-1-1-1h-2.5a1.5 1.5 0 0 1 0-3H20a1 1 0 0 0 1-1 8.99 8.99 0 0 0-9-4Zm-5.5 9A1.5 1.5 0 1 1 8 13.5 1.5 1.5 0 0 1 6.5 12Zm3-4A1.5 1.5 0 1 1 11 9.5 1.5 1.5 0 0 1 9.5 8Zm5 0A1.5 1.5 0 1 1 16 9.5 1.5 1.5 0 0 1 14.5 8Z" fill="currentColor"/></svg>',
20
+ "chart-bar": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4.75 5a.76.76 0 0 1 .75.75v11c0 .438.313.75.75.75h13a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-13C5 19 4 18 4 16.75v-11A.74.74 0 0 1 4.75 5ZM8 8.25a.74.74 0 0 1 .75-.75h6.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-6.5A.722.722 0 0 1 8 8.25Zm.75 2.25h4.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-4.5a.723.723 0 0 1-.75-.75.74.74 0 0 1 .75-.75Zm0 3h8.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-8.5a.723.723 0 0 1-.75-.75.74.74 0 0 1 .75-.75Z" fill="currentColor"/></svg>',
21
+ beaker: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21c-.85 0-1.454-.38-1.813-1.137-.358-.759-.27-1.463.263-2.113L9 11V5H8a.968.968 0 0 1-.713-.287A.968.968 0 0 1 7 4c0-.283.096-.52.287-.712A.968.968 0 0 1 8 3h8c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.713A.968.968 0 0 1 16 5h-1v6l5.55 6.75c.533.65.62 1.354.262 2.113C20.454 20.62 19.85 21 19 21H5Zm2-3h10l-3.4-4h-3.2L7 18Zm-2 1h14l-6-7.3V5h-2v6.7L5 19Z" fill="currentColor"/></svg>',
22
+ protractor: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m6.75 21-.25-2.2 2.85-7.85a3.95 3.95 0 0 0 1.75.95l-2.75 7.55L6.75 21Zm10.5 0-1.6-1.55-2.75-7.55a3.948 3.948 0 0 0 1.75-.95l2.85 7.85-.25 2.2ZM12 11a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 9 8c0-.65.188-1.23.563-1.737A2.935 2.935 0 0 1 11 5.2V3h2v2.2c.583.2 1.063.554 1.438 1.063C14.812 6.77 15 7.35 15 8c0 .833-.292 1.542-.875 2.125A2.893 2.893 0 0 1 12 11Zm0-2c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Z" fill="currentColor"/></svg>',
23
+ "bars-3": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6.85 15c.517 0 .98-.15 1.388-.45.408-.3.695-.692.862-1.175l.375-1.15c.267-.8.2-1.537-.2-2.213C8.875 9.337 8.3 9 7.55 9H4.025l.475 3.925c.083.583.346 1.075.787 1.475.442.4.963.6 1.563.6Zm10.3 0c.6 0 1.12-.2 1.563-.6.441-.4.704-.892.787-1.475L19.975 9h-3.5c-.75 0-1.325.342-1.725 1.025-.4.683-.467 1.425-.2 2.225l.35 1.125c.167.483.454.875.862 1.175.409.3.871.45 1.388.45Zm-10.3 2c-1.1 0-2.063-.363-2.887-1.088a4.198 4.198 0 0 1-1.438-2.737L2 9H1V7h6.55c.733 0 1.404.18 2.013.537A3.906 3.906 0 0 1 11 9h2.025c.35-.617.83-1.104 1.438-1.463A3.892 3.892 0 0 1 16.474 7H23v2h-1l-.525 4.175a4.198 4.198 0 0 1-1.438 2.737A4.238 4.238 0 0 1 17.15 17c-.95 0-1.804-.27-2.562-.813A4.234 4.234 0 0 1 13 14.026l-.375-1.125a21.35 21.35 0 0 1-.1-.363 4.926 4.926 0 0 1-.1-.537h-.85c-.033.2-.067.363-.1.488a21.35 21.35 0 0 1-.1.362L11 14a4.3 4.3 0 0 1-1.588 2.175A4.258 4.258 0 0 1 6.85 17Z" fill="currentColor"/></svg>',
24
+ ruler: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m8.8 10.95 2.15-2.175-1.4-1.425-1.1 1.1-1.4-1.4 1.075-1.1L7 4.825 4.825 7 8.8 10.95Zm8.2 8.225L19.175 17l-1.125-1.125-1.1 1.075-1.4-1.4 1.075-1.1-1.425-1.4-2.15 2.15L17 19.175ZM7.25 21H3v-4.25l4.375-4.375L2 7l5-5 5.4 5.4 3.775-3.8c.2-.2.425-.35.675-.45a2.068 2.068 0 0 1 1.55 0c.25.1.475.25.675.45L20.4 4.95c.2.2.35.425.45.675.1.25.15.508.15.775a1.975 1.975 0 0 1-.6 1.425l-3.775 3.8L22 17l-5 5-5.375-5.375L7.25 21ZM5 19h1.4l9.8-9.775L14.775 7.8 5 17.6V19Z" fill="currentColor"/></svg>',
25
+ "book-open": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 6.25a6.6 6.6 0 0 0-2.3-1.32A8.6 8.6 0 0 0 7 4.5c-.63 0-1.25.06-1.85.18-.6.12-1.19.3-1.76.54a1.5 1.5 0 0 0-.93 1.4v10.1c0 .52.26.98.68 1.24.42.26.92.28 1.35.06.36-.18.75-.31 1.18-.4.42-.09.86-.13 1.33-.13.78 0 1.53.14 2.24.42.7.28 1.35.7 1.93 1.24l.83-.75.83.75c.58-.54 1.22-.96 1.93-1.24a6.1 6.1 0 0 1 2.24-.42c.47 0 .91.04 1.33.13.43.09.82.22 1.18.4.43.22.93.2 1.35-.06.42-.26.68-.72.68-1.24V6.62c0-.6-.36-1.15-.93-1.4a9.3 9.3 0 0 0-1.76-.54A9.8 9.8 0 0 0 17 4.5a8.6 8.6 0 0 0-2.7.43A6.6 6.6 0 0 0 12 6.25Zm1 10.06V8.02c.55-.5 1.16-.88 1.84-1.13A6 6 0 0 1 17 6.5c.45 0 .89.04 1.32.11.43.08.85.19 1.26.34v9.24a8.2 8.2 0 0 0-1.28-.24A10 10 0 0 0 17 15.85c-.72 0-1.42.09-2.09.26a7.7 7.7 0 0 0-1.91.8Zm-2 0a7.7 7.7 0 0 0-1.91-.8 8.4 8.4 0 0 0-2.09-.26c-.44 0-.87.02-1.3.07-.43.05-.86.13-1.28.24V6.95c.41-.15.83-.26 1.26-.34A7.7 7.7 0 0 1 7 6.5c.76 0 1.48.13 2.16.39.68.25 1.29.63 1.84 1.13Z" fill="currentColor"/></svg>',
26
+ photo: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 21c-.55 0-1.02-.2-1.41-.59A1.93 1.93 0 0 1 2 19V5c0-.55.2-1.02.59-1.41A1.93 1.93 0 0 1 4 3h16c.55 0 1.02.2 1.41.59.39.39.59.86.59 1.41v14c0 .55-.2 1.02-.59 1.41-.39.39-.86.59-1.41.59H4Zm0-2h16V5H4v14Zm1.5-2.5h13l-4.1-5.47-3.4 4.47-2.5-3.25L5.5 16.5ZM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" fill="currentColor"/></svg>',
27
+ };
28
+ /** Icon markup for `name`, or `null` when nothing built in matches. */
29
+ export function resolveFallbackToolIcon(name) {
30
+ return TOOL_FALLBACK_ICONS[name] || null;
31
+ }
@@ -0,0 +1,18 @@
1
+ /** Fully bundled open-source calculator adapter for the tool-provider registry. */
2
+ import type { CalculatorToolProviderInitConfig } from "./LazyCalculatorToolProvider.js";
3
+ import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
4
+ import type { ToolProviderCapabilities } from "./ToolProviderApi.js";
5
+ export type CortexToolProviderConfig = CalculatorToolProviderInitConfig;
6
+ export declare class CortexToolProvider extends LazyCalculatorToolProvider<CortexToolProviderConfig> {
7
+ readonly providerId = "cortex-calculator";
8
+ readonly providerName = "PIE Open-Source Calculator";
9
+ readonly version = "1";
10
+ readonly requiresAuth = false;
11
+ protected getDefinition(): {
12
+ backend: string;
13
+ moduleImportOperation: string;
14
+ loadProvider: () => Promise<typeof import("@pie-players/pie-calculator-cortex").CortexCalculatorProvider>;
15
+ initializationErrorMessage: string;
16
+ };
17
+ getCapabilities(): ToolProviderCapabilities;
18
+ }
@@ -0,0 +1,32 @@
1
+ /** Fully bundled open-source calculator adapter for the tool-provider registry. */
2
+ import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
3
+ export class CortexToolProvider extends LazyCalculatorToolProvider {
4
+ providerId = "cortex-calculator";
5
+ providerName = "PIE Open-Source Calculator";
6
+ version = "1";
7
+ requiresAuth = false;
8
+ getDefinition() {
9
+ return {
10
+ backend: "cortex",
11
+ moduleImportOperation: "cortex-provider-module-import",
12
+ loadProvider: async () => {
13
+ const module = await import("@pie-players/pie-calculator-cortex");
14
+ return module.CortexCalculatorProvider;
15
+ },
16
+ initializationErrorMessage: "Failed to initialize the PIE open-source calculator provider. Confirm that this browser supports module workers and that the bundled worker assets are available.",
17
+ };
18
+ }
19
+ getCapabilities() {
20
+ return {
21
+ supportsOffline: true,
22
+ requiresAuth: false,
23
+ maxInstances: null,
24
+ features: {
25
+ basic: true,
26
+ scientific: true,
27
+ graphing: true,
28
+ fourFunction: true,
29
+ },
30
+ };
31
+ }
32
+ }
@@ -1,107 +1,19 @@
1
- /**
2
- * Desmos Calculator Tool Provider
3
- *
4
- * Provides Desmos calculators (basic, scientific, graphing)
5
- * with authentication and proxy support.
6
- *
7
- * SECURITY BEST PRACTICE:
8
- * - Development: Pass apiKey directly for local testing
9
- * - Production: Use proxyEndpoint or authFetcher to keep API key server-side
10
- *
11
- * Part of PIE Assessment Toolkit.
12
- */
13
- import type { CalculatorProvider, DesmosCalculatorConfig } from "@pie-players/pie-calculator";
14
- import type { ToolProviderApi, ToolProviderCapabilities } from "./ToolProviderApi.js";
15
- /**
16
- * Desmos tool provider configuration
17
- */
18
- export interface DesmosToolProviderConfig {
19
- /**
20
- * Desmos API key (DEVELOPMENT ONLY)
21
- * Never expose in production client code!
22
- *
23
- * Obtain from: https://www.desmos.com/api
24
- */
25
- apiKey?: string;
26
- /**
27
- * Server proxy endpoint (PRODUCTION RECOMMENDED)
28
- * Backend handles API key securely
29
- *
30
- * @example '/api/desmos/token'
31
- * @example 'https://api.myapp.com/tools/desmos/auth'
32
- */
33
- proxyEndpoint?: string;
34
- /**
35
- * Default calculator configuration applied to all instances
36
- */
37
- defaultConfig?: DesmosCalculatorConfig;
38
- /**
39
- * Optional telemetry callback for tool/backend instrumentation.
40
- */
41
- onTelemetry?: (eventName: string, payload?: Record<string, unknown>) => void | Promise<void>;
42
- }
43
- /**
44
- * Desmos Calculator Tool Provider
45
- *
46
- * Wraps DesmosCalculatorProvider with the ToolProviderApi interface
47
- * for use in the ToolProviderRegistry.
48
- *
49
- * @example
50
- * ```typescript
51
- * const provider = new DesmosToolProvider();
52
- *
53
- * await provider.initialize({
54
- * apiKey: 'your-api-key', // Development only
55
- * proxyEndpoint: '/api/desmos/token', // Production
56
- * });
57
- *
58
- * const calculatorProvider = await provider.createInstance();
59
- * ```
60
- */
61
- export declare class DesmosToolProvider implements ToolProviderApi<DesmosToolProviderConfig, CalculatorProvider> {
1
+ /** Desmos calculator adapter for the generic tool-provider registry. */
2
+ import type { CalculatorProviderInit } from "@pie-players/pie-calculator";
3
+ import { LazyCalculatorToolProvider } from "./LazyCalculatorToolProvider.js";
4
+ import type { ToolProviderCapabilities } from "./ToolProviderApi.js";
5
+ /** Provider initialization is the provider-neutral calculator contract. */
6
+ export type DesmosToolProviderConfig = CalculatorProviderInit;
7
+ export declare class DesmosToolProvider extends LazyCalculatorToolProvider<DesmosToolProviderConfig> {
62
8
  readonly providerId = "desmos-calculator";
63
9
  readonly providerName = "Desmos Calculator";
64
- readonly category: "calculator";
65
10
  readonly version = "1.12";
66
11
  readonly requiresAuth = true;
67
- private desmosProvider;
68
- private config;
69
- private emitTelemetry;
70
- /**
71
- * Initialize Desmos calculator provider
72
- *
73
- * Loads the Desmos API library and authenticates with provided credentials.
74
- *
75
- * @param config Configuration with API key or proxy endpoint
76
- * @throws Error if initialization fails
77
- */
78
- initialize(config: DesmosToolProviderConfig): Promise<void>;
79
- /**
80
- * Create a calculator provider instance
81
- *
82
- * Returns the initialized Desmos calculator provider.
83
- *
84
- * @param config Optional instance-specific configuration (currently unused)
85
- * @returns Desmos calculator provider
86
- * @throws Error if provider not initialized
87
- */
88
- createInstance(config?: Partial<DesmosToolProviderConfig>): Promise<CalculatorProvider>;
89
- /**
90
- * Get provider capabilities
91
- *
92
- * @returns Desmos calculator capabilities
93
- */
12
+ protected getDefinition(): {
13
+ backend: string;
14
+ moduleImportOperation: string;
15
+ loadProvider: () => Promise<typeof import("@pie-players/pie-calculator-desmos").DesmosCalculatorProvider>;
16
+ initializationErrorMessage: string;
17
+ };
94
18
  getCapabilities(): ToolProviderCapabilities;
95
- /**
96
- * Check if provider is ready
97
- *
98
- * @returns true if provider is initialized
99
- */
100
- isReady(): boolean;
101
- /**
102
- * Clean up provider resources
103
- *
104
- * Destroys the Desmos calculator provider and releases resources.
105
- */
106
- destroy(): void;
107
19
  }