@pie-players/pie-assessment-toolkit 1.0.0-beta.0 → 1.0.0-beta.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.
Files changed (35) hide show
  1. package/README.md +3 -1
  2. package/dist/components/ItemScope.custom-element.js +1 -1
  3. package/dist/components/ItemToolBar.custom-element.js +1 -1
  4. package/dist/components/PieAssessmentToolkit.custom-element.js +7 -7
  5. package/dist/components/SectionToolBar.custom-element.js +1 -1
  6. package/dist/components/chunks/ItemToolBar-341vf20n.js +2 -0
  7. package/dist/components/chunks/ItemToolBar-jpac75fa.js +4 -0
  8. package/dist/components/chunks/ItemToolBar-wbem8cna.js +40 -0
  9. package/dist/policy/core/compose-decision.js +2 -0
  10. package/dist/policy/core/decision-types.d.ts +10 -5
  11. package/dist/policy/core/feature-decision.d.ts +3 -3
  12. package/dist/policy/core/pnp-policy-inputs.d.ts +2 -1
  13. package/dist/policy/core/pnp-policy-inputs.js +6 -2
  14. package/dist/policy/core/provenance.d.ts +1 -1
  15. package/dist/policy/core/tool-parameters.d.ts +3 -2
  16. package/dist/policy/core/tool-parameters.js +17 -5
  17. package/dist/policy/sources/PnpPolicySource.d.ts +4 -2
  18. package/dist/policy/sources/PnpPolicySource.js +27 -1
  19. package/dist/services/TTSService.d.ts +5 -4
  20. package/dist/services/TTSService.js +25 -11
  21. package/dist/services/ToolRegistry.d.ts +4 -3
  22. package/dist/services/ToolRegistry.js +2 -1
  23. package/dist/services/ToolkitCoordinator.js +6 -5
  24. package/dist/services/interfaces.d.ts +4 -0
  25. package/dist/services/tool-context.d.ts +33 -2
  26. package/dist/services/tool-context.js +69 -49
  27. package/dist/services/toolbar-decided-tools.d.ts +1 -0
  28. package/dist/services/toolbar-decided-tools.js +1 -0
  29. package/dist/services/tts/browser-provider.js +9 -2
  30. package/dist/tools/registration.d.ts +2 -2
  31. package/dist/tools/registration.js +1 -1
  32. package/package.json +7 -7
  33. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +0 -4
  34. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +0 -40
  35. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +0 -2
@@ -19,6 +19,7 @@ export class PnpPolicySource {
19
19
  const allSupports = new Set();
20
20
  pnp?.supports?.forEach((s) => allSupports.add(s));
21
21
  pnp?.prohibitedSupports?.forEach((s) => allSupports.add(s));
22
+ pnp?.activateAtInit?.forEach((s) => allSupports.add(s));
22
23
  districtPolicy?.blockedTools?.forEach((s) => allSupports.add(s));
23
24
  districtPolicy?.requiredTools?.forEach((s) => allSupports.add(s));
24
25
  for (const s of Object.keys(testAdmin?.toolOverrides ?? {})) {
@@ -162,6 +163,7 @@ export class PnpPolicySource {
162
163
  });
163
164
  return;
164
165
  }
166
+ const activateAtInit = ctx.pnp?.activateAtInit?.includes(supportId) === true;
165
167
  const mandate = ctx.itemSettings?.requiredTools?.includes(supportId)
166
168
  ? "item-requirement"
167
169
  : ctx.districtPolicy?.requiredTools?.includes(supportId)
@@ -175,6 +177,7 @@ export class PnpPolicySource {
175
177
  out.perToolFlags.set(supportId, {
176
178
  required: mandate !== undefined,
177
179
  alwaysAvailable: true,
180
+ activateAtInit,
178
181
  rule: "test-admin-override",
179
182
  });
180
183
  out.decisions.push({
@@ -194,6 +197,7 @@ export class PnpPolicySource {
194
197
  out.perToolFlags.set(supportId, {
195
198
  required: true,
196
199
  alwaysAvailable: false,
200
+ activateAtInit,
197
201
  rule: "item-requirement",
198
202
  });
199
203
  out.decisions.push({
@@ -213,6 +217,7 @@ export class PnpPolicySource {
213
217
  out.perToolFlags.set(supportId, {
214
218
  required: true,
215
219
  alwaysAvailable: false,
220
+ activateAtInit,
216
221
  rule: "district-requirement",
217
222
  });
218
223
  out.decisions.push({
@@ -226,11 +231,31 @@ export class PnpPolicySource {
226
231
  });
227
232
  return;
228
233
  }
229
- // 8. Profile supports (student needs)
234
+ // 8. Profile supports (student needs). `activateAtInit` grants as
235
+ // `supports` does and names the tool to turn on at start.
236
+ if (activateAtInit) {
237
+ out.perToolFlags.set(supportId, {
238
+ required: false,
239
+ alwaysAvailable: true,
240
+ activateAtInit,
241
+ rule: "pnp-activate-at-init",
242
+ });
243
+ out.decisions.push({
244
+ precedence: 8,
245
+ rule: "pnp-activate-at-init",
246
+ featureId: supportId,
247
+ action: "enable",
248
+ sourceType: "student",
249
+ reason: `Student PNP profile turns on "${supportId}" at start`,
250
+ value: ctx.pnp?.activateAtInit,
251
+ });
252
+ return;
253
+ }
230
254
  if (ctx.pnp?.supports?.includes(supportId)) {
231
255
  out.perToolFlags.set(supportId, {
232
256
  required: false,
233
257
  alwaysAvailable: true,
258
+ activateAtInit: false,
234
259
  rule: "pnp-support",
235
260
  });
236
261
  out.decisions.push({
@@ -281,6 +306,7 @@ export class PnpPolicySource {
281
306
  ["pnp-prohibited", pnp?.prohibitedSupports],
282
307
  ["item-requirement", items.flatMap((item) => item.requiredTools ?? [])],
283
308
  ["district-requirement", settings?.districtPolicy?.requiredTools],
309
+ ["pnp-activate-at-init", pnp?.activateAtInit],
284
310
  ["pnp-support", pnp?.supports],
285
311
  ];
286
312
  for (const [rule, ids] of named) {
@@ -121,7 +121,8 @@ export declare class TTSService {
121
121
  */
122
122
  initialize(provider: ITTSProvider, config?: Partial<ToolkitTTSConfig>): Promise<void>;
123
123
  /**
124
- * Update rate, pitch, voice or provider options without reinitializing.
124
+ * Update rate, pitch, volume, voice or provider options without
125
+ * reinitializing.
125
126
  * `providerOptions` merge over the ones already configured.
126
127
  */
127
128
  updateSettings(settings: Partial<TTSConfig>): Promise<void>;
@@ -254,10 +255,10 @@ export declare class TTSService {
254
255
  */
255
256
  speak(target: Range | Element, options?: SpeakOptions): Promise<void>;
256
257
  /**
257
- * Sets the rate a run starts at, after any rate write in flight. The run has
258
- * no playback yet, so nothing restarts.
258
+ * Sets the rate, pitch and volume a run starts at, after any rate write in
259
+ * flight. The run has no playback yet, so nothing restarts.
259
260
  */
260
- private applyRunRate;
261
+ private applyRunSettings;
261
262
  /**
262
263
  * Suppression is read from the live ancestors of what is read, because the
263
264
  * element carrying it may sit above the content root.
@@ -389,7 +389,8 @@ export class TTSService {
389
389
  this.provider = await provider.initialize(config);
390
390
  }
391
391
  /**
392
- * Update rate, pitch, voice or provider options without reinitializing.
392
+ * Update rate, pitch, volume, voice or provider options without
393
+ * reinitializing.
393
394
  * `providerOptions` merge over the ones already configured.
394
395
  */
395
396
  async updateSettings(settings) {
@@ -1160,24 +1161,37 @@ export class TTSService {
1160
1161
  return;
1161
1162
  }
1162
1163
  const runId = waitingRunId ?? this.beginRun(owner);
1163
- if (options.rate !== undefined) {
1164
- await this.applyRunRate(options.rate);
1164
+ if (options.rate !== undefined ||
1165
+ options.pitch !== undefined ||
1166
+ options.volume !== undefined) {
1167
+ await this.applyRunSettings(options);
1165
1168
  if (runId !== this.speakRunId)
1166
1169
  return;
1167
1170
  }
1168
1171
  await this.speakContent(content.text, content.options, runId);
1169
1172
  }
1170
1173
  /**
1171
- * Sets the rate a run starts at, after any rate write in flight. The run has
1172
- * no playback yet, so nothing restarts.
1174
+ * Sets the rate, pitch and volume a run starts at, after any rate write in
1175
+ * flight. The run has no playback yet, so nothing restarts.
1173
1176
  */
1174
- async applyRunRate(rate) {
1177
+ async applyRunSettings(options) {
1175
1178
  await this.playbackRateWriteQueue;
1176
- const nextRate = this.normalizePlaybackRate(rate);
1177
- const currentRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate ?? 1));
1178
- if (nextRate === currentRate)
1179
- return;
1180
- await this.updateSettings({ rate: nextRate });
1179
+ const settings = {};
1180
+ if (options.rate !== undefined) {
1181
+ const nextRate = this.normalizePlaybackRate(options.rate);
1182
+ const currentRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate ?? 1));
1183
+ if (nextRate !== currentRate)
1184
+ settings.rate = nextRate;
1185
+ }
1186
+ if (options.pitch !== undefined && options.pitch !== this.ttsConfig.pitch) {
1187
+ settings.pitch = options.pitch;
1188
+ }
1189
+ if (options.volume !== undefined &&
1190
+ options.volume !== this.ttsConfig.volume) {
1191
+ settings.volume = options.volume;
1192
+ }
1193
+ if (Object.keys(settings).length > 0)
1194
+ await this.updateSettings(settings);
1181
1195
  }
1182
1196
  /**
1183
1197
  * Suppression is read from the live ancestors of what is read, because the
@@ -114,9 +114,10 @@ export interface ToolbarContext {
114
114
  getResolvedToolContext?: (toolId: string) => ResolvedToolContext | null;
115
115
  getToolRenderParams?: (toolId: string) => Record<string, unknown> | null;
116
116
  /**
117
- * The tool's policy parameters on this toolbar: the item's `toolParameters`
118
- * entry on an item's own toolbar, else the assessment's `toolParameters` entry.
119
- * Authored content, so a registration checks what it reads.
117
+ * The tool's policy parameters on this toolbar: the profile's, assessment's
118
+ * and, on an item's own toolbar, item's `toolParameters` entries merged key by
119
+ * key, later ones winning. Authored content, so a registration checks what it
120
+ * reads.
120
121
  */
121
122
  getToolParameters?: <K extends string>(toolId: K) => ToolParametersFor<K> | null;
122
123
  }
@@ -12,6 +12,7 @@
12
12
  * warnings and the toolbar and surface render helpers.
13
13
  */
14
14
  import { dynamicMessageKey } from "@pie-players/pie-players-shared/i18n/provider";
15
+ import { settleAll } from "@pie-players/pie-players-shared/promise";
15
16
  import { resolveToolTag, } from "../tools/tool-tag-map.js";
16
17
  import { PENDING_INPUT_WARNING_DELAY_MS, warnOncePerDocument, } from "../runtime/page-warnings.js";
17
18
  const logToolCallbackFailure = (toolId, phase, error) => console.error(`[ToolRegistry] Tool '${toolId}' ${phase} check threw:`, error);
@@ -608,7 +609,7 @@ export class ToolRegistry {
608
609
  * tools loaded and usable. A failed tool is retried on the next call.
609
610
  */
610
611
  async ensureToolModulesLoaded(toolIds) {
611
- const results = await Promise.allSettled(toolIds.map((toolId) => this.ensureToolModuleLoaded(toolId)));
612
+ const results = await settleAll(toolIds.map((toolId) => this.ensureToolModuleLoaded(toolId)));
612
613
  const failures = new Map();
613
614
  results.forEach((result, index) => {
614
615
  if (result.status === "rejected")
@@ -21,6 +21,7 @@
21
21
  * - tool-open requests;
22
22
  * - config-change application and TTS reconfiguration, last.
23
23
  */
24
+ import { settleAll } from "@pie-players/pie-players-shared/promise";
24
25
  import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
25
26
  import { collectToolConfigDiagnostics, normalizeToolConfigStrictness, reportToolConfigDiagnostics, } from "./tool-config-validation.js";
26
27
  import { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
@@ -1272,7 +1273,7 @@ export class ToolkitCoordinator {
1272
1273
  // retired controller out of the cache immediately, but do not let its
1273
1274
  // replacement hydrate until that exact cohort's save/dispose pipeline has
1274
1275
  // settled. Other cohorts remain independent.
1275
- await Promise.allSettled([pendingDisposal]);
1276
+ await settleAll([pendingDisposal]);
1276
1277
  }
1277
1278
  if (this.disposePromise !== null) {
1278
1279
  throw new ToolkitCoordinatorDisposedError();
@@ -1540,7 +1541,7 @@ export class ToolkitCoordinator {
1540
1541
  return existingDisposal;
1541
1542
  if (!initEntry)
1542
1543
  return Promise.resolve();
1543
- const retirementBarrier = Promise.allSettled([initEntry.promise]).then(() => undefined);
1544
+ const retirementBarrier = settleAll([initEntry.promise]).then(() => undefined);
1544
1545
  return this.trackSectionControllerDisposal(mapKey, retirementBarrier);
1545
1546
  }
1546
1547
  const persistenceStrategy = this.sectionPersistenceStrategies.get(mapKey);
@@ -1576,7 +1577,7 @@ export class ToolkitCoordinator {
1576
1577
  persistenceStrategy,
1577
1578
  });
1578
1579
  const disposalBarrier = initEntry
1579
- ? Promise.allSettled([initEntry.promise]).then(() => controllerDisposalPromise)
1580
+ ? settleAll([initEntry.promise]).then(() => controllerDisposalPromise)
1580
1581
  : controllerDisposalPromise;
1581
1582
  return this.trackSectionControllerDisposal(mapKey, disposalBarrier);
1582
1583
  }
@@ -1694,7 +1695,7 @@ export class ToolkitCoordinator {
1694
1695
  attemptId: key.attemptId,
1695
1696
  });
1696
1697
  }
1697
- await Promise.allSettled(Array.from(this.sectionControllerDisposePromises.values()));
1698
+ await settleAll(Array.from(this.sectionControllerDisposePromises.values()));
1698
1699
  this.sectionControllers.clear();
1699
1700
  this.sectionControllerKeys.clear();
1700
1701
  this.sectionControllerInitEntries.clear();
@@ -1742,7 +1743,7 @@ export class ToolkitCoordinator {
1742
1743
  for (const promise of this.providerInitPromises.values()) {
1743
1744
  pending.add(promise);
1744
1745
  }
1745
- await Promise.allSettled(pending);
1746
+ await settleAll(pending);
1746
1747
  }
1747
1748
  /**
1748
1749
  * Initialize the TTS service from the `textToSpeech` tool config.
@@ -211,6 +211,10 @@ export interface SpeakOptions {
211
211
  ownerId?: string;
212
212
  /** The rate the read starts at; it stays the service's rate afterwards. */
213
213
  rate?: number;
214
+ /** The pitch the read starts at, 0–2 with 1 normal; it stays the service's. */
215
+ pitch?: number;
216
+ /** The volume the read starts at, 0–1; it stays the service's. */
217
+ volume?: number;
214
218
  }
215
219
  /**
216
220
  * TTS service interface
@@ -107,9 +107,40 @@ export type ToolContext = AssessmentToolContext | SectionToolContext | ItemToolC
107
107
  */
108
108
  export declare function hasMathContent(context: ToolContext): boolean;
109
109
  /**
110
- * Helper to check if context contains choice-based interactions
110
+ * What one element of a context declares about the PNP supports it implements.
111
+ *
112
+ * An element declares them on its class, as `static pieSupports`: a list of AfA
113
+ * PNP 3.0 support terms, such as `answer-masking`. The toolkit reads the class
114
+ * `customElements` holds under the element's runtime tag, so the declaration is
115
+ * known before the element renders. What a support needs from the rendered
116
+ * element is between the element and the tool that provides it, and that tool
117
+ * documents it.
118
+ */
119
+ export interface ElementSupports {
120
+ /** The element's model id. */
121
+ elementId: string | undefined;
122
+ /** The tag the item binds the element to, as authored. */
123
+ tag: string;
124
+ /** The element's package spec in the item's `elements` map, when it has one. */
125
+ packageSpec: string | undefined;
126
+ /**
127
+ * The terms the element's class declares: `undefined` while the class is
128
+ * undefined, and for a class that declares nothing. An empty list is a
129
+ * declaration that the element implements none.
130
+ */
131
+ supports: readonly string[] | undefined;
132
+ /**
133
+ * The runtime tags the class can still be defined under, empty once it is
134
+ * defined. A definition landing under one of them can change `supports`.
135
+ */
136
+ pendingTags: readonly string[];
137
+ }
138
+ /**
139
+ * The supports each element of a context declares: the element's own at element
140
+ * level, every element of the item or passage config at those levels, and none
141
+ * elsewhere. A model that names no element renders nothing and is skipped.
111
142
  */
112
- export declare function hasChoiceInteraction(context: ToolContext): boolean;
143
+ export declare function elementSupports(context: ToolContext): ElementSupports[];
113
144
  /**
114
145
  * Helper to check if context contains text that can be read aloud
115
146
  */
@@ -4,6 +4,8 @@
4
4
  * Defines the context types passed to tools for making visibility and behavior decisions.
5
5
  * Each context level provides increasingly specific information about where the tool is being used.
6
6
  */
7
+ import { runtimeElementTags } from "@pie-players/pie-players-shared/pie";
8
+ import { parseVersionedTagName } from "@pie-players/pie-players-shared/pie/tag-names";
7
9
  /**
8
10
  * Type guard to check if context is at item level
9
11
  */
@@ -300,63 +302,81 @@ export function hasMathContent(context) {
300
302
  hasSpacedSubtraction(text) ||
301
303
  hasAppliedPercentage(text));
302
304
  }
303
- const CHOICE_INTERACTION_ELEMENTS = new Set([
304
- "multiple-choice",
305
- "inline-choice",
306
- "select-text",
307
- "ebsr",
308
- ]);
309
- // Content names one interaction several ways: `multiple-choice`, `pie-multiple-choice`,
310
- // `pie-element-multiple-choice`, a runtime `--version-*` tag, or any tag the item's
311
- // `elements` map binds to `@pie-element/multiple-choice`.
312
- const baseElementName = (tag) => tag
313
- .toLowerCase()
314
- .replace(/--version-[0-9a-z-]+$/, "")
315
- .replace(/^pie-(element-)?/, "");
316
- function isChoiceElement(tag, elements) {
317
- if (CHOICE_INTERACTION_ELEMENTS.has(baseElementName(tag)))
318
- return true;
305
+ function packageSpecFor(tag, elements) {
319
306
  if (!elements || typeof elements !== "object")
320
- return false;
307
+ return undefined;
321
308
  const map = elements;
322
- const spec = map[tag] ?? map[tag.replace(/--version-[0-9A-Za-z-]+$/, "")];
323
- if (typeof spec !== "string")
324
- return false;
325
- const packageName = spec.match(/^@pie-element\/([^@]+)/)?.[1];
326
- return !!packageName && CHOICE_INTERACTION_ELEMENTS.has(packageName);
309
+ const spec = map[tag] ?? map[parseVersionedTagName(tag).baseName];
310
+ return typeof spec === "string" && spec ? spec : undefined;
311
+ }
312
+ function declaredSupports(elementClass) {
313
+ let declared;
314
+ try {
315
+ declared = elementClass.pieSupports;
316
+ }
317
+ catch {
318
+ return undefined;
319
+ }
320
+ if (!Array.isArray(declared))
321
+ return undefined;
322
+ return declared.filter((term) => typeof term === "string");
323
+ }
324
+ function readElementSupports(model, elements) {
325
+ if (!model || typeof model !== "object")
326
+ return null;
327
+ const { id, element } = model;
328
+ if (typeof element !== "string" || !element)
329
+ return null;
330
+ const packageSpec = packageSpecFor(element, elements);
331
+ const candidates = runtimeElementTags(element, packageSpec);
332
+ const registry = typeof customElements === "undefined" ? undefined : customElements;
333
+ for (const tag of candidates) {
334
+ const elementClass = registry?.get(tag);
335
+ if (elementClass) {
336
+ return {
337
+ elementId: typeof id === "string" ? id : undefined,
338
+ tag: element,
339
+ packageSpec,
340
+ supports: declaredSupports(elementClass),
341
+ pendingTags: [],
342
+ };
343
+ }
344
+ }
345
+ return {
346
+ elementId: typeof id === "string" ? id : undefined,
347
+ tag: element,
348
+ packageSpec,
349
+ supports: undefined,
350
+ pendingTags: registry ? candidates : [],
351
+ };
327
352
  }
328
353
  /**
329
- * Helper to check if context contains choice-based interactions
354
+ * The supports each element of a context declares: the element's own at element
355
+ * level, every element of the item or passage config at those levels, and none
356
+ * elsewhere. A model that names no element renders nothing and is skipped.
330
357
  */
331
- export function hasChoiceInteraction(context) {
358
+ export function elementSupports(context) {
359
+ let config;
360
+ let models;
332
361
  if (isElementContext(context)) {
333
- const config = context.item.config;
334
- if (!config?.models)
335
- return false;
336
- const model = findElementModel(config.models, context.elementId);
337
- if (!model)
338
- return false;
339
- const type = model.element || "";
340
- return isChoiceElement(type, config.elements);
362
+ config = context.item.config;
363
+ models = [findElementModel(config?.models, context.elementId)];
341
364
  }
342
- if (isItemContext(context)) {
343
- const config = context.item.config;
344
- return normalizeModels(config?.models).some((candidate) => {
345
- if (!candidate || typeof candidate !== "object")
346
- return false;
347
- const model = candidate;
348
- const type = model.element || "";
349
- // A model that names its element has answered the question, whichever way
350
- // the answer falls: `placement-ordering`, `categorize` and
351
- // `drag-in-the-blank` each hold their draggables in `choices` without
352
- // being choice interactions. The `choices` heuristic remains for configs
353
- // that name no element.
354
- if (type)
355
- return isChoiceElement(type, config?.elements);
356
- return Array.isArray(model.choices) && model.choices.length > 0;
357
- });
365
+ else if (isItemContext(context)) {
366
+ config = context.item?.config;
367
+ models = normalizeModels(config?.models);
358
368
  }
359
- return false;
369
+ else if (isPassageContext(context)) {
370
+ config = context.passage?.config;
371
+ models = normalizeModels(config?.models);
372
+ }
373
+ else {
374
+ return [];
375
+ }
376
+ return models.flatMap((model) => {
377
+ const supports = readElementSupports(model, config?.elements);
378
+ return supports ? [supports] : [];
379
+ });
360
380
  }
361
381
  /**
362
382
  * Helper to check if context contains text that can be read aloud
@@ -4,6 +4,7 @@ export interface DecidedToolbarTool {
4
4
  toolId: string;
5
5
  required: boolean;
6
6
  alwaysAvailable: boolean;
7
+ activateAtInit: boolean;
7
8
  parameters?: Record<string, unknown>;
8
9
  }
9
10
  /**
@@ -17,6 +17,7 @@ export function createDecidedToolsTracker() {
17
17
  toolId: entry.toolId,
18
18
  required: entry.required,
19
19
  alwaysAvailable: entry.alwaysAvailable,
20
+ activateAtInit: entry.activateAtInit,
20
21
  parameters: entry.parameters,
21
22
  }));
22
23
  if (last?.renderVersion === renderVersion &&
@@ -37,6 +37,8 @@ export const browserFallbackConfig = (config) => {
37
37
  fallback.rate = config.rate;
38
38
  if (config.pitch !== undefined)
39
39
  fallback.pitch = config.pitch;
40
+ if (config.volume !== undefined)
41
+ fallback.volume = config.volume;
40
42
  if (config.mathTokenHighlighting !== undefined) {
41
43
  fallback.mathTokenHighlighting = config.mathTokenHighlighting;
42
44
  }
@@ -437,8 +439,10 @@ class BrowserTTSProviderImpl {
437
439
  utterance.voice = voice;
438
440
  if (this.config?.rate)
439
441
  utterance.rate = this.config.rate;
440
- if (this.config?.pitch)
442
+ if (this.config?.pitch !== undefined)
441
443
  utterance.pitch = this.config.pitch;
444
+ if (this.config?.volume !== undefined)
445
+ utterance.volume = this.config.volume;
442
446
  let didStart = false;
443
447
  let settled = false;
444
448
  let startTimeout = null;
@@ -616,7 +620,7 @@ class BrowserTTSProviderImpl {
616
620
  this.releaseHold();
617
621
  }
618
622
  /**
619
- * Update settings dynamically (rate, pitch, voice)
623
+ * Update settings dynamically (rate, pitch, volume, voice)
620
624
  * Changes take effect on the next speak() call
621
625
  */
622
626
  updateSettings(settings) {
@@ -629,6 +633,9 @@ class BrowserTTSProviderImpl {
629
633
  if (settings.pitch !== undefined) {
630
634
  this.config.pitch = settings.pitch;
631
635
  }
636
+ if (settings.volume !== undefined) {
637
+ this.config.volume = settings.volume;
638
+ }
632
639
  if (settings.voice !== undefined) {
633
640
  this.config.voice = settings.voice;
634
641
  }
@@ -32,8 +32,8 @@ export type { ContentCapabilityPhase, ContentCapabilityPolicy, ResolveContentCap
32
32
  export { resolveContentCapabilities } from "./content-capability-resolution.js";
33
33
  export type { ToolSurfaceHost, ToolSurfaceHostInput, ToolSurfaceHostOptions, ToolSurfaceHostSnapshot, ToolSurfaceScope, } from "./tool-surface-host.js";
34
34
  export { createToolSurfaceHost } from "./tool-surface-host.js";
35
- export type { ToolContext } from "../services/tool-context.js";
36
- export { hasChoiceInteraction, hasMathContent, hasReadableText, hasSpokenContent, hasScienceContent, } from "../services/tool-context.js";
35
+ export type { ElementSupports, ToolContext, } from "../services/tool-context.js";
36
+ export { elementSupports, hasMathContent, hasReadableText, hasSpokenContent, hasScienceContent, } from "../services/tool-context.js";
37
37
  export type { ParsedToolInstanceId } from "../services/tool-instance-id.js";
38
38
  export { createScopedToolId, parseScopedToolId, } from "../services/tool-instance-id.js";
39
39
  export type { ToolComponentFactory, ToolComponentFactoryMap, ToolComponentOverrides, ToolTagMap, } from "./tool-tag-map.js";
@@ -30,7 +30,7 @@ export { reportToolFailure } from "../services/tool-failure.js";
30
30
  export { resolveFallbackToolIcon } from "../services/tool-icons.js";
31
31
  export { resolveContentCapabilities } from "./content-capability-resolution.js";
32
32
  export { createToolSurfaceHost } from "./tool-surface-host.js";
33
- export { hasChoiceInteraction, hasMathContent, hasReadableText, hasSpokenContent, hasScienceContent, } from "../services/tool-context.js";
33
+ export { elementSupports, hasMathContent, hasReadableText, hasSpokenContent, hasScienceContent, } from "../services/tool-context.js";
34
34
  export { createScopedToolId, parseScopedToolId, } from "../services/tool-instance-id.js";
35
35
  export { createToolElement, resolveToolTag } from "./tool-tag-map.js";
36
36
  // Toolbar button/overlay wiring shared by every toolbar-toggle registration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-assessment-toolkit",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "type": "module",
5
5
  "description": "PIE assessment toolkit: composable services + reference implementation for assessment players and tool coordination",
6
6
  "license": "MIT",
@@ -69,17 +69,17 @@
69
69
  "test": "bun test"
70
70
  },
71
71
  "dependencies": {
72
- "@pie-players/pie-calculator": "1.0.0-beta.0",
73
- "@pie-players/pie-context": "1.0.0-beta.0",
74
- "@pie-players/pie-players-shared": "1.0.0-beta.0",
75
- "@pie-players/pie-tts": "1.0.0-beta.0",
72
+ "@pie-players/pie-calculator": "1.0.0-beta.2",
73
+ "@pie-players/pie-context": "1.0.0-beta.2",
74
+ "@pie-players/pie-players-shared": "1.0.0-beta.2",
75
+ "@pie-players/pie-tts": "1.0.0-beta.2",
76
76
  "speech-rule-engine": "5.0.0-rc.4"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@biomejs/biome": "^2.5.15",
80
80
  "@happy-dom/global-registrator": "^20.14.5",
81
- "@pie-players/tts-client-server": "1.0.0-beta.0",
82
- "svelte": "^5.57.1",
81
+ "@pie-players/tts-client-server": "1.0.0-beta.2",
82
+ "svelte": "^5.57.2",
83
83
  "svelte-check": "^4.7.6",
84
84
  "typescript": "^5.9.3"
85
85
  },