@pie-players/pie-assessment-toolkit 0.3.66 → 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.
- package/README.md +1 -1
- package/dist/attempt/AssessmentSession.d.ts +7 -27
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-38mhtjsq.js +51 -0
- package/dist/components/chunks/ItemToolBar-9ymm7pd1.js +46 -0
- package/dist/components/item-toolbar-element.js +1 -0
- package/dist/components/pie-assessment-toolkit-element.js +1 -0
- package/dist/components/section-toolbar-element.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +28 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +12 -0
- package/dist/policy/core/ToolPolicyEngine.js +28 -2
- package/dist/policy/core/feature-decision.d.ts +28 -2
- package/dist/policy/core/feature-decision.js +35 -0
- package/dist/policy/engine.d.ts +1 -1
- package/dist/policy/engine.js +1 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
- package/dist/runtime/SectionRuntimeEngine.js +66 -1
- package/dist/runtime/core/engine-resolver.d.ts +25 -1
- package/dist/runtime/registration-events.d.ts +52 -0
- package/dist/runtime/registration-events.js +2 -0
- package/dist/services/AccessibilityCatalogResolver.js +21 -5
- package/dist/services/I18nService.d.ts +28 -100
- package/dist/services/I18nService.js +43 -233
- package/dist/services/TTSService.d.ts +12 -0
- package/dist/services/TTSService.js +18 -17
- package/dist/services/ToolRegistry.d.ts +86 -2
- package/dist/services/ToolRegistry.js +30 -0
- package/dist/services/ToolkitCoordinator.d.ts +39 -37
- package/dist/services/ToolkitCoordinator.js +40 -0
- package/dist/services/audio-handoff.d.ts +39 -0
- package/dist/services/audio-handoff.js +58 -0
- package/dist/services/catalog-media.d.ts +35 -4
- package/dist/services/catalog-media.js +92 -3
- package/dist/services/framework-error.d.ts +15 -1
- package/dist/services/interfaces.d.ts +28 -0
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/section-controller-types.d.ts +218 -7
- package/dist/services/selection-action.d.ts +49 -0
- package/dist/services/selection-action.js +10 -0
- package/dist/services/spoken-audio-cards.js +5 -1
- package/dist/services/tool-context.d.ts +6 -5
- package/dist/services/tool-context.js +197 -152
- package/dist/services/tool-icons.d.ts +18 -0
- package/dist/services/tool-icons.js +31 -0
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +6 -5
- package/dist/services/tool-request.d.ts +106 -0
- package/dist/services/tool-request.js +127 -0
- package/dist/services/toolbar-items.d.ts +6 -0
- package/dist/tools/client.d.ts +0 -1
- package/dist/tools/client.js +0 -2
- package/dist/tools/content-capability-resolution.d.ts +106 -0
- package/dist/tools/content-capability-resolution.js +136 -0
- package/dist/tools/internal.d.ts +8 -0
- package/dist/tools/internal.js +8 -0
- package/dist/tools/tool-surface-host.d.ts +57 -0
- package/dist/tools/tool-surface-host.js +610 -0
- package/package.json +14 -10
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
- package/dist/tools/calculators/desmos-provider.d.ts +0 -46
- package/dist/tools/calculators/desmos-provider.js +0 -393
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Central registry for all assessment tools. Manages tool metadata, visibility logic,
|
|
5
5
|
* and button/instance creation. Supports dynamic registration and override by integrators.
|
|
6
6
|
*/
|
|
7
|
+
import type { I18nProvider } from "@pie-players/pie-players-shared/i18n/types";
|
|
7
8
|
import type { CatalogOwnerSnapshot } from "./AccessibilityCatalogResolver.js";
|
|
8
9
|
import type { ToolContext, ToolLevel } from "./tool-context.js";
|
|
9
10
|
import type { ToolComponentOverrides } from "../tools/tool-tag-map.js";
|
|
@@ -30,6 +31,17 @@ export interface ToolToolbarButtonDefinition {
|
|
|
30
31
|
* `assertToolRegistrationShape` enforces that.
|
|
31
32
|
*/
|
|
32
33
|
icon?: string;
|
|
34
|
+
/**
|
|
35
|
+
* FontAwesome icon name, opting this button into `<nds-icon-button>` rendering
|
|
36
|
+
* where the host enables NDS icons (`ndsIcons`). Absent means the toolbar keeps
|
|
37
|
+
* its own button rendering, which is what every capability got before the NDS
|
|
38
|
+
* button existed.
|
|
39
|
+
*
|
|
40
|
+
* A declaration rather than a toolId lookup in the toolbar: which capabilities
|
|
41
|
+
* a deployment renders through the host's design system is a composition-layer
|
|
42
|
+
* decision, and the generic core names no capability.
|
|
43
|
+
*/
|
|
44
|
+
faIconName?: string;
|
|
33
45
|
ariaLabel: string;
|
|
34
46
|
tooltip?: string;
|
|
35
47
|
onClick: () => void;
|
|
@@ -49,7 +61,24 @@ export interface ToolbarContext {
|
|
|
49
61
|
};
|
|
50
62
|
itemId: string;
|
|
51
63
|
catalogId: string;
|
|
64
|
+
/**
|
|
65
|
+
* Content-alternate language: which authored alternate the catalog resolver
|
|
66
|
+
* should select. Not the interface locale — see {@link ToolbarContext.i18n}. The
|
|
67
|
+
* two are independent by QTI 3's own statement, and conflating them is how a
|
|
68
|
+
* Spanish passage ends up forcing Spanish widget chrome.
|
|
69
|
+
*/
|
|
52
70
|
language: string;
|
|
71
|
+
/**
|
|
72
|
+
* Interface-locale provider for this capability's own UI strings.
|
|
73
|
+
*
|
|
74
|
+
* Required, and always the facade `resolveInterfaceI18n` returns: the toolbar
|
|
75
|
+
* resolves it once from the toolkit runtime context, so a registration reads
|
|
76
|
+
* `toolbarContext.i18n` and neither repeats the no-publisher fallback nor
|
|
77
|
+
* misses the republish that a locale change produces. With no publisher the
|
|
78
|
+
* facade wraps the English-only default, which is why this can be required
|
|
79
|
+
* rather than optional.
|
|
80
|
+
*/
|
|
81
|
+
i18n: I18nProvider;
|
|
53
82
|
ui?: {
|
|
54
83
|
size?: string;
|
|
55
84
|
};
|
|
@@ -137,6 +166,23 @@ export interface ToolWindowShellConfig {
|
|
|
137
166
|
initialMargin?: number;
|
|
138
167
|
content?: ToolWindowShellContentConfig;
|
|
139
168
|
actions?: ToolWindowShellAction[];
|
|
169
|
+
/**
|
|
170
|
+
* Render the shell's own header controls as `<nds-icon-button>`s where the host
|
|
171
|
+
* enables NDS icons. Shells without this keep the plain `<button>` controls, and
|
|
172
|
+
* an opted-out host gets those either way.
|
|
173
|
+
*/
|
|
174
|
+
ndsHeaderControls?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Let Tab and Shift+Tab cross between the page and this shell rather than
|
|
177
|
+
* cycling inside it.
|
|
178
|
+
*
|
|
179
|
+
* The shell is appended to `<body>`, so it sits outside the document order its
|
|
180
|
+
* opener lives in. A shell a learner works *alongside* — reading the question
|
|
181
|
+
* while using the tool — needs Tab from the opener to enter it and Shift+Tab
|
|
182
|
+
* from the first question control to reach its end. A shell a learner works
|
|
183
|
+
* *inside* wants the default trap, which keeps focus captured until dismissed.
|
|
184
|
+
*/
|
|
185
|
+
pageTabOrder?: boolean;
|
|
140
186
|
}
|
|
141
187
|
export interface HostedToolContext {
|
|
142
188
|
toolId: string;
|
|
@@ -163,6 +209,19 @@ export interface ToolToolbarRenderResult {
|
|
|
163
209
|
sync?: () => void;
|
|
164
210
|
subscribeActive?: (callback: (active: boolean) => void) => () => void;
|
|
165
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* A registration's display name in the interface locale.
|
|
214
|
+
*
|
|
215
|
+
* Precedence: the resolved `nameKey`, then `name`. A key that does not resolve
|
|
216
|
+
* falls back to `name` rather than rendering the key, so a catalog gap degrades
|
|
217
|
+
* to English instead of to `tools.something.name` on a toolbar button.
|
|
218
|
+
*
|
|
219
|
+
* Both display resolvers live here so the toolbars, the settings panels and the
|
|
220
|
+
* PNP debugger cannot each invent their own precedence.
|
|
221
|
+
*/
|
|
222
|
+
export declare function resolveToolRegistrationName(registration: Pick<ToolRegistration, "name" | "nameKey">, i18n?: I18nProvider): string;
|
|
223
|
+
/** A registration's description in the interface locale. See the name resolver. */
|
|
224
|
+
export declare function resolveToolRegistrationDescription(registration: Pick<ToolRegistration, "description" | "descriptionKey">, i18n?: I18nProvider): string;
|
|
166
225
|
export type ToolActivation = "toolbar-toggle" | "selection-gateway" | "region";
|
|
167
226
|
export type ToolSingletonScope = "section";
|
|
168
227
|
/**
|
|
@@ -177,6 +236,13 @@ export interface ToolSurfaceServices {
|
|
|
177
236
|
toolkitCoordinator: ToolkitCoordinatorApi | null;
|
|
178
237
|
ttsService: TtsServiceApi | null;
|
|
179
238
|
catalogResolver: AccessibilityCatalogResolverApi | null;
|
|
239
|
+
/**
|
|
240
|
+
* Interface-locale provider for the surface's own labels — a region's
|
|
241
|
+
* `aria-label`, a heading it emits. Required, and always the facade
|
|
242
|
+
* `resolveInterfaceI18n` returns, which wraps the English-only default where
|
|
243
|
+
* the host published nothing.
|
|
244
|
+
*/
|
|
245
|
+
i18n: I18nProvider;
|
|
180
246
|
}
|
|
181
247
|
/**
|
|
182
248
|
* What a host tells a capability when asking it to fill a surface.
|
|
@@ -310,10 +376,28 @@ export interface ToolSurfaceRenderResult {
|
|
|
310
376
|
export interface ToolRegistration {
|
|
311
377
|
/** Unique tool identifier (e.g., 'calculator', 'textToSpeech') */
|
|
312
378
|
toolId: string;
|
|
313
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* Human-readable name.
|
|
381
|
+
*
|
|
382
|
+
* Required, and stays required: it is part of the contract a host implements
|
|
383
|
+
* when it contributes its own capability, and a host cannot be made to ship a
|
|
384
|
+
* message catalog. Treat it as the English source.
|
|
385
|
+
*/
|
|
314
386
|
name: string;
|
|
315
|
-
/** Description of what the tool does */
|
|
387
|
+
/** Description of what the tool does. English source, like {@link name}. */
|
|
316
388
|
description: string;
|
|
389
|
+
/**
|
|
390
|
+
* Message key resolving to {@link name} in the interface locale.
|
|
391
|
+
*
|
|
392
|
+
* Optional by design. A capability that supplies one is localizable; one that
|
|
393
|
+
* does not renders `name` verbatim, which is what keeps a host-authored
|
|
394
|
+
* registration working with no catalog. Resolve with
|
|
395
|
+
* {@link resolveToolRegistrationName} rather than reading either field
|
|
396
|
+
* directly, so precedence stays in one place.
|
|
397
|
+
*/
|
|
398
|
+
nameKey?: string;
|
|
399
|
+
/** Message key resolving to {@link description} in the interface locale. */
|
|
400
|
+
descriptionKey?: string;
|
|
317
401
|
/**
|
|
318
402
|
* Icon identifier or SVG string. Required for the activations that render a
|
|
319
403
|
* toolbar button; a region capability has no button, so it has no icon.
|
|
@@ -4,7 +4,37 @@
|
|
|
4
4
|
* Central registry for all assessment tools. Manages tool metadata, visibility logic,
|
|
5
5
|
* and button/instance creation. Supports dynamic registration and override by integrators.
|
|
6
6
|
*/
|
|
7
|
+
import { dynamicMessageKey } from "@pie-players/pie-players-shared/i18n/provider";
|
|
7
8
|
import { normalizeToolAlias } from "./tools-config-normalizer.js";
|
|
9
|
+
/**
|
|
10
|
+
* A registration's display name in the interface locale.
|
|
11
|
+
*
|
|
12
|
+
* Precedence: the resolved `nameKey`, then `name`. A key that does not resolve
|
|
13
|
+
* falls back to `name` rather than rendering the key, so a catalog gap degrades
|
|
14
|
+
* to English instead of to `tools.something.name` on a toolbar button.
|
|
15
|
+
*
|
|
16
|
+
* Both display resolvers live here so the toolbars, the settings panels and the
|
|
17
|
+
* PNP debugger cannot each invent their own precedence.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveToolRegistrationName(registration, i18n) {
|
|
20
|
+
return resolveKeyedString(registration.name, registration.nameKey, i18n);
|
|
21
|
+
}
|
|
22
|
+
/** A registration's description in the interface locale. See the name resolver. */
|
|
23
|
+
export function resolveToolRegistrationDescription(registration, i18n) {
|
|
24
|
+
return resolveKeyedString(registration.description, registration.descriptionKey, i18n);
|
|
25
|
+
}
|
|
26
|
+
function resolveKeyedString(source, key, i18n) {
|
|
27
|
+
if (!key || !i18n)
|
|
28
|
+
return source;
|
|
29
|
+
// A registration may be host-authored against a host catalog, so the key is
|
|
30
|
+
// not drawn from PIE's `MessageKey` union and has to be asserted.
|
|
31
|
+
const messageKey = dynamicMessageKey(key);
|
|
32
|
+
if (i18n.hasKey)
|
|
33
|
+
return i18n.hasKey(messageKey) ? i18n.t(messageKey) : source;
|
|
34
|
+
// A provider without `hasKey` still signals a miss by returning the key.
|
|
35
|
+
const resolved = i18n.t(messageKey);
|
|
36
|
+
return resolved === key ? source : resolved;
|
|
37
|
+
}
|
|
8
38
|
const VALID_TOOL_LEVELS = [
|
|
9
39
|
"assessment",
|
|
10
40
|
"section",
|
|
@@ -24,13 +24,14 @@ import { FrameworkErrorBus, type FrameworkErrorListener } from "./framework-erro
|
|
|
24
24
|
import { HighlightCoordinator } from "./HighlightCoordinator.js";
|
|
25
25
|
import { ToolCoordinator } from "./ToolCoordinator.js";
|
|
26
26
|
import { TTSService } from "./TTSService.js";
|
|
27
|
-
import { type
|
|
27
|
+
import { type TTSRuntimeSettings } from "./tts-runtime-config.js";
|
|
28
28
|
import type { SREMathSpeechOptions } from "./tts/math-speech.js";
|
|
29
29
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
30
30
|
import type { ToolProviderApi } from "./tool-providers/ToolProviderApi.js";
|
|
31
31
|
import type { TTSToolProviderConfig } from "./tool-providers/index.js";
|
|
32
32
|
import { ToolRegistry } from "./ToolRegistry.js";
|
|
33
33
|
import type { ResolvedToolContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap } from "./ToolRegistry.js";
|
|
34
|
+
import type { ToolOpenRequest, ToolRequestTarget } from "./tool-request.js";
|
|
34
35
|
import { type FeaturePolicyDecision, type PnpEnforcementMode, type PolicySource, type ResolvedEngineInputs, type ToolPolicyChangeListener, type ToolPolicyDecision, type ToolPolicyDecisionRequest } from "../policy/engine.js";
|
|
35
36
|
import type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionSessionPersistenceStrategy, SectionPersistenceFactoryDefaults } from "./section-controller-types.js";
|
|
36
37
|
export type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionControllerLoadedRenderable, SectionSessionPersistenceConfig, SectionSessionPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState, SectionPersistenceFactoryDefaults, } from "./section-controller-types.js";
|
|
@@ -45,45 +46,25 @@ export interface ToolConfig {
|
|
|
45
46
|
[key: string]: unknown;
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
|
-
* TTS
|
|
49
|
+
* TTS configuration as a host writes it.
|
|
50
|
+
*
|
|
51
|
+
* The field set is `TTSRuntimeSettings`, which the runtime resolver owns: the two
|
|
52
|
+
* were declared separately and had already drifted in both directions, so a field
|
|
53
|
+
* the runtime honoured could not be named here. What this adds is the two things
|
|
54
|
+
* only a host-facing config has — a place to stash unrecognised keys, and a
|
|
55
|
+
* callback for fetching provider credentials, neither of which the resolved
|
|
56
|
+
* runtime settings carry.
|
|
57
|
+
*
|
|
58
|
+
* An intersection rather than an interface: `ToolConfig.provider` is `unknown`
|
|
59
|
+
* where the runtime settings narrow it to the three provider ids, and an interface
|
|
60
|
+
* cannot inherit a member from two parents that type it differently.
|
|
49
61
|
*/
|
|
50
|
-
export
|
|
51
|
-
backend?: "browser" | "polly" | "google" | "server";
|
|
52
|
-
provider?: "polly" | "google" | "custom";
|
|
53
|
-
serverProvider?: "polly" | "google" | "custom";
|
|
54
|
-
engine?: "standard" | "neural";
|
|
55
|
-
sampleRate?: number;
|
|
56
|
-
format?: "mp3" | "ogg" | "pcm";
|
|
57
|
-
speechMarksMode?: "word" | "word+sentence";
|
|
58
|
-
defaultVoice?: string;
|
|
59
|
-
rate?: number;
|
|
60
|
-
pitch?: number;
|
|
61
|
-
apiEndpoint?: string;
|
|
62
|
-
language?: string;
|
|
63
|
-
transportMode?: "pie" | "custom";
|
|
64
|
-
endpointMode?: "synthesizePath" | "rootPost";
|
|
65
|
-
endpointValidationMode?: "voices" | "endpoint" | "none";
|
|
66
|
-
includeAuthOnAssetFetch?: boolean;
|
|
67
|
-
validateEndpoint?: boolean;
|
|
68
|
-
cache?: boolean;
|
|
69
|
-
speedRate?: "slow" | "medium" | "fast";
|
|
70
|
-
lang_id?: string;
|
|
71
|
-
/**
|
|
72
|
-
* Optional inline TTS speed buttons.
|
|
73
|
-
* - Omitted/non-array: default speed buttons are shown.
|
|
74
|
-
* - Empty array: hide speed buttons.
|
|
75
|
-
* - Arrays that sanitize to no valid values: default speed buttons are shown.
|
|
76
|
-
* - Object entries can customize button text while preserving numeric rates.
|
|
77
|
-
*/
|
|
78
|
-
speedOptions?: TTSSpeedOption[];
|
|
79
|
-
layoutMode?: TTSLayoutMode;
|
|
80
|
-
/** Speech Rule Engine options for generated MathML speech. */
|
|
81
|
-
mathSpeech?: SREMathSpeechOptions;
|
|
62
|
+
export type TTSToolConfig = ToolConfig & TTSRuntimeSettings & {
|
|
82
63
|
settings?: Record<string, unknown> & {
|
|
83
64
|
mathSpeech?: SREMathSpeechOptions;
|
|
84
65
|
};
|
|
85
66
|
authFetcher?: () => Promise<Partial<TTSToolProviderConfig>>;
|
|
86
|
-
}
|
|
67
|
+
};
|
|
87
68
|
/**
|
|
88
69
|
* Answer eliminator tool configuration
|
|
89
70
|
*/
|
|
@@ -205,8 +186,8 @@ export interface SectionEventSubscriptionArgs {
|
|
|
205
186
|
eventTypes?: readonly SectionControllerEventType[];
|
|
206
187
|
itemIds?: readonly string[];
|
|
207
188
|
}
|
|
208
|
-
export type SectionItemEventType = Exclude<SectionControllerEventType, "section-navigation-change" | "section-session-applied" | "section-loading-complete" | "section-items-complete-changed" | "section-error">;
|
|
209
|
-
export type SectionScopedEventType = Extract<SectionControllerEventType, "section-navigation-change" | "section-session-applied" | "section-loading-complete" | "section-items-complete-changed" | "section-error">;
|
|
189
|
+
export type SectionItemEventType = Exclude<SectionControllerEventType, "section-navigation-change" | "section-session-applied" | "section-loading-complete" | "section-items-complete-changed" | "section-error" | "timed-media-cue-changed" | "timed-media-policy-degraded" | "timed-media-invalid">;
|
|
190
|
+
export type SectionScopedEventType = Extract<SectionControllerEventType, "section-navigation-change" | "section-session-applied" | "section-loading-complete" | "section-items-complete-changed" | "section-error" | "timed-media-cue-changed" | "timed-media-policy-degraded" | "timed-media-invalid">;
|
|
210
191
|
export type SectionItemEvent = Extract<SectionControllerEvent, {
|
|
211
192
|
type: SectionItemEventType;
|
|
212
193
|
}>;
|
|
@@ -339,6 +320,7 @@ export declare class ToolkitCoordinator {
|
|
|
339
320
|
private readonly toolRegistry;
|
|
340
321
|
private readonly toolContextResolvers;
|
|
341
322
|
private readonly toolContextResolverChangeListeners;
|
|
323
|
+
private readonly toolRequests;
|
|
342
324
|
private readonly sectionControllers;
|
|
343
325
|
private readonly sectionControllerKeys;
|
|
344
326
|
private readonly sectionControllerInitPromises;
|
|
@@ -778,6 +760,26 @@ export declare class ToolkitCoordinator {
|
|
|
778
760
|
hasToolContextResolver(toolId: string): boolean;
|
|
779
761
|
resolveToolContext(context: ToolContextResolverContext): ResolvedToolContext | null;
|
|
780
762
|
onToolContextResolverChange(listener: () => void): () => void;
|
|
763
|
+
/**
|
|
764
|
+
* Claim requests for one placement level. Called by a toolbar on mount.
|
|
765
|
+
*/
|
|
766
|
+
registerToolRequestTarget(target: ToolRequestTarget): () => void;
|
|
767
|
+
/**
|
|
768
|
+
* Ask the toolbar hosting a tool to open it, handing it `params`.
|
|
769
|
+
*
|
|
770
|
+
* Returns whether a toolbar claimed the request. A surface offering this as an
|
|
771
|
+
* affordance should gate on {@link canRequestTool} first rather than acting on
|
|
772
|
+
* the return value, so the learner never sees a control that does nothing.
|
|
773
|
+
*/
|
|
774
|
+
requestTool(request: ToolOpenRequest): boolean;
|
|
775
|
+
/**
|
|
776
|
+
* Answers `false` for a tool this deployment does not carry rather than
|
|
777
|
+
* throwing as {@link requestTool} does. A composer asks this while rendering a
|
|
778
|
+
* surface, and a host that swapped the registry for one without the tool would
|
|
779
|
+
* otherwise lose the whole surface to an exception over an absent action.
|
|
780
|
+
*/
|
|
781
|
+
canRequestTool(toolId: string, level?: ToolOpenRequest["level"]): boolean;
|
|
782
|
+
onToolRequestTargetsChange(listener: () => void): () => void;
|
|
781
783
|
/**
|
|
782
784
|
* Compute the effective PNP/profile enforcement mode given the explicit
|
|
783
785
|
* host override and the auto-mode helper.
|
|
@@ -26,6 +26,7 @@ import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
|
26
26
|
import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "./tts-runtime-config.js";
|
|
27
27
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
28
28
|
import { ToolRegistry } from "./ToolRegistry.js";
|
|
29
|
+
import { ToolRequestRegistry } from "./tool-request.js";
|
|
29
30
|
import { ToolPolicyEngine, } from "../policy/engine.js";
|
|
30
31
|
import { resolveDefaultPnpEnforcement } from "../policy/internal.js";
|
|
31
32
|
const isPlainRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -59,6 +60,9 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
59
60
|
"section-loading-complete",
|
|
60
61
|
"section-items-complete-changed",
|
|
61
62
|
"section-error",
|
|
63
|
+
"timed-media-cue-changed",
|
|
64
|
+
"timed-media-policy-degraded",
|
|
65
|
+
"timed-media-invalid",
|
|
62
66
|
];
|
|
63
67
|
/**
|
|
64
68
|
* ToolkitCoordinator - Orchestrates all assessment toolkit services
|
|
@@ -113,6 +117,7 @@ export class ToolkitCoordinator {
|
|
|
113
117
|
toolRegistry;
|
|
114
118
|
toolContextResolvers = new Map();
|
|
115
119
|
toolContextResolverChangeListeners = new Set();
|
|
120
|
+
toolRequests = new ToolRequestRegistry();
|
|
116
121
|
sectionControllers = new Map();
|
|
117
122
|
sectionControllerKeys = new Map();
|
|
118
123
|
sectionControllerInitPromises = new Map();
|
|
@@ -1741,6 +1746,41 @@ export class ToolkitCoordinator {
|
|
|
1741
1746
|
this.toolContextResolverChangeListeners.delete(listener);
|
|
1742
1747
|
};
|
|
1743
1748
|
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Claim requests for one placement level. Called by a toolbar on mount.
|
|
1751
|
+
*/
|
|
1752
|
+
registerToolRequestTarget(target) {
|
|
1753
|
+
return this.toolRequests.registerTarget(target);
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Ask the toolbar hosting a tool to open it, handing it `params`.
|
|
1757
|
+
*
|
|
1758
|
+
* Returns whether a toolbar claimed the request. A surface offering this as an
|
|
1759
|
+
* affordance should gate on {@link canRequestTool} first rather than acting on
|
|
1760
|
+
* the return value, so the learner never sees a control that does nothing.
|
|
1761
|
+
*/
|
|
1762
|
+
requestTool(request) {
|
|
1763
|
+
this.assertCanonicalToolId(request.toolId);
|
|
1764
|
+
return this.toolRequests.request(request);
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Answers `false` for a tool this deployment does not carry rather than
|
|
1768
|
+
* throwing as {@link requestTool} does. A composer asks this while rendering a
|
|
1769
|
+
* surface, and a host that swapped the registry for one without the tool would
|
|
1770
|
+
* otherwise lose the whole surface to an exception over an absent action.
|
|
1771
|
+
*/
|
|
1772
|
+
canRequestTool(toolId, level) {
|
|
1773
|
+
try {
|
|
1774
|
+
this.assertCanonicalToolId(toolId);
|
|
1775
|
+
}
|
|
1776
|
+
catch {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
return this.toolRequests.canRequest(toolId, level);
|
|
1780
|
+
}
|
|
1781
|
+
onToolRequestTargetsChange(listener) {
|
|
1782
|
+
return this.toolRequests.onTargetsChange(listener);
|
|
1783
|
+
}
|
|
1744
1784
|
/**
|
|
1745
1785
|
* Compute the effective PNP/profile enforcement mode given the explicit
|
|
1746
1786
|
* host override and the auto-mode helper.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio handoff between read-aloud and a media surface.
|
|
3
|
+
*
|
|
4
|
+
* One learner, one pair of ears: read-aloud and media audio must never run at
|
|
5
|
+
* once, and the action the learner just took wins. Every media surface in the
|
|
6
|
+
* toolkit's graph needs both halves of that rule — pause yourself when speech
|
|
7
|
+
* starts, pause speech when you start — and there are already two such surfaces
|
|
8
|
+
* (the signing region, a timed-media stimulus reached through its port) with
|
|
9
|
+
* nothing in common but this. Shared so the rule has one statement: which states
|
|
10
|
+
* count as speaking is the part that would drift.
|
|
11
|
+
*
|
|
12
|
+
* Neither half resumes what it silenced. The learner presses play.
|
|
13
|
+
*/
|
|
14
|
+
import type { TtsServiceApi } from "./interfaces.js";
|
|
15
|
+
/** Only what a handoff needs, so a host passing a partial service still works. */
|
|
16
|
+
type TtsHandoffSubscriber = Pick<TtsServiceApi, "onStateChange" | "offStateChange">;
|
|
17
|
+
type TtsHandoffPlayback = Pick<TtsServiceApi, "isPlaying" | "pause">;
|
|
18
|
+
/**
|
|
19
|
+
* Silence a media surface whenever read-aloud starts speaking.
|
|
20
|
+
*
|
|
21
|
+
* `LOADING` as well as `PLAYING`, so the pause lands before the first word rather
|
|
22
|
+
* than a provider round-trip after it. A `LOADING` that then fails leaves media
|
|
23
|
+
* paused, which costs the learner one press of play.
|
|
24
|
+
*
|
|
25
|
+
* Returns the teardown, including where there was nothing to bind.
|
|
26
|
+
*/
|
|
27
|
+
export declare function bindTtsAudioHandoff(args: {
|
|
28
|
+
ttsService: Partial<TtsHandoffSubscriber> | null | undefined;
|
|
29
|
+
/** Unique per surface: the service keys its listener sets by this. */
|
|
30
|
+
listenerId: string;
|
|
31
|
+
silence: () => void;
|
|
32
|
+
}): () => void;
|
|
33
|
+
/**
|
|
34
|
+
* The other half: media audio has started, so read-aloud yields.
|
|
35
|
+
*
|
|
36
|
+
* Paused rather than stopped, so the learner keeps their place in the passage.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pauseTtsForMediaAudio(ttsService: Partial<TtsHandoffPlayback> | null | undefined): void;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio handoff between read-aloud and a media surface.
|
|
3
|
+
*
|
|
4
|
+
* One learner, one pair of ears: read-aloud and media audio must never run at
|
|
5
|
+
* once, and the action the learner just took wins. Every media surface in the
|
|
6
|
+
* toolkit's graph needs both halves of that rule — pause yourself when speech
|
|
7
|
+
* starts, pause speech when you start — and there are already two such surfaces
|
|
8
|
+
* (the signing region, a timed-media stimulus reached through its port) with
|
|
9
|
+
* nothing in common but this. Shared so the rule has one statement: which states
|
|
10
|
+
* count as speaking is the part that would drift.
|
|
11
|
+
*
|
|
12
|
+
* Neither half resumes what it silenced. The learner presses play.
|
|
13
|
+
*/
|
|
14
|
+
import { PlaybackState } from "./TTSService.js";
|
|
15
|
+
const NOOP = () => { };
|
|
16
|
+
/**
|
|
17
|
+
* Silence a media surface whenever read-aloud starts speaking.
|
|
18
|
+
*
|
|
19
|
+
* `LOADING` as well as `PLAYING`, so the pause lands before the first word rather
|
|
20
|
+
* than a provider round-trip after it. A `LOADING` that then fails leaves media
|
|
21
|
+
* paused, which costs the learner one press of play.
|
|
22
|
+
*
|
|
23
|
+
* Returns the teardown, including where there was nothing to bind.
|
|
24
|
+
*/
|
|
25
|
+
export function bindTtsAudioHandoff(args) {
|
|
26
|
+
const { ttsService, listenerId, silence } = args;
|
|
27
|
+
if (typeof ttsService?.onStateChange !== "function")
|
|
28
|
+
return NOOP;
|
|
29
|
+
const onTtsState = (state) => {
|
|
30
|
+
if (state !== PlaybackState.PLAYING && state !== PlaybackState.LOADING) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
silence();
|
|
34
|
+
};
|
|
35
|
+
ttsService.onStateChange(listenerId, onTtsState);
|
|
36
|
+
return () => {
|
|
37
|
+
try {
|
|
38
|
+
ttsService.offStateChange?.(listenerId, onTtsState);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// A torn-down service is not a failure to detach from.
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The other half: media audio has started, so read-aloud yields.
|
|
47
|
+
*
|
|
48
|
+
* Paused rather than stopped, so the learner keeps their place in the passage.
|
|
49
|
+
*/
|
|
50
|
+
export function pauseTtsForMediaAudio(ttsService) {
|
|
51
|
+
try {
|
|
52
|
+
if (ttsService?.isPlaying?.())
|
|
53
|
+
ttsService.pause?.();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// A torn-down or uninitialized TTS service must not break playback.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -12,14 +12,45 @@
|
|
|
12
12
|
* Part of PIE Assessment Toolkit.
|
|
13
13
|
*/
|
|
14
14
|
import type { MediaFragmentRange, MediaSource } from "@pie-players/pie-players-shared/types";
|
|
15
|
+
/**
|
|
16
|
+
* The only `MediaAssetRef.version` this build renders.
|
|
17
|
+
*
|
|
18
|
+
* `media-asset-contract.md` requires unknown-version rejection for runtime
|
|
19
|
+
* rendering: bumping to `2` obliges every consumer to accept both for as long as
|
|
20
|
+
* any producer emits `1`, so a card claiming a version this build does not
|
|
21
|
+
* implement must not be rendered on a guess at which fields still mean what they
|
|
22
|
+
* did.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SUPPORTED_MEDIA_ASSET_VERSION = 1;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a media reference claims a version this build cannot render.
|
|
27
|
+
*
|
|
28
|
+
* An absent `version` is accepted rather than rejected — the same posture the
|
|
29
|
+
* rest of this module takes toward absent fields, and the one `media.kind` and
|
|
30
|
+
* `matchesRequestedSignLanguage` already take: only a positive claim of
|
|
31
|
+
* something else is refused. Producers predate the field, and treating its
|
|
32
|
+
* absence as a rejection would drop cards that are otherwise wholly valid.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isUnsupportedMediaAssetVersion(version: unknown): boolean;
|
|
15
35
|
export declare function isSafeMediaSrc(raw: unknown): raw is string;
|
|
16
36
|
export declare function normalizeMediaSources(raw: unknown): MediaSource[];
|
|
17
37
|
export declare function normalizeMediaFragment(raw: unknown): MediaFragmentRange | undefined;
|
|
18
38
|
export declare function trimmedOrUndefined(value: unknown): string | undefined;
|
|
19
39
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
40
|
+
* Hold a media element to one fragment's range, and return the disposer.
|
|
41
|
+
*
|
|
42
|
+
* The `#t=` URI `applyMediaFragment` writes is a hint browsers honour at neither
|
|
43
|
+
* bound reliably, so every consumer that means "play only this slice" enforces it
|
|
44
|
+
* here instead of reimplementing the pair. Both shipped consumers do —
|
|
45
|
+
* `SignLanguageMediaRegion` pausing at the end, `TTSService.playRecordedAudio`
|
|
46
|
+
* ending the clip so the chunk sequence advances — which is why the end action is
|
|
47
|
+
* the caller's and only the arithmetic is shared.
|
|
48
|
+
*
|
|
49
|
+
* The start seek is forward only: a browser that did honour the URI has already
|
|
50
|
+
* positioned past the start, and seeking back would replay audio the learner has
|
|
51
|
+
* heard. `onReachedEnd` may fire more than once and must tolerate it; polling
|
|
52
|
+
* stops while the element is paused, so an element left sitting past its end bound
|
|
53
|
+
* goes quiet rather than being told repeatedly.
|
|
24
54
|
*/
|
|
55
|
+
export declare function enforceMediaFragment(element: HTMLMediaElement, fragment: MediaFragmentRange | undefined, onReachedEnd: () => void): () => void;
|
|
25
56
|
export declare function applyMediaFragment(src: string, fragment?: MediaFragmentRange): string;
|
|
@@ -20,6 +20,28 @@
|
|
|
20
20
|
*/
|
|
21
21
|
const DISALLOWED_SRC_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
22
22
|
const ALLOWED_SRC_SCHEMES = new Set(["http:", "https:", "data:", "blob:"]);
|
|
23
|
+
/**
|
|
24
|
+
* The only `MediaAssetRef.version` this build renders.
|
|
25
|
+
*
|
|
26
|
+
* `media-asset-contract.md` requires unknown-version rejection for runtime
|
|
27
|
+
* rendering: bumping to `2` obliges every consumer to accept both for as long as
|
|
28
|
+
* any producer emits `1`, so a card claiming a version this build does not
|
|
29
|
+
* implement must not be rendered on a guess at which fields still mean what they
|
|
30
|
+
* did.
|
|
31
|
+
*/
|
|
32
|
+
export const SUPPORTED_MEDIA_ASSET_VERSION = 1;
|
|
33
|
+
/**
|
|
34
|
+
* Whether a media reference claims a version this build cannot render.
|
|
35
|
+
*
|
|
36
|
+
* An absent `version` is accepted rather than rejected — the same posture the
|
|
37
|
+
* rest of this module takes toward absent fields, and the one `media.kind` and
|
|
38
|
+
* `matchesRequestedSignLanguage` already take: only a positive claim of
|
|
39
|
+
* something else is refused. Producers predate the field, and treating its
|
|
40
|
+
* absence as a rejection would drop cards that are otherwise wholly valid.
|
|
41
|
+
*/
|
|
42
|
+
export function isUnsupportedMediaAssetVersion(version) {
|
|
43
|
+
return version !== undefined && version !== SUPPORTED_MEDIA_ASSET_VERSION;
|
|
44
|
+
}
|
|
23
45
|
export function isSafeMediaSrc(raw) {
|
|
24
46
|
if (typeof raw !== "string")
|
|
25
47
|
return false;
|
|
@@ -85,10 +107,77 @@ export function trimmedOrUndefined(value) {
|
|
|
85
107
|
}
|
|
86
108
|
/**
|
|
87
109
|
* Apply a fragment range to a source URL as a Media Fragments URI, so one
|
|
88
|
-
* recording can serve several content nodes.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
110
|
+
* recording can serve several content nodes.
|
|
111
|
+
*
|
|
112
|
+
* The URI is a hint only: browsers honour both bounds inconsistently, so the
|
|
113
|
+
* caller enforces the range itself — seek forward to the start once metadata is
|
|
114
|
+
* available, and stop at the end. `SignLanguageMediaRegion` and
|
|
115
|
+
* `TTSService.playRecordedAudio` are the two shipped consumers that do so.
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* How often the end bound is re-checked while a slice is playing. `timeupdate`
|
|
119
|
+
* alone fires about four times a second, which is loose enough to leak a sliver
|
|
120
|
+
* of the next node's recording.
|
|
121
|
+
*/
|
|
122
|
+
const END_CHECK_INTERVAL_MS = 100;
|
|
123
|
+
/** `HTMLMediaElement.HAVE_METADATA`, which not every DOM implementation exposes. */
|
|
124
|
+
const HAVE_METADATA = 1;
|
|
125
|
+
/**
|
|
126
|
+
* Hold a media element to one fragment's range, and return the disposer.
|
|
127
|
+
*
|
|
128
|
+
* The `#t=` URI `applyMediaFragment` writes is a hint browsers honour at neither
|
|
129
|
+
* bound reliably, so every consumer that means "play only this slice" enforces it
|
|
130
|
+
* here instead of reimplementing the pair. Both shipped consumers do —
|
|
131
|
+
* `SignLanguageMediaRegion` pausing at the end, `TTSService.playRecordedAudio`
|
|
132
|
+
* ending the clip so the chunk sequence advances — which is why the end action is
|
|
133
|
+
* the caller's and only the arithmetic is shared.
|
|
134
|
+
*
|
|
135
|
+
* The start seek is forward only: a browser that did honour the URI has already
|
|
136
|
+
* positioned past the start, and seeking back would replay audio the learner has
|
|
137
|
+
* heard. `onReachedEnd` may fire more than once and must tolerate it; polling
|
|
138
|
+
* stops while the element is paused, so an element left sitting past its end bound
|
|
139
|
+
* goes quiet rather than being told repeatedly.
|
|
91
140
|
*/
|
|
141
|
+
export function enforceMediaFragment(element, fragment, onReachedEnd) {
|
|
142
|
+
if (!fragment)
|
|
143
|
+
return () => { };
|
|
144
|
+
const { startSeconds, endSeconds } = fragment;
|
|
145
|
+
let seekToStart;
|
|
146
|
+
if (startSeconds > 0) {
|
|
147
|
+
seekToStart = () => {
|
|
148
|
+
if (element.currentTime < startSeconds) {
|
|
149
|
+
element.currentTime = startSeconds;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
// Seeking before metadata is available only sets the default start
|
|
153
|
+
// position, so wait for it unless it already landed.
|
|
154
|
+
if (element.readyState >= HAVE_METADATA)
|
|
155
|
+
seekToStart();
|
|
156
|
+
else
|
|
157
|
+
element.addEventListener("loadedmetadata", seekToStart);
|
|
158
|
+
}
|
|
159
|
+
let checkEnd;
|
|
160
|
+
let endPoll;
|
|
161
|
+
if (endSeconds !== undefined) {
|
|
162
|
+
checkEnd = () => {
|
|
163
|
+
if (element.currentTime >= endSeconds)
|
|
164
|
+
onReachedEnd();
|
|
165
|
+
};
|
|
166
|
+
element.addEventListener("timeupdate", checkEnd);
|
|
167
|
+
endPoll = setInterval(() => {
|
|
168
|
+
if (!element.paused)
|
|
169
|
+
checkEnd?.();
|
|
170
|
+
}, END_CHECK_INTERVAL_MS);
|
|
171
|
+
}
|
|
172
|
+
return () => {
|
|
173
|
+
if (seekToStart)
|
|
174
|
+
element.removeEventListener("loadedmetadata", seekToStart);
|
|
175
|
+
if (checkEnd)
|
|
176
|
+
element.removeEventListener("timeupdate", checkEnd);
|
|
177
|
+
if (endPoll !== undefined)
|
|
178
|
+
clearInterval(endPoll);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
92
181
|
export function applyMediaFragment(src, fragment) {
|
|
93
182
|
if (!fragment)
|
|
94
183
|
return src;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { ToolConfigDiagnostic } from "./tool-config-validation.js";
|
|
2
|
-
export type FrameworkErrorKind = "tool-config" | "runtime-init" | "runtime-dispose" | "coordinator-init" | "provider-init" | "provider-register" | "tts-init" | "tool-state-load" | "tool-state-save" | "section-controller-init" | "section-controller-dispose" | "tool-surface"
|
|
2
|
+
export type FrameworkErrorKind = "tool-config" | "runtime-init" | "runtime-dispose" | "coordinator-init" | "provider-init" | "provider-register" | "tts-init" | "tool-state-load" | "tool-state-save" | "section-controller-init" | "section-controller-dispose" | "tool-surface"
|
|
3
|
+
/**
|
|
4
|
+
* A timed-media section could not deliver a policy as authored: a media time
|
|
5
|
+
* source missing `canPause` / `canRestrictSeeking` (recoverable — cues still
|
|
6
|
+
* fire and state is still recorded, only enforcement is lost), or authored
|
|
7
|
+
* `timedMedia` that failed validation (not recoverable — the section delivers
|
|
8
|
+
* as an ordinary section instead).
|
|
9
|
+
*/
|
|
10
|
+
| "timed-media"
|
|
11
|
+
/**
|
|
12
|
+
* An interface-locale catalog failed to load. Recoverable by construction: every
|
|
13
|
+
* key still resolves through the English fallback chain, so the player renders
|
|
14
|
+
* — in the wrong language, which a host wants to know about.
|
|
15
|
+
*/
|
|
16
|
+
| "i18n-locale-load" | "unknown";
|
|
3
17
|
export type FrameworkErrorSeverity = "warning" | "error";
|
|
4
18
|
export interface FrameworkErrorModel {
|
|
5
19
|
kind: FrameworkErrorKind;
|