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

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 (57) hide show
  1. package/README.md +1 -1
  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 +13 -13
  5. package/dist/components/SectionToolBar.custom-element.js +1 -1
  6. package/dist/components/chunks/ItemToolBar-38mhtjsq.js +51 -0
  7. package/dist/components/chunks/ItemToolBar-9ymm7pd1.js +46 -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/runtime/SectionRuntimeEngine.d.ts +56 -0
  15. package/dist/runtime/SectionRuntimeEngine.js +66 -1
  16. package/dist/runtime/core/engine-resolver.d.ts +25 -1
  17. package/dist/runtime/registration-events.d.ts +52 -0
  18. package/dist/runtime/registration-events.js +2 -0
  19. package/dist/services/AccessibilityCatalogResolver.js +21 -5
  20. package/dist/services/I18nService.d.ts +28 -100
  21. package/dist/services/I18nService.js +43 -233
  22. package/dist/services/TTSService.d.ts +12 -0
  23. package/dist/services/TTSService.js +18 -17
  24. package/dist/services/ToolRegistry.d.ts +86 -2
  25. package/dist/services/ToolRegistry.js +30 -0
  26. package/dist/services/ToolkitCoordinator.d.ts +39 -37
  27. package/dist/services/ToolkitCoordinator.js +40 -0
  28. package/dist/services/audio-handoff.d.ts +39 -0
  29. package/dist/services/audio-handoff.js +58 -0
  30. package/dist/services/catalog-media.d.ts +35 -4
  31. package/dist/services/catalog-media.js +92 -3
  32. package/dist/services/framework-error.d.ts +15 -1
  33. package/dist/services/interfaces.d.ts +28 -0
  34. package/dist/services/pnp-standard-features.d.ts +1 -1
  35. package/dist/services/section-controller-types.d.ts +218 -7
  36. package/dist/services/selection-action.d.ts +49 -0
  37. package/dist/services/selection-action.js +10 -0
  38. package/dist/services/spoken-audio-cards.js +5 -1
  39. package/dist/services/tool-context.d.ts +6 -5
  40. package/dist/services/tool-context.js +197 -152
  41. package/dist/services/tool-icons.d.ts +18 -0
  42. package/dist/services/tool-icons.js +31 -0
  43. package/dist/services/tool-providers/DesmosToolProvider.d.ts +6 -5
  44. package/dist/services/tool-request.d.ts +106 -0
  45. package/dist/services/tool-request.js +127 -0
  46. package/dist/services/toolbar-items.d.ts +6 -0
  47. package/dist/tools/client.d.ts +0 -1
  48. package/dist/tools/client.js +0 -2
  49. package/dist/tools/internal.d.ts +6 -0
  50. package/dist/tools/internal.js +7 -0
  51. package/dist/tools/tool-surface-host.d.ts +57 -0
  52. package/dist/tools/tool-surface-host.js +610 -0
  53. package/package.json +10 -10
  54. package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
  55. package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
  56. package/dist/tools/calculators/desmos-provider.d.ts +0 -46
  57. package/dist/tools/calculators/desmos-provider.js +0 -393
@@ -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
@@ -281,14 +322,18 @@ export function hasReadableText(context) {
281
322
  */
282
323
  export function hasScienceContent(context) {
283
324
  const text = extractTextContent(context);
284
- // Look for common science indicators
325
+ // The element-symbol pattern used to be `/\b[A-Z][a-z]?\d*\b/`, which matches
326
+ // any one- or two-letter capitalised word: "It", "In", "A", "No". Every item
327
+ // beginning a sentence with one read as science.
328
+ if (hasChemicalFormula(text))
329
+ return true;
330
+ if (/[A-Z][a-z]?[\u2080-\u2089]/.test(text))
331
+ return true; // Subscripted: H₂O, CO₂
285
332
  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,
333
+ /chemistry|chemical|molecule|compound|periodic\s+table/i,
334
+ /\bchemical\s+element\b|\belement\s+symbol\b/i,
335
+ /biology|organism|\bDNA\b|\bRNA\b|protein|photosynthesis|ecosystem/i,
336
+ /physics|\bforce\b|\benergy\b|velocity|acceleration|momentum/i,
292
337
  ];
293
338
  return scienceIndicators.some((pattern) => pattern.test(text));
294
339
  }
@@ -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
+ }
@@ -10,10 +10,15 @@
10
10
  *
11
11
  * Part of PIE Assessment Toolkit.
12
12
  */
13
- import type { CalculatorProvider, DesmosCalculatorConfig } from "@pie-players/pie-calculator";
13
+ import type { CalculatorProvider } from "@pie-players/pie-calculator";
14
14
  import type { ToolProviderApi, ToolProviderCapabilities } from "./ToolProviderApi.js";
15
15
  /**
16
16
  * Desmos tool provider configuration
17
+ *
18
+ * Auth and telemetry only. Per-calculator Desmos options are owned by the
19
+ * calculator component, which derives them from the calculator type and passes
20
+ * them to `createCalculator()` directly — this provider never sees that config,
21
+ * so a defaults field here would silently do nothing.
17
22
  */
18
23
  export interface DesmosToolProviderConfig {
19
24
  /**
@@ -31,10 +36,6 @@ export interface DesmosToolProviderConfig {
31
36
  * @example 'https://api.myapp.com/tools/desmos/auth'
32
37
  */
33
38
  proxyEndpoint?: string;
34
- /**
35
- * Default calculator configuration applied to all instances
36
- */
37
- defaultConfig?: DesmosCalculatorConfig;
38
39
  /**
39
40
  * Optional telemetry callback for tool/backend instrumentation.
40
41
  */
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tool open requests.
3
+ *
4
+ * A surface that acts on the learner's current selection has to hand that selection
5
+ * to a tool it does not mount. The annotation strip is the case PIE ships: it is a
6
+ * section-scoped singleton in its own shadow root, and the tool it opens is mounted
7
+ * by a toolbar under a scoped instance id the strip cannot construct.
8
+ *
9
+ * Resolution is a claim, not a broadcast. Each toolbar registers as the target for its
10
+ * placement level, and a request reaches exactly one: the first target that currently
11
+ * hosts the tool, preferring section scope. A broadcast would open a panel in every
12
+ * toolbar whose scope contains the selection, which in a section player is the item
13
+ * card's toolbar and the section's both.
14
+ *
15
+ * `params` reaches the tool through the same seam a host-registered context resolver
16
+ * feeds, so receiving a request costs a tool nothing: whatever already reads
17
+ * `getToolRenderParams` sees it.
18
+ *
19
+ * Core names no capability. The requester supplies the tool id, which is why the
20
+ * pairing of a selection action to a dictionary lives in the composition layer.
21
+ */
22
+ import type { ToolPlacementLevel } from "./tools-config-normalizer.js";
23
+ /** Level a request resolves against when the requester names none. */
24
+ export declare const DEFAULT_TOOL_REQUEST_LEVEL: ToolPlacementLevel;
25
+ export interface ToolOpenRequest {
26
+ /** Unscoped tool id, as the registration declares it. */
27
+ toolId: string;
28
+ /**
29
+ * Merged over the host-resolved render params for this tool rather than
30
+ * replacing them, so a request carrying a term to look up leaves the endpoint
31
+ * the host configured in place.
32
+ */
33
+ params?: Record<string, unknown>;
34
+ /**
35
+ * Placement level of the toolbar that should open the tool.
36
+ *
37
+ * Naming one is a constraint and is honoured strictly: a requester that asks for
38
+ * `"item"` gets an item toolbar or nothing. Leaving it out asks for whichever
39
+ * toolbar hosts the tool, preferring `"section"` — the level at which a whole
40
+ * section shares one instance. A host that places a tool only at item scope would
41
+ * otherwise have the affordance silently disappear, and configuring a level here
42
+ * to match a placement made elsewhere is a step it has no reason to expect.
43
+ *
44
+ * At `"item"` and `"passage"` a section holds one target per card, and the first
45
+ * registered one that hosts the tool claims the request. A requester that needs a
46
+ * particular card's instance cannot express that here, and the gateway PIE ships
47
+ * does not need to: the strip is a section-scoped singleton acting on passage
48
+ * selections, so the selection belongs to no card, and what opens is a floating
49
+ * shell rather than anything rendered inside one.
50
+ */
51
+ level?: ToolPlacementLevel;
52
+ }
53
+ export interface ToolRequestTarget {
54
+ /** The placement level this toolbar renders. */
55
+ level: ToolPlacementLevel;
56
+ /** Whether this toolbar currently renders the tool, per its own policy pass. */
57
+ hostsTool: (toolId: string) => boolean;
58
+ /**
59
+ * Show the tool with `params` already applied.
60
+ *
61
+ * Show rather than toggle: a learner who selects a second word and asks for the
62
+ * dictionary again is asking for the dictionary, and a toggle would close it.
63
+ */
64
+ open: (toolId: string, params?: Record<string, unknown>) => void;
65
+ }
66
+ /**
67
+ * Registry of the toolbars a request can reach.
68
+ *
69
+ * Insertion order is the tie-break within a level, so a target registered while an
70
+ * earlier one is still mounted does not displace it.
71
+ */
72
+ export declare class ToolRequestRegistry {
73
+ private readonly targets;
74
+ private readonly changeListeners;
75
+ registerTarget(target: ToolRequestTarget): () => void;
76
+ /**
77
+ * Whether a request for this tool would reach a toolbar.
78
+ *
79
+ * A surface asks before offering the affordance: a button that silently does
80
+ * nothing is worse than an absent one, and availability moves with policy —
81
+ * hence {@link onTargetsChange}.
82
+ */
83
+ canRequest(toolId: string, level?: ToolPlacementLevel): boolean;
84
+ /** Returns whether a target claimed the request. */
85
+ request(request: ToolOpenRequest): boolean;
86
+ /**
87
+ * Fires when a toolbar registers or unregisters.
88
+ *
89
+ * Not when a registered toolbar's own visible set changes: `hostsTool` is read
90
+ * live, so a caller re-asking `canRequest` gets the current answer. A surface
91
+ * that needs to notice a policy change should also follow the policy signal it
92
+ * already has.
93
+ */
94
+ onTargetsChange(listener: () => void): () => void;
95
+ /**
96
+ * An explicit level is a constraint; the default is a preference.
97
+ *
98
+ * Falling back off `"section"` is what lets a host place a tool at item scope only
99
+ * and still have a section-scoped gateway reach it. Requesting a level explicitly
100
+ * does not fall back, because a requester that named one meant it.
101
+ */
102
+ private findTarget;
103
+ private findTargetAtLevel;
104
+ private hostsTool;
105
+ private notifyChange;
106
+ }