@pie-players/pie-assessment-toolkit 0.2.7 → 0.2.9

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 (220) hide show
  1. package/README.md +200 -67
  2. package/dist/attempt/TestSession.d.ts +23 -21
  3. package/dist/attempt/TestSession.d.ts.map +1 -1
  4. package/dist/attempt/TestSession.js +17 -15
  5. package/dist/attempt/TestSession.js.map +1 -1
  6. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +50 -0
  7. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts.map +1 -0
  8. package/dist/attempt/adapters/activity-to-test-attempt-session.js +135 -0
  9. package/dist/attempt/adapters/activity-to-test-attempt-session.js.map +1 -0
  10. package/dist/components/ItemToolBar.custom-element.js +628 -0
  11. package/dist/components/PieAssessmentToolkit.custom-element.js +670 -0
  12. package/dist/components/item-toolbar-element.d.ts +3 -0
  13. package/dist/components/item-toolbar-element.d.ts.map +1 -0
  14. package/dist/components/item-toolbar-element.js +2 -0
  15. package/dist/components/item-toolbar-element.js.map +1 -0
  16. package/dist/components/pie-assessment-toolkit-element.d.ts +3 -0
  17. package/dist/components/pie-assessment-toolkit-element.d.ts.map +1 -0
  18. package/dist/components/pie-assessment-toolkit-element.js +2 -0
  19. package/dist/components/pie-assessment-toolkit-element.js.map +1 -0
  20. package/dist/context/assessment-toolkit-context.d.ts +52 -0
  21. package/dist/context/assessment-toolkit-context.d.ts.map +1 -0
  22. package/dist/context/assessment-toolkit-context.js +6 -0
  23. package/dist/context/assessment-toolkit-context.js.map +1 -0
  24. package/dist/context/runtime-context-consumer.d.ts +14 -0
  25. package/dist/context/runtime-context-consumer.d.ts.map +1 -0
  26. package/dist/context/runtime-context-consumer.js +52 -0
  27. package/dist/context/runtime-context-consumer.js.map +1 -0
  28. package/dist/index.d.ts +16 -5
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +13 -11
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/RuntimeRegistry.d.ts +13 -0
  33. package/dist/runtime/RuntimeRegistry.d.ts.map +1 -0
  34. package/dist/runtime/RuntimeRegistry.js +50 -0
  35. package/dist/runtime/RuntimeRegistry.js.map +1 -0
  36. package/dist/runtime/SectionRuntimeEngine.d.ts +45 -0
  37. package/dist/runtime/SectionRuntimeEngine.d.ts.map +1 -0
  38. package/dist/runtime/SectionRuntimeEngine.js +81 -0
  39. package/dist/runtime/SectionRuntimeEngine.js.map +1 -0
  40. package/dist/runtime/registration-events.d.ts +19 -0
  41. package/dist/runtime/registration-events.d.ts.map +1 -0
  42. package/dist/runtime/registration-events.js +4 -0
  43. package/dist/runtime/registration-events.js.map +1 -0
  44. package/dist/runtime/runtime-event-guards.d.ts +3 -0
  45. package/dist/runtime/runtime-event-guards.d.ts.map +1 -0
  46. package/dist/runtime/runtime-event-guards.js +9 -0
  47. package/dist/runtime/runtime-event-guards.js.map +1 -0
  48. package/dist/runtime/tool-host-contract.d.ts +30 -0
  49. package/dist/runtime/tool-host-contract.d.ts.map +1 -0
  50. package/dist/runtime/tool-host-contract.js +48 -0
  51. package/dist/runtime/tool-host-contract.js.map +1 -0
  52. package/dist/services/TTSService.d.ts +3 -1
  53. package/dist/services/TTSService.d.ts.map +1 -1
  54. package/dist/services/TTSService.js +13 -12
  55. package/dist/services/TTSService.js.map +1 -1
  56. package/dist/services/ThemeProvider.d.ts +1 -0
  57. package/dist/services/ThemeProvider.d.ts.map +1 -1
  58. package/dist/services/ThemeProvider.js +82 -46
  59. package/dist/services/ThemeProvider.js.map +1 -1
  60. package/dist/services/ToolConfigResolver.d.ts +1 -1
  61. package/dist/services/ToolConfigResolver.js +1 -1
  62. package/dist/services/ToolCoordinator.d.ts +1 -1
  63. package/dist/services/ToolCoordinator.d.ts.map +1 -1
  64. package/dist/services/ToolRegistry.d.ts +64 -67
  65. package/dist/services/ToolRegistry.d.ts.map +1 -1
  66. package/dist/services/ToolRegistry.js +72 -22
  67. package/dist/services/ToolRegistry.js.map +1 -1
  68. package/dist/services/ToolkitCoordinator.d.ts +106 -8
  69. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  70. package/dist/services/ToolkitCoordinator.js +502 -71
  71. package/dist/services/ToolkitCoordinator.js.map +1 -1
  72. package/dist/services/createDefaultToolRegistry.d.ts +12 -7
  73. package/dist/services/createDefaultToolRegistry.d.ts.map +1 -1
  74. package/dist/services/createDefaultToolRegistry.js +6 -10
  75. package/dist/services/createDefaultToolRegistry.js.map +1 -1
  76. package/dist/services/defaultPersonalNeedsProfile.d.ts +4 -0
  77. package/dist/services/defaultPersonalNeedsProfile.d.ts.map +1 -0
  78. package/dist/services/defaultPersonalNeedsProfile.js +25 -0
  79. package/dist/services/defaultPersonalNeedsProfile.js.map +1 -0
  80. package/dist/services/interfaces.d.ts +61 -1
  81. package/dist/services/interfaces.d.ts.map +1 -1
  82. package/dist/services/interfaces.js.map +1 -1
  83. package/dist/services/pnp-standard-features.d.ts +3 -4
  84. package/dist/services/pnp-standard-features.d.ts.map +1 -1
  85. package/dist/services/pnp-standard-features.js +0 -3
  86. package/dist/services/pnp-standard-features.js.map +1 -1
  87. package/dist/services/section-controller-types.d.ts +32 -0
  88. package/dist/services/section-controller-types.d.ts.map +1 -0
  89. package/dist/services/section-controller-types.js +2 -0
  90. package/dist/services/section-controller-types.js.map +1 -0
  91. package/dist/services/tool-context.d.ts +4 -4
  92. package/dist/services/tool-context.d.ts.map +1 -1
  93. package/dist/services/tool-context.js +106 -17
  94. package/dist/services/tool-context.js.map +1 -1
  95. package/dist/services/tool-instance-id.d.ts +17 -0
  96. package/dist/services/tool-instance-id.d.ts.map +1 -0
  97. package/dist/services/tool-instance-id.js +61 -0
  98. package/dist/services/tool-instance-id.js.map +1 -0
  99. package/dist/services/tool-providers/IToolProvider.d.ts +2 -3
  100. package/dist/services/tool-providers/IToolProvider.d.ts.map +1 -1
  101. package/dist/services/tool-providers/IToolProvider.js +1 -2
  102. package/dist/services/tool-providers/IToolProvider.js.map +1 -1
  103. package/dist/services/tool-providers/MathJsToolProvider.d.ts +25 -0
  104. package/dist/services/tool-providers/MathJsToolProvider.d.ts.map +1 -0
  105. package/dist/services/tool-providers/MathJsToolProvider.js +53 -0
  106. package/dist/services/tool-providers/MathJsToolProvider.js.map +1 -0
  107. package/dist/services/tool-providers/index.d.ts +2 -0
  108. package/dist/services/tool-providers/index.d.ts.map +1 -1
  109. package/dist/services/tool-providers/index.js +1 -0
  110. package/dist/services/tool-providers/index.js.map +1 -1
  111. package/dist/services/tools-config-normalizer.d.ts +26 -0
  112. package/dist/services/tools-config-normalizer.d.ts.map +1 -0
  113. package/dist/services/tools-config-normalizer.js +63 -0
  114. package/dist/services/tools-config-normalizer.js.map +1 -0
  115. package/dist/tools/calculators/ti-provider.d.ts.map +1 -1
  116. package/dist/tools/calculators/ti-provider.js +127 -4
  117. package/dist/tools/calculators/ti-provider.js.map +1 -1
  118. package/dist/tools/client.d.ts +2 -1
  119. package/dist/tools/client.d.ts.map +1 -1
  120. package/dist/tools/client.js +2 -1
  121. package/dist/tools/client.js.map +1 -1
  122. package/dist/tools/default-tool-module-loaders.d.ts +9 -0
  123. package/dist/tools/default-tool-module-loaders.d.ts.map +1 -0
  124. package/dist/tools/default-tool-module-loaders.js +17 -0
  125. package/dist/tools/default-tool-module-loaders.js.map +1 -0
  126. package/dist/tools/index.d.ts +2 -1
  127. package/dist/tools/index.d.ts.map +1 -1
  128. package/dist/tools/index.js +2 -1
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/tools/library-loader.d.ts +1 -1
  131. package/dist/tools/library-loader.d.ts.map +1 -1
  132. package/dist/tools/library-loader.js +7 -2
  133. package/dist/tools/library-loader.js.map +1 -1
  134. package/dist/tools/registrations/accessibility-tools.d.ts +1 -9
  135. package/dist/tools/registrations/accessibility-tools.d.ts.map +1 -1
  136. package/dist/tools/registrations/accessibility-tools.js +95 -113
  137. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  138. package/dist/tools/registrations/calculator.d.ts +2 -2
  139. package/dist/tools/registrations/calculator.d.ts.map +1 -1
  140. package/dist/tools/registrations/calculator.js +34 -55
  141. package/dist/tools/registrations/calculator.js.map +1 -1
  142. package/dist/tools/registrations/interaction-tools.d.ts.map +1 -1
  143. package/dist/tools/registrations/interaction-tools.js +77 -52
  144. package/dist/tools/registrations/interaction-tools.js.map +1 -1
  145. package/dist/tools/registrations/measurement-tools.d.ts.map +1 -1
  146. package/dist/tools/registrations/measurement-tools.js +63 -38
  147. package/dist/tools/registrations/measurement-tools.js.map +1 -1
  148. package/dist/tools/registrations/subject-specific-tools.d.ts.map +1 -1
  149. package/dist/tools/registrations/subject-specific-tools.js +63 -38
  150. package/dist/tools/registrations/subject-specific-tools.js.map +1 -1
  151. package/dist/tools/registrations/tts.d.ts +1 -1
  152. package/dist/tools/registrations/tts.d.ts.map +1 -1
  153. package/dist/tools/registrations/tts.js +27 -41
  154. package/dist/tools/registrations/tts.js.map +1 -1
  155. package/dist/tools/response-discovery.d.ts +12 -14
  156. package/dist/tools/response-discovery.d.ts.map +1 -1
  157. package/dist/tools/response-discovery.js +23 -80
  158. package/dist/tools/response-discovery.js.map +1 -1
  159. package/dist/tools/tool-tag-map.d.ts +3 -3
  160. package/dist/tools/tool-tag-map.d.ts.map +1 -1
  161. package/dist/tools/tool-tag-map.js +2 -3
  162. package/dist/tools/tool-tag-map.js.map +1 -1
  163. package/dist/tools/types.d.ts +3 -1
  164. package/dist/tools/types.d.ts.map +1 -1
  165. package/package.json +25 -28
  166. package/src/README.md +16 -32
  167. package/src/components/ItemToolBar.svelte +489 -0
  168. package/src/components/PNPProvenanceViewer.svelte +14 -4
  169. package/src/components/PieAssessmentToolkit.svelte +517 -0
  170. package/src/components/ToolButton.svelte +11 -11
  171. package/src/components/ToolButtonGroup.svelte +27 -11
  172. package/src/tools/README.md +6 -5
  173. package/src/tools/calculators/README.md +4 -4
  174. package/dist/player/AssessmentPlayer.d.ts +0 -397
  175. package/dist/player/AssessmentPlayer.d.ts.map +0 -1
  176. package/dist/player/AssessmentPlayer.js +0 -974
  177. package/dist/player/AssessmentPlayer.js.map +0 -1
  178. package/dist/player/index.d.ts +0 -17
  179. package/dist/player/index.d.ts.map +0 -1
  180. package/dist/player/index.js +0 -15
  181. package/dist/player/index.js.map +0 -1
  182. package/dist/player/navigation-types.d.ts +0 -63
  183. package/dist/player/navigation-types.d.ts.map +0 -1
  184. package/dist/player/navigation-types.js +0 -7
  185. package/dist/player/navigation-types.js.map +0 -1
  186. package/dist/player/qti-navigation.d.ts +0 -29
  187. package/dist/player/qti-navigation.d.ts.map +0 -1
  188. package/dist/player/qti-navigation.js +0 -160
  189. package/dist/player/qti-navigation.js.map +0 -1
  190. package/dist/reference-layout/index.d.ts +0 -19
  191. package/dist/reference-layout/index.d.ts.map +0 -1
  192. package/dist/reference-layout/index.js +0 -20
  193. package/dist/reference-layout/index.js.map +0 -1
  194. package/dist/services/AnnotationToolbarConfig.d.ts +0 -134
  195. package/dist/services/AnnotationToolbarConfig.d.ts.map +0 -1
  196. package/dist/services/AnnotationToolbarConfig.js +0 -100
  197. package/dist/services/AnnotationToolbarConfig.js.map +0 -1
  198. package/dist/services/AssessmentAuthoringService.d.ts +0 -60
  199. package/dist/services/AssessmentAuthoringService.d.ts.map +0 -1
  200. package/dist/services/AssessmentAuthoringService.js +0 -183
  201. package/dist/services/AssessmentAuthoringService.js.map +0 -1
  202. package/dist/services/PNPMapper.d.ts +0 -81
  203. package/dist/services/PNPMapper.d.ts.map +0 -1
  204. package/dist/services/PNPMapper.js +0 -108
  205. package/dist/services/PNPMapper.js.map +0 -1
  206. package/src/components/PNPProfileTester.example.svelte +0 -188
  207. package/src/components/PNPProfileTester.svelte +0 -397
  208. package/src/components/QuestionToolBar.svelte +0 -483
  209. package/src/player/AssessmentLayout.svelte +0 -68
  210. package/src/player/README.md +0 -154
  211. package/src/reference-layout/README.md +0 -135
  212. package/src/reference-layout/ReferenceLayout.svelte +0 -201
  213. package/src/reference-layout/components/AssessmentContent.svelte +0 -259
  214. package/src/reference-layout/components/AssessmentFooter.svelte +0 -89
  215. package/src/reference-layout/components/AssessmentHeader.svelte +0 -250
  216. package/src/reference-layout/components/AssessmentNavigation.svelte +0 -134
  217. package/src/reference-layout/components/AssessmentToolsBar.svelte +0 -176
  218. package/src/reference-layout/components/ItemPanel.svelte +0 -200
  219. package/src/reference-layout/components/NotesPanel.svelte +0 -179
  220. package/src/reference-layout/components/PassagePanel.svelte +0 -76
@@ -13,13 +13,14 @@
13
13
  *
14
14
  * Part of PIE Assessment Toolkit.
15
15
  */
16
+ import { normalizeToolsConfig, resolveToolsForLevel, } from "./tools-config-normalizer.js";
16
17
  import { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
17
18
  import { ElementToolStateStore } from "./ElementToolStateStore.js";
18
19
  import { HighlightCoordinator } from "./HighlightCoordinator.js";
19
20
  import { ToolCoordinator } from "./ToolCoordinator.js";
20
21
  import { TTSService } from "./TTSService.js";
21
22
  import { BrowserTTSProvider } from "./tts/browser-provider.js";
22
- import { ToolProviderRegistry, DesmosToolProvider, TIToolProvider, TTSToolProvider, } from "./tool-providers/index.js";
23
+ import { ToolProviderRegistry, DesmosToolProvider, MathJsToolProvider, TIToolProvider, TTSToolProvider, } from "./tool-providers/index.js";
23
24
  /**
24
25
  * ToolkitCoordinator - Orchestrates all assessment toolkit services
25
26
  *
@@ -54,106 +55,447 @@ export class ToolkitCoordinator {
54
55
  elementToolStateStore;
55
56
  catalogResolver;
56
57
  toolProviderRegistry;
58
+ hooks;
59
+ lazyInit;
57
60
  /** Track TTS initialization state */
58
61
  ttsInitialized = false;
62
+ ttsInitPromise;
63
+ stateLoaded = false;
64
+ stateLoadPromise;
65
+ coordinatorReadyPromise;
66
+ coordinatorReadyNotified = false;
67
+ providerInitPromises = new Map();
68
+ sectionControllers = new Map();
69
+ sectionControllerInitPromises = new Map();
70
+ sectionPersistenceStrategies = new Map();
59
71
  /** Callback for floating tools changes */
60
72
  floatingToolsChangeCallback = null;
73
+ static resolveConfig(config) {
74
+ const normalized = normalizeToolsConfig(config.tools);
75
+ const defaultProviders = {
76
+ tts: {
77
+ enabled: true,
78
+ backend: "browser",
79
+ },
80
+ calculator: {
81
+ enabled: true,
82
+ provider: "desmos",
83
+ },
84
+ };
85
+ return {
86
+ ...config,
87
+ tools: {
88
+ ...normalized,
89
+ providers: {
90
+ ...defaultProviders,
91
+ ...normalized.providers,
92
+ },
93
+ },
94
+ accessibility: {
95
+ language: "en-US",
96
+ ...(config.accessibility ?? {}),
97
+ catalogs: config.accessibility?.catalogs ?? [],
98
+ },
99
+ };
100
+ }
61
101
  constructor(config) {
62
102
  if (!config.assessmentId) {
63
103
  throw new Error("ToolkitCoordinator requires assessmentId in config");
64
104
  }
65
- this.assessmentId = config.assessmentId;
66
- this.config = config;
105
+ const resolvedConfig = ToolkitCoordinator.resolveConfig(config);
106
+ this.assessmentId = resolvedConfig.assessmentId;
107
+ this.config = resolvedConfig;
108
+ this.hooks = resolvedConfig.hooks ?? {};
109
+ this.lazyInit = config.lazyInit === true;
67
110
  // Initialize all services
68
111
  this.toolCoordinator = new ToolCoordinator();
69
112
  this.highlightCoordinator = new HighlightCoordinator();
70
113
  this.elementToolStateStore = new ElementToolStateStore();
71
- this.catalogResolver = new AccessibilityCatalogResolver(config.accessibility?.catalogs || [], config.accessibility?.language || "en-US");
114
+ this.catalogResolver = new AccessibilityCatalogResolver(resolvedConfig.accessibility?.catalogs || [], resolvedConfig.accessibility?.language || "en-US");
72
115
  // Initialize tool provider registry
73
116
  this.toolProviderRegistry = new ToolProviderRegistry();
74
117
  this._registerToolProviders();
75
118
  // Initialize TTS service based on config
76
119
  this.ttsService = new TTSService();
77
- const ttsConfig = config.tools?.tts;
78
- if (ttsConfig?.enabled !== false) {
79
- // Initialize async (fire and forget - service will become available when ready)
80
- this._initializeTTS(ttsConfig).catch((err) => {
81
- console.error("[ToolkitCoordinator] Failed to initialize TTS:", err);
120
+ this.setupStatePersistenceHooks();
121
+ if (!this.lazyInit) {
122
+ void this.waitUntilReady().catch((err) => {
123
+ console.error("[ToolkitCoordinator] Failed eager initialization:", err);
124
+ this.handleError(err, { phase: "coordinator-ready" });
82
125
  });
83
126
  }
84
127
  }
128
+ async emitTelemetry(eventName, payload) {
129
+ try {
130
+ await this.hooks.onTelemetry?.(eventName, payload);
131
+ }
132
+ catch (err) {
133
+ console.warn("[ToolkitCoordinator] telemetry hook failed:", err);
134
+ }
135
+ }
136
+ handleError(error, context) {
137
+ const normalized = error instanceof Error ? error : new Error(String(error));
138
+ try {
139
+ this.hooks.onError?.(normalized, context);
140
+ }
141
+ catch (hookError) {
142
+ console.warn("[ToolkitCoordinator] onError hook failed:", hookError);
143
+ }
144
+ }
145
+ getSectionControllerMapKey(key) {
146
+ return `${key.assessmentId}::${key.sectionId}::${key.attemptId || ""}`;
147
+ }
148
+ createDefaultSectionPersistence() {
149
+ const storage = (() => {
150
+ try {
151
+ if (typeof window === "undefined")
152
+ return null;
153
+ return window.localStorage;
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ })();
159
+ const getStorageKey = (context) => {
160
+ const { assessmentId, sectionId, attemptId } = context.key;
161
+ return `pie:section-controller:v1:${assessmentId}:${sectionId}:${attemptId || "default"}`;
162
+ };
163
+ return {
164
+ async load(context) {
165
+ if (!storage)
166
+ return null;
167
+ const value = storage.getItem(getStorageKey(context));
168
+ if (!value)
169
+ return null;
170
+ try {
171
+ return JSON.parse(value);
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ },
177
+ async save(context, snapshot) {
178
+ if (!storage)
179
+ return;
180
+ storage.setItem(getStorageKey(context), JSON.stringify(snapshot));
181
+ },
182
+ async clear(context) {
183
+ if (!storage)
184
+ return;
185
+ storage.removeItem(getStorageKey(context));
186
+ },
187
+ };
188
+ }
189
+ async resolveSectionPersistence(context) {
190
+ const cacheKey = this.getSectionControllerMapKey(context.key);
191
+ const existing = this.sectionPersistenceStrategies.get(cacheKey);
192
+ if (existing)
193
+ return existing;
194
+ const defaults = {
195
+ createDefaultPersistence: () => this.createDefaultSectionPersistence(),
196
+ };
197
+ const strategy = (await this.hooks.createSectionPersistence?.(context, defaults)) ??
198
+ (await defaults.createDefaultPersistence());
199
+ this.sectionPersistenceStrategies.set(cacheKey, strategy);
200
+ return strategy;
201
+ }
202
+ setupStatePersistenceHooks() {
203
+ this.elementToolStateStore.setOnStateChange((state) => {
204
+ if (!this.hooks.saveToolState)
205
+ return;
206
+ void Promise.resolve(this.hooks.saveToolState(state)).catch((err) => {
207
+ this.handleError(err, { phase: "state-save" });
208
+ });
209
+ });
210
+ }
211
+ async ensureStateLoaded() {
212
+ if (this.stateLoaded)
213
+ return;
214
+ if (this.stateLoadPromise)
215
+ return this.stateLoadPromise;
216
+ this.stateLoadPromise = (async () => {
217
+ const loader = this.hooks.loadToolState;
218
+ if (!loader) {
219
+ return;
220
+ }
221
+ try {
222
+ const state = await loader();
223
+ if (state && typeof state === "object") {
224
+ this.elementToolStateStore.loadState(state);
225
+ }
226
+ this.stateLoaded = true;
227
+ await this.emitTelemetry("tool-state-loaded", {
228
+ hasState: Boolean(state),
229
+ });
230
+ }
231
+ catch (err) {
232
+ this.handleError(err, { phase: "state-load" });
233
+ }
234
+ })().finally(() => {
235
+ this.stateLoadPromise = undefined;
236
+ });
237
+ return this.stateLoadPromise;
238
+ }
85
239
  /**
86
240
  * Register tool providers in the registry
87
241
  */
88
242
  _registerToolProviders() {
89
243
  // Register TTS provider
90
- const ttsConfig = this.config.tools?.tts;
244
+ const ttsConfig = this.config.tools?.providers?.tts;
91
245
  if (ttsConfig?.enabled !== false) {
92
246
  const backend = ttsConfig?.backend || "browser";
93
- try {
94
- this.toolProviderRegistry.register("tts", {
95
- provider: new TTSToolProvider(backend),
96
- config: {
97
- backend,
98
- apiEndpoint: ttsConfig?.apiEndpoint,
99
- voice: ttsConfig?.defaultVoice,
100
- rate: ttsConfig?.rate,
101
- pitch: ttsConfig?.pitch,
102
- },
103
- lazy: true,
104
- authFetcher: ttsConfig?.authFetcher,
105
- });
106
- }
107
- catch (error) {
108
- console.warn("[ToolkitCoordinator] Failed to register TTS provider:", error);
109
- }
247
+ void this.registerProvider("tts", {
248
+ provider: new TTSToolProvider(backend),
249
+ config: {
250
+ backend,
251
+ apiEndpoint: ttsConfig?.apiEndpoint,
252
+ voice: ttsConfig?.defaultVoice,
253
+ rate: ttsConfig?.rate,
254
+ pitch: ttsConfig?.pitch,
255
+ },
256
+ lazy: true,
257
+ authFetcher: ttsConfig?.authFetcher,
258
+ });
110
259
  }
111
260
  // Register calculator providers
112
- const floatingTools = this.config.tools?.floatingTools;
113
- const calculatorConfig = floatingTools?.calculator;
261
+ const calculatorConfig = this.config.tools?.providers?.calculator;
114
262
  if (calculatorConfig?.enabled !== false) {
115
263
  const provider = calculatorConfig?.provider || "desmos";
116
264
  if (provider === "desmos") {
117
- try {
118
- this.toolProviderRegistry.register("calculator-desmos", {
119
- provider: new DesmosToolProvider(),
120
- config: {},
121
- lazy: true,
122
- authFetcher: calculatorConfig?.authFetcher,
123
- });
124
- }
125
- catch (error) {
126
- console.warn("[ToolkitCoordinator] Failed to register Desmos calculator provider:", error);
127
- }
265
+ void this.registerProvider("calculator-desmos", {
266
+ provider: new DesmosToolProvider(),
267
+ config: {},
268
+ lazy: true,
269
+ authFetcher: calculatorConfig?.authFetcher,
270
+ });
128
271
  }
129
272
  else if (provider === "ti") {
130
- try {
131
- this.toolProviderRegistry.register("calculator-ti", {
132
- provider: new TIToolProvider(),
133
- config: {},
134
- lazy: true,
135
- });
136
- }
137
- catch (error) {
138
- console.warn("[ToolkitCoordinator] Failed to register TI calculator provider:", error);
139
- }
273
+ void this.registerProvider("calculator-ti", {
274
+ provider: new TIToolProvider(),
275
+ config: {},
276
+ lazy: true,
277
+ });
278
+ }
279
+ else if (provider === "mathjs") {
280
+ void this.registerProvider("calculator-mathjs", {
281
+ provider: new MathJsToolProvider(),
282
+ config: {},
283
+ lazy: true,
284
+ });
140
285
  }
141
286
  }
142
287
  }
288
+ async registerProvider(providerId, config) {
289
+ try {
290
+ this.toolProviderRegistry.register(providerId, config);
291
+ const meta = {
292
+ providerId,
293
+ providerName: config.provider.providerName,
294
+ };
295
+ await this.hooks.onProviderRegistered?.(providerId, meta);
296
+ await this.emitTelemetry("provider-registered", {
297
+ providerId,
298
+ providerName: config.provider.providerName,
299
+ });
300
+ }
301
+ catch (err) {
302
+ console.warn(`[ToolkitCoordinator] Failed to register provider "${providerId}":`, err);
303
+ this.handleError(err, { phase: "provider-register", providerId });
304
+ this.hooks.onProviderError?.(providerId, err, {
305
+ phase: "provider-register",
306
+ providerId,
307
+ });
308
+ }
309
+ }
310
+ async ensureProviderReady(providerId) {
311
+ const existing = this.providerInitPromises.get(providerId);
312
+ if (existing)
313
+ return existing;
314
+ const promise = (async () => {
315
+ const provider = await this.toolProviderRegistry.getProvider(providerId, false);
316
+ const meta = {
317
+ providerId,
318
+ providerName: provider.providerName,
319
+ };
320
+ try {
321
+ await this.hooks.onProviderInitStart?.(providerId, meta);
322
+ await this.toolProviderRegistry.initialize(providerId);
323
+ await this.hooks.onProviderReady?.(providerId, meta);
324
+ await this.emitTelemetry("provider-ready", { providerId });
325
+ return provider;
326
+ }
327
+ catch (err) {
328
+ const error = err instanceof Error ? err : new Error(String(err));
329
+ this.hooks.onProviderError?.(providerId, error, {
330
+ phase: "provider-init",
331
+ providerId,
332
+ });
333
+ this.handleError(error, { phase: "provider-init", providerId });
334
+ throw error;
335
+ }
336
+ })().finally(() => {
337
+ this.providerInitPromises.delete(providerId);
338
+ });
339
+ this.providerInitPromises.set(providerId, promise);
340
+ return promise;
341
+ }
342
+ setHooks(hooks) {
343
+ Object.assign(this.hooks, hooks);
344
+ this.setupStatePersistenceHooks();
345
+ if (hooks.onCoordinatorReady && this.isReady()) {
346
+ void Promise.resolve(hooks.onCoordinatorReady(this)).catch((err) => {
347
+ this.handleError(err, { phase: "coordinator-ready" });
348
+ });
349
+ }
350
+ }
351
+ getSectionController(args) {
352
+ const key = {
353
+ assessmentId: this.assessmentId,
354
+ sectionId: args.sectionId,
355
+ attemptId: args.attemptId,
356
+ };
357
+ return this.sectionControllers.get(this.getSectionControllerMapKey(key));
358
+ }
359
+ async getOrCreateSectionController(args) {
360
+ const key = {
361
+ assessmentId: this.assessmentId,
362
+ sectionId: args.sectionId,
363
+ attemptId: args.attemptId,
364
+ };
365
+ const mapKey = this.getSectionControllerMapKey(key);
366
+ const existingController = this.sectionControllers.get(mapKey);
367
+ if (existingController) {
368
+ if (args.updateExisting !== false) {
369
+ await existingController.updateInput?.(args.input);
370
+ }
371
+ return existingController;
372
+ }
373
+ const existingPromise = this.sectionControllerInitPromises.get(mapKey);
374
+ if (existingPromise)
375
+ return existingPromise;
376
+ const initPromise = (async () => {
377
+ const context = {
378
+ key,
379
+ coordinator: this,
380
+ input: args.input,
381
+ };
382
+ const persistence = await this.resolveSectionPersistence(context);
383
+ const defaults = {
384
+ createDefaultController: args.createDefaultController,
385
+ };
386
+ const controller = (await this.hooks.createSectionController?.(context, defaults)) ??
387
+ (await defaults.createDefaultController());
388
+ await controller.setPersistenceStrategy?.(persistence);
389
+ await controller.setPersistenceContext?.(context);
390
+ await controller.initialize?.(args.input);
391
+ await controller.hydrate?.();
392
+ this.sectionControllers.set(mapKey, controller);
393
+ await this.hooks.onSectionControllerReady?.(context, controller);
394
+ await this.emitTelemetry("section-controller-ready", {
395
+ assessmentId: key.assessmentId,
396
+ sectionId: key.sectionId,
397
+ attemptId: key.attemptId,
398
+ });
399
+ return controller;
400
+ })().catch((err) => {
401
+ this.handleError(err, {
402
+ phase: "section-controller-init",
403
+ details: {
404
+ sectionId: args.sectionId,
405
+ attemptId: args.attemptId,
406
+ },
407
+ });
408
+ throw err;
409
+ }).finally(() => {
410
+ this.sectionControllerInitPromises.delete(mapKey);
411
+ });
412
+ this.sectionControllerInitPromises.set(mapKey, initPromise);
413
+ return initPromise;
414
+ }
415
+ async disposeSectionController(args) {
416
+ const key = {
417
+ assessmentId: this.assessmentId,
418
+ sectionId: args.sectionId,
419
+ attemptId: args.attemptId,
420
+ };
421
+ const mapKey = this.getSectionControllerMapKey(key);
422
+ const controller = this.sectionControllers.get(mapKey);
423
+ if (!controller)
424
+ return;
425
+ const context = {
426
+ key,
427
+ coordinator: this,
428
+ };
429
+ try {
430
+ if (args.persistBeforeDispose !== false) {
431
+ await controller.persist?.();
432
+ }
433
+ await controller.dispose?.();
434
+ await this.hooks.onSectionControllerDispose?.(context, controller);
435
+ await this.emitTelemetry("section-controller-disposed", {
436
+ assessmentId: key.assessmentId,
437
+ sectionId: key.sectionId,
438
+ attemptId: key.attemptId,
439
+ });
440
+ }
441
+ catch (err) {
442
+ this.handleError(err, {
443
+ phase: "section-controller-dispose",
444
+ details: {
445
+ sectionId: args.sectionId,
446
+ attemptId: args.attemptId,
447
+ },
448
+ });
449
+ }
450
+ finally {
451
+ this.sectionControllers.delete(mapKey);
452
+ if (args.clearPersistence) {
453
+ const strategy = this.sectionPersistenceStrategies.get(mapKey);
454
+ await strategy?.clear?.(context);
455
+ }
456
+ this.sectionPersistenceStrategies.delete(mapKey);
457
+ }
458
+ }
143
459
  /**
144
460
  * Initialize TTS service with provider
145
461
  */
462
+ async ensureTTSReady(config) {
463
+ if (this.ttsInitialized)
464
+ return;
465
+ if (this.ttsInitPromise)
466
+ return this.ttsInitPromise;
467
+ this.ttsInitPromise = this._initializeTTS(config).finally(() => {
468
+ this.ttsInitPromise = undefined;
469
+ });
470
+ return this.ttsInitPromise;
471
+ }
146
472
  async _initializeTTS(config) {
147
473
  if (this.ttsInitialized)
148
474
  return;
475
+ await this.hooks.onBeforeTTSInit?.({
476
+ phase: "tts-init",
477
+ details: {
478
+ backend: config?.backend,
479
+ },
480
+ });
481
+ await this.emitTelemetry("tts-init-start", {
482
+ backend: config?.backend ??
483
+ this.config.tools?.providers?.tts
484
+ ?.backend ??
485
+ "browser",
486
+ });
149
487
  // Try to use TTS provider from registry if available
150
488
  if (this.toolProviderRegistry.has("tts")) {
151
489
  try {
152
- const ttsProvider = await this.toolProviderRegistry.getProvider("tts");
490
+ const ttsProvider = await this.ensureProviderReady("tts");
153
491
  const providerInstance = await ttsProvider.createInstance();
154
492
  await this.ttsService.initialize(providerInstance);
155
493
  this.ttsService.setCatalogResolver(this.catalogResolver);
156
494
  this.ttsInitialized = true;
495
+ await this.hooks.onTTSReady?.();
496
+ await this.emitTelemetry("tts-init-success", {
497
+ provider: "registry",
498
+ });
157
499
  console.log("[ToolkitCoordinator] TTS initialized via ToolProviderRegistry");
158
500
  return;
159
501
  }
@@ -163,14 +505,24 @@ export class ToolkitCoordinator {
163
505
  }
164
506
  // Fallback to browser provider
165
507
  const provider = new BrowserTTSProvider();
166
- await this.ttsService.initialize(provider);
167
- this.ttsService.setCatalogResolver(this.catalogResolver);
168
- // Apply TTS-specific configuration
169
- if (config) {
170
- // Future: Apply voice, rate, pitch settings to provider
171
- // For now, these are hints for UI (voice picker, rate slider)
508
+ try {
509
+ await this.ttsService.initialize(provider, config);
510
+ this.ttsService.setCatalogResolver(this.catalogResolver);
511
+ this.ttsInitialized = true;
512
+ await this.hooks.onTTSReady?.();
513
+ await this.emitTelemetry("tts-init-success", {
514
+ provider: "browser-fallback",
515
+ });
516
+ }
517
+ catch (error) {
518
+ const normalized = error instanceof Error ? error : new Error(String(error));
519
+ this.hooks.onTTSError?.(normalized, { phase: "tts-init" });
520
+ this.handleError(normalized, { phase: "tts-init" });
521
+ await this.emitTelemetry("tts-init-error", {
522
+ message: normalized.message,
523
+ });
524
+ throw normalized;
172
525
  }
173
- this.ttsInitialized = true;
174
526
  }
175
527
  /**
176
528
  * Get all services as a bundle for section player convenience.
@@ -193,7 +545,48 @@ export class ToolkitCoordinator {
193
545
  * @returns Tool provider instance
194
546
  */
195
547
  async getToolProvider(providerId) {
196
- return this.toolProviderRegistry.getProvider(providerId);
548
+ return this.ensureProviderReady(providerId);
549
+ }
550
+ async waitUntilReady() {
551
+ if (this.isReady())
552
+ return;
553
+ if (this.coordinatorReadyPromise)
554
+ return this.coordinatorReadyPromise;
555
+ this.coordinatorReadyPromise = (async () => {
556
+ await this.ensureStateLoaded();
557
+ const ttsConfig = this.config.tools?.providers?.tts;
558
+ if (ttsConfig?.enabled !== false) {
559
+ await this.ensureTTSReady(ttsConfig);
560
+ }
561
+ if (!this.coordinatorReadyNotified) {
562
+ this.coordinatorReadyNotified = true;
563
+ await this.hooks.onCoordinatorReady?.(this);
564
+ await this.emitTelemetry("coordinator-ready", {
565
+ assessmentId: this.assessmentId,
566
+ });
567
+ }
568
+ })().finally(() => {
569
+ this.coordinatorReadyPromise = undefined;
570
+ });
571
+ return this.coordinatorReadyPromise;
572
+ }
573
+ isReady() {
574
+ return this.getInitStatus().coordinator;
575
+ }
576
+ getInitStatus() {
577
+ const providers = {};
578
+ for (const providerId of this.toolProviderRegistry.getProviderIds()) {
579
+ providers[providerId] = this.toolProviderRegistry.isInitialized(providerId);
580
+ }
581
+ return {
582
+ tts: this.ttsInitialized,
583
+ stateLoaded: this.stateLoaded || !this.hooks.loadToolState,
584
+ coordinator: (this.stateLoaded || !this.hooks.loadToolState) &&
585
+ (this.ttsInitialized ||
586
+ this.config.tools?.providers?.tts
587
+ ?.enabled === false),
588
+ providers,
589
+ };
197
590
  }
198
591
  /**
199
592
  * Check if a tool is enabled.
@@ -203,7 +596,7 @@ export class ToolkitCoordinator {
203
596
  * @returns True if tool is enabled
204
597
  */
205
598
  isToolEnabled(toolId) {
206
- const toolConfig = this.config.tools?.[toolId];
599
+ const toolConfig = this.config.tools?.providers?.[toolId];
207
600
  // Enabled by default unless explicitly set to false
208
601
  return toolConfig?.enabled !== false;
209
602
  }
@@ -214,7 +607,7 @@ export class ToolkitCoordinator {
214
607
  * @returns Tool configuration or null if not configured
215
608
  */
216
609
  getToolConfig(toolId) {
217
- return this.config.tools?.[toolId] || null;
610
+ return this.config.tools?.providers?.[toolId] || null;
218
611
  }
219
612
  /**
220
613
  * Update tool configuration.
@@ -227,9 +620,12 @@ export class ToolkitCoordinator {
227
620
  // Update config
228
621
  const current = this.getToolConfig(toolId) || {};
229
622
  if (!this.config.tools) {
230
- this.config.tools = {};
623
+ this.config.tools = normalizeToolsConfig();
624
+ }
625
+ if (!this.config.tools.providers) {
626
+ this.config.tools.providers = {};
231
627
  }
232
- this.config.tools[toolId] = { ...current, ...updates };
628
+ this.config.tools.providers[toolId] = { ...current, ...updates };
233
629
  // Apply configuration changes to services
234
630
  this._applyToolConfigChange(toolId, updates);
235
631
  }
@@ -241,12 +637,12 @@ export class ToolkitCoordinator {
241
637
  */
242
638
  updateFloatingTools(toolIds) {
243
639
  if (!this.config.tools) {
244
- this.config.tools = {};
640
+ this.config.tools = normalizeToolsConfig();
245
641
  }
246
- if (!this.config.tools.floatingTools) {
247
- this.config.tools.floatingTools = {};
642
+ if (!this.config.tools.placement) {
643
+ this.config.tools.placement = normalizeToolsConfig().placement;
248
644
  }
249
- this.config.tools.floatingTools.enabledTools = toolIds;
645
+ this.config.tools.placement.section = [...toolIds];
250
646
  // Notify listener of change
251
647
  if (this.floatingToolsChangeCallback) {
252
648
  this.floatingToolsChangeCallback(toolIds);
@@ -258,7 +654,9 @@ export class ToolkitCoordinator {
258
654
  * @returns Array of enabled tool IDs
259
655
  */
260
656
  getFloatingTools() {
261
- return this.config.tools?.floatingTools?.enabledTools || [];
657
+ if (!this.config.tools)
658
+ return [];
659
+ return resolveToolsForLevel(this.config.tools, "section");
262
660
  }
263
661
  /**
264
662
  * Set a callback to be notified when floating tools change.
@@ -279,12 +677,16 @@ export class ToolkitCoordinator {
279
677
  * Apply tool configuration changes to underlying services.
280
678
  * Called after updateToolConfig().
281
679
  */
282
- _applyToolConfigChange(toolId, updates) {
680
+ _applyToolConfigChange(toolId, _updates) {
283
681
  // Apply configuration changes based on tool
284
682
  switch (toolId) {
285
683
  case "tts":
286
- // Future: Update TTSService with voice/rate/pitch changes
287
- // For now, config is used by UI widgets to show current settings
684
+ void this._reconfigureTTSProvider().then(async () => {
685
+ const ttsConfig = this.config.tools?.providers?.tts;
686
+ if (!this.lazyInit && ttsConfig?.enabled !== false) {
687
+ await this.ensureTTSReady(ttsConfig);
688
+ }
689
+ });
288
690
  break;
289
691
  case "answerEliminator":
290
692
  // Future: Could notify answer eliminator tools of strategy change
@@ -292,5 +694,34 @@ export class ToolkitCoordinator {
292
694
  // Add cases for other tools as needed
293
695
  }
294
696
  }
697
+ async _reconfigureTTSProvider() {
698
+ this.ttsInitialized = false;
699
+ this.ttsInitPromise = undefined;
700
+ try {
701
+ this.ttsService.stop();
702
+ }
703
+ catch {
704
+ // noop: stop best effort
705
+ }
706
+ if (this.toolProviderRegistry.has("tts")) {
707
+ await this.toolProviderRegistry.unregister("tts");
708
+ }
709
+ const ttsConfig = this.config.tools?.providers?.tts;
710
+ if (ttsConfig?.enabled === false)
711
+ return;
712
+ const backend = ttsConfig?.backend || "browser";
713
+ await this.registerProvider("tts", {
714
+ provider: new TTSToolProvider(backend),
715
+ config: {
716
+ backend,
717
+ apiEndpoint: ttsConfig?.apiEndpoint,
718
+ voice: ttsConfig?.defaultVoice,
719
+ rate: ttsConfig?.rate,
720
+ pitch: ttsConfig?.pitch,
721
+ },
722
+ lazy: true,
723
+ authFetcher: ttsConfig?.authFetcher,
724
+ });
725
+ }
295
726
  }
296
727
  //# sourceMappingURL=ToolkitCoordinator.js.map