@pie-players/pie-assessment-toolkit 0.2.0

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 (194) hide show
  1. package/README.md +520 -0
  2. package/dist/attempt/TestSession.d.ts +90 -0
  3. package/dist/attempt/TestSession.d.ts.map +1 -0
  4. package/dist/attempt/TestSession.js +174 -0
  5. package/dist/attempt/TestSession.js.map +1 -0
  6. package/dist/core/TypedEventBus.d.ts +48 -0
  7. package/dist/core/TypedEventBus.d.ts.map +1 -0
  8. package/dist/core/TypedEventBus.js +63 -0
  9. package/dist/core/TypedEventBus.js.map +1 -0
  10. package/dist/index.d.ts +37 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +44 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/item-loader.d.ts +45 -0
  15. package/dist/item-loader.d.ts.map +1 -0
  16. package/dist/item-loader.js +62 -0
  17. package/dist/item-loader.js.map +1 -0
  18. package/dist/player/AssessmentPlayer.d.ts +388 -0
  19. package/dist/player/AssessmentPlayer.d.ts.map +1 -0
  20. package/dist/player/AssessmentPlayer.js +965 -0
  21. package/dist/player/AssessmentPlayer.js.map +1 -0
  22. package/dist/player/index.d.ts +17 -0
  23. package/dist/player/index.d.ts.map +1 -0
  24. package/dist/player/index.js +15 -0
  25. package/dist/player/index.js.map +1 -0
  26. package/dist/player/navigation-types.d.ts +63 -0
  27. package/dist/player/navigation-types.d.ts.map +1 -0
  28. package/dist/player/navigation-types.js +7 -0
  29. package/dist/player/navigation-types.js.map +1 -0
  30. package/dist/player/qti-navigation.d.ts +29 -0
  31. package/dist/player/qti-navigation.d.ts.map +1 -0
  32. package/dist/player/qti-navigation.js +160 -0
  33. package/dist/player/qti-navigation.js.map +1 -0
  34. package/dist/reference-layout/index.d.ts +19 -0
  35. package/dist/reference-layout/index.d.ts.map +1 -0
  36. package/dist/reference-layout/index.js +20 -0
  37. package/dist/reference-layout/index.js.map +1 -0
  38. package/dist/services/AccessibilityCatalogResolver.d.ts +141 -0
  39. package/dist/services/AccessibilityCatalogResolver.d.ts.map +1 -0
  40. package/dist/services/AccessibilityCatalogResolver.js +249 -0
  41. package/dist/services/AccessibilityCatalogResolver.js.map +1 -0
  42. package/dist/services/AssessmentAuthoringService.d.ts +60 -0
  43. package/dist/services/AssessmentAuthoringService.d.ts.map +1 -0
  44. package/dist/services/AssessmentAuthoringService.js +183 -0
  45. package/dist/services/AssessmentAuthoringService.js.map +1 -0
  46. package/dist/services/ContextVariableStore.d.ts +114 -0
  47. package/dist/services/ContextVariableStore.d.ts.map +1 -0
  48. package/dist/services/ContextVariableStore.js +207 -0
  49. package/dist/services/ContextVariableStore.js.map +1 -0
  50. package/dist/services/ElementToolStateStore.d.ts +125 -0
  51. package/dist/services/ElementToolStateStore.d.ts.map +1 -0
  52. package/dist/services/ElementToolStateStore.js +200 -0
  53. package/dist/services/ElementToolStateStore.js.map +1 -0
  54. package/dist/services/HighlightCoordinator.d.ts +179 -0
  55. package/dist/services/HighlightCoordinator.d.ts.map +1 -0
  56. package/dist/services/HighlightCoordinator.js +446 -0
  57. package/dist/services/HighlightCoordinator.js.map +1 -0
  58. package/dist/services/I18nService.d.ts +121 -0
  59. package/dist/services/I18nService.d.ts.map +1 -0
  60. package/dist/services/I18nService.js +276 -0
  61. package/dist/services/I18nService.js.map +1 -0
  62. package/dist/services/PNPMapper.d.ts +71 -0
  63. package/dist/services/PNPMapper.d.ts.map +1 -0
  64. package/dist/services/PNPMapper.js +98 -0
  65. package/dist/services/PNPMapper.js.map +1 -0
  66. package/dist/services/PNPToolResolver.d.ts +127 -0
  67. package/dist/services/PNPToolResolver.d.ts.map +1 -0
  68. package/dist/services/PNPToolResolver.js +222 -0
  69. package/dist/services/PNPToolResolver.js.map +1 -0
  70. package/dist/services/SSMLExtractor.d.ts +61 -0
  71. package/dist/services/SSMLExtractor.d.ts.map +1 -0
  72. package/dist/services/SSMLExtractor.js +181 -0
  73. package/dist/services/SSMLExtractor.js.map +1 -0
  74. package/dist/services/TTSService.d.ts +162 -0
  75. package/dist/services/TTSService.d.ts.map +1 -0
  76. package/dist/services/TTSService.js +465 -0
  77. package/dist/services/TTSService.js.map +1 -0
  78. package/dist/services/ThemeProvider.d.ts +95 -0
  79. package/dist/services/ThemeProvider.d.ts.map +1 -0
  80. package/dist/services/ThemeProvider.js +299 -0
  81. package/dist/services/ThemeProvider.js.map +1 -0
  82. package/dist/services/ToolConfigResolver.d.ts +162 -0
  83. package/dist/services/ToolConfigResolver.d.ts.map +1 -0
  84. package/dist/services/ToolConfigResolver.js +230 -0
  85. package/dist/services/ToolConfigResolver.js.map +1 -0
  86. package/dist/services/ToolCoordinator.d.ts +137 -0
  87. package/dist/services/ToolCoordinator.d.ts.map +1 -0
  88. package/dist/services/ToolCoordinator.js +318 -0
  89. package/dist/services/ToolCoordinator.js.map +1 -0
  90. package/dist/services/ToolkitCoordinator.d.ts +188 -0
  91. package/dist/services/ToolkitCoordinator.d.ts.map +1 -0
  92. package/dist/services/ToolkitCoordinator.js +252 -0
  93. package/dist/services/ToolkitCoordinator.js.map +1 -0
  94. package/dist/services/interfaces.d.ts +391 -0
  95. package/dist/services/interfaces.d.ts.map +1 -0
  96. package/dist/services/interfaces.js +12 -0
  97. package/dist/services/interfaces.js.map +1 -0
  98. package/dist/services/tool-providers/DesmosToolProvider.d.ts +103 -0
  99. package/dist/services/tool-providers/DesmosToolProvider.d.ts.map +1 -0
  100. package/dist/services/tool-providers/DesmosToolProvider.js +127 -0
  101. package/dist/services/tool-providers/DesmosToolProvider.js.map +1 -0
  102. package/dist/services/tool-providers/IToolProvider.d.ts +137 -0
  103. package/dist/services/tool-providers/IToolProvider.d.ts.map +1 -0
  104. package/dist/services/tool-providers/IToolProvider.js +13 -0
  105. package/dist/services/tool-providers/IToolProvider.js.map +1 -0
  106. package/dist/services/tool-providers/TIToolProvider.d.ts +107 -0
  107. package/dist/services/tool-providers/TIToolProvider.d.ts.map +1 -0
  108. package/dist/services/tool-providers/TIToolProvider.js +123 -0
  109. package/dist/services/tool-providers/TIToolProvider.js.map +1 -0
  110. package/dist/services/tool-providers/TTSToolProvider.d.ts +143 -0
  111. package/dist/services/tool-providers/TTSToolProvider.d.ts.map +1 -0
  112. package/dist/services/tool-providers/TTSToolProvider.js +168 -0
  113. package/dist/services/tool-providers/TTSToolProvider.js.map +1 -0
  114. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +165 -0
  115. package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -0
  116. package/dist/services/tool-providers/ToolProviderRegistry.js +240 -0
  117. package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -0
  118. package/dist/services/tool-providers/index.d.ts +18 -0
  119. package/dist/services/tool-providers/index.d.ts.map +1 -0
  120. package/dist/services/tool-providers/index.js +15 -0
  121. package/dist/services/tool-providers/index.js.map +1 -0
  122. package/dist/services/tts/browser-provider.d.ts +24 -0
  123. package/dist/services/tts/browser-provider.d.ts.map +1 -0
  124. package/dist/services/tts/browser-provider.js +177 -0
  125. package/dist/services/tts/browser-provider.js.map +1 -0
  126. package/dist/services/tts/provider-interface.d.ts +131 -0
  127. package/dist/services/tts/provider-interface.d.ts.map +1 -0
  128. package/dist/services/tts/provider-interface.js +10 -0
  129. package/dist/services/tts/provider-interface.js.map +1 -0
  130. package/dist/tools/calculators/desmos-provider.d.ts +54 -0
  131. package/dist/tools/calculators/desmos-provider.d.ts.map +1 -0
  132. package/dist/tools/calculators/desmos-provider.js +384 -0
  133. package/dist/tools/calculators/desmos-provider.js.map +1 -0
  134. package/dist/tools/calculators/mathjs-provider.d.ts +50 -0
  135. package/dist/tools/calculators/mathjs-provider.d.ts.map +1 -0
  136. package/dist/tools/calculators/mathjs-provider.js +660 -0
  137. package/dist/tools/calculators/mathjs-provider.js.map +1 -0
  138. package/dist/tools/calculators/ti-provider.d.ts +122 -0
  139. package/dist/tools/calculators/ti-provider.d.ts.map +1 -0
  140. package/dist/tools/calculators/ti-provider.js +587 -0
  141. package/dist/tools/calculators/ti-provider.js.map +1 -0
  142. package/dist/tools/client.d.ts +17 -0
  143. package/dist/tools/client.d.ts.map +1 -0
  144. package/dist/tools/client.js +26 -0
  145. package/dist/tools/client.js.map +1 -0
  146. package/dist/tools/index.d.ts +8 -0
  147. package/dist/tools/index.d.ts.map +1 -0
  148. package/dist/tools/index.js +10 -0
  149. package/dist/tools/index.js.map +1 -0
  150. package/dist/tools/library-loader.d.ts +63 -0
  151. package/dist/tools/library-loader.d.ts.map +1 -0
  152. package/dist/tools/library-loader.js +292 -0
  153. package/dist/tools/library-loader.js.map +1 -0
  154. package/dist/tools/response-discovery.d.ts +72 -0
  155. package/dist/tools/response-discovery.d.ts.map +1 -0
  156. package/dist/tools/response-discovery.js +183 -0
  157. package/dist/tools/response-discovery.js.map +1 -0
  158. package/dist/tools/tool-coordinator.d.ts +76 -0
  159. package/dist/tools/tool-coordinator.d.ts.map +1 -0
  160. package/dist/tools/tool-coordinator.js +197 -0
  161. package/dist/tools/tool-coordinator.js.map +1 -0
  162. package/dist/tools/types.d.ts +427 -0
  163. package/dist/tools/types.d.ts.map +1 -0
  164. package/dist/tools/types.js +13 -0
  165. package/dist/tools/types.js.map +1 -0
  166. package/dist/tools/variant-resolver.d.ts +48 -0
  167. package/dist/tools/variant-resolver.d.ts.map +1 -0
  168. package/dist/tools/variant-resolver.js +214 -0
  169. package/dist/tools/variant-resolver.js.map +1 -0
  170. package/dist/types/events.d.ts +157 -0
  171. package/dist/types/events.d.ts.map +1 -0
  172. package/dist/types/events.js +11 -0
  173. package/dist/types/events.js.map +1 -0
  174. package/dist/utils/logger.d.ts +7 -0
  175. package/dist/utils/logger.d.ts.map +1 -0
  176. package/dist/utils/logger.js +11 -0
  177. package/dist/utils/logger.js.map +1 -0
  178. package/package.json +71 -0
  179. package/src/README.md +626 -0
  180. package/src/components/QuestionToolBar.svelte +406 -0
  181. package/src/player/AssessmentLayout.svelte +68 -0
  182. package/src/player/README.md +154 -0
  183. package/src/reference-layout/README.md +135 -0
  184. package/src/reference-layout/ReferenceLayout.svelte +209 -0
  185. package/src/reference-layout/components/AssessmentContent.svelte +259 -0
  186. package/src/reference-layout/components/AssessmentFooter.svelte +89 -0
  187. package/src/reference-layout/components/AssessmentHeader.svelte +250 -0
  188. package/src/reference-layout/components/AssessmentNavigation.svelte +134 -0
  189. package/src/reference-layout/components/AssessmentToolsBar.svelte +176 -0
  190. package/src/reference-layout/components/ItemPanel.svelte +200 -0
  191. package/src/reference-layout/components/NotesPanel.svelte +179 -0
  192. package/src/reference-layout/components/PassagePanel.svelte +76 -0
  193. package/src/tools/README.md +616 -0
  194. package/src/tools/calculators/README.md +395 -0
@@ -0,0 +1,965 @@
1
+ /**
2
+ * AssessmentPlayer
3
+ *
4
+ * Reference implementation showing how to wire PIE Assessment Toolkit services
5
+ * together for a complete assessment player.
6
+ *
7
+ * This implementation provides:
8
+ * - Navigation through assessment items (linear or nonlinear)
9
+ * - QTI navigation model (testParts/sections/questionRefs)
10
+ * - Section awareness and rubric block support
11
+ * - Event-driven architecture using TypedEventBus
12
+ * - Session state management
13
+ * - TTS integration via TTSService
14
+ * - Theme support via ThemeProvider
15
+ * - Tool coordination (optional)
16
+ *
17
+ * Products can use this as-is, extend it, or use it as a pattern for their own implementation.
18
+ */
19
+ import { createNewTestSession, createTestSessionIdentifier, getBrowserLocalStorage, loadTestSession, saveTestSession, setCurrentPosition, upsertItemSessionFromPieSessionChange, upsertVisitedItem, } from "../attempt/TestSession";
20
+ import { HighlightCoordinator, I18nService, ThemeProvider, ToolCoordinator, TTSService, TypedEventBus, } from "../index";
21
+ import { AssessmentAuthoringService } from "../services/AssessmentAuthoringService";
22
+ import { ContextVariableStore } from "../services/ContextVariableStore";
23
+ import { PNPToolResolver, } from "../services/PNPToolResolver";
24
+ import { BrowserTTSProvider } from "../services/tts/browser-provider";
25
+ import { DesmosCalculatorProvider, TICalculatorProvider, } from "../tools/client";
26
+ import { buildNavigationStructure, detectAssessmentFormat, getAllQuestionRefs, } from "./qti-navigation";
27
+ export class AssessmentPlayer {
28
+ // Toolkit services
29
+ eventBus;
30
+ ttsService;
31
+ themeProvider = null;
32
+ toolCoordinator;
33
+ highlightCoordinator = null;
34
+ i18nService;
35
+ desmosProvider;
36
+ tiProvider;
37
+ authoringService = null;
38
+ // Configuration
39
+ config;
40
+ // PNP-based tool resolution
41
+ pnpResolver;
42
+ currentTools = [];
43
+ // QTI 3.0 Context Variables
44
+ contextStore;
45
+ // Assessment format and navigation
46
+ assessmentFormat;
47
+ questionRefs;
48
+ navigationStructure;
49
+ navigationMode;
50
+ // State
51
+ currentItemIndex = -1;
52
+ currentItem = null;
53
+ isLoadingItem = false;
54
+ sessionState = { id: "", data: [] };
55
+ // QTI-like attempt (client-only)
56
+ attemptStorage = null;
57
+ testSession = null;
58
+ // TTS state
59
+ ttsInitialized = false;
60
+ ttsSpeaking = false;
61
+ ttsPaused = false;
62
+ ttsRootElement = null;
63
+ // State change listeners
64
+ navigationListeners = new Set();
65
+ itemListeners = new Set();
66
+ loadingListeners = new Set();
67
+ sessionListeners = new Set();
68
+ ttsStateListeners = new Set();
69
+ constructor(config) {
70
+ this.config = config;
71
+ // Detect assessment format and build navigation structure
72
+ this.assessmentFormat = detectAssessmentFormat(config.assessment);
73
+ this.questionRefs = getAllQuestionRefs(config.assessment);
74
+ this.navigationStructure = buildNavigationStructure(config.assessment);
75
+ // Initialize/load QTI-like TestSession (client-only)
76
+ this.attemptStorage = config.attemptStorage || getBrowserLocalStorage();
77
+ this.initializeOrLoadTestSession();
78
+ // Determine navigation mode
79
+ // Priority: explicit config > QTI testPart setting > default to nonlinear
80
+ if (config.navigationMode) {
81
+ this.navigationMode = config.navigationMode;
82
+ }
83
+ else {
84
+ const qtiAssessment = config.assessment;
85
+ this.navigationMode =
86
+ qtiAssessment.testParts?.[0]?.navigationMode || "nonlinear";
87
+ }
88
+ // Initialize toolkit services (with dependency injection support)
89
+ this.eventBus =
90
+ config.services?.eventBus ?? new TypedEventBus();
91
+ this.ttsService = config.services?.ttsService ?? new TTSService();
92
+ this.toolCoordinator =
93
+ config.services?.toolCoordinator ?? new ToolCoordinator();
94
+ this.i18nService = config.services?.i18nService ?? new I18nService();
95
+ this.desmosProvider =
96
+ config.services?.desmosProvider ?? new DesmosCalculatorProvider();
97
+ this.tiProvider = config.services?.tiProvider ?? new TICalculatorProvider();
98
+ // Initialize PNP resolver and tools from assessment
99
+ this.pnpResolver = new PNPToolResolver();
100
+ this.initializeTools();
101
+ this.applyAssessmentTheme();
102
+ this.autoActivateTools();
103
+ // Initialize context variable store from QTI 3.0 contextDeclarations
104
+ this.contextStore = new ContextVariableStore(config.assessment.contextDeclarations);
105
+ this.restoreContextVariables();
106
+ // Initialize highlight coordinator
107
+ // Use injected coordinator, or create new one if supported
108
+ if (config.services?.highlightCoordinator) {
109
+ this.highlightCoordinator = config.services.highlightCoordinator;
110
+ }
111
+ else {
112
+ const tempCoordinator = new HighlightCoordinator();
113
+ if (tempCoordinator.isSupported()) {
114
+ this.highlightCoordinator = tempCoordinator;
115
+ }
116
+ }
117
+ // Connect highlight coordinator to TTS service for word highlighting
118
+ if (this.highlightCoordinator) {
119
+ this.ttsService.setHighlightCoordinator(this.highlightCoordinator);
120
+ }
121
+ // Initialize authoring service if in author mode
122
+ if (config.mode === "author" && config.authoringCallbacks) {
123
+ this.authoringService = new AssessmentAuthoringService(config.assessment, config.authoringCallbacks);
124
+ }
125
+ // Set up event listeners
126
+ this.setupEventListeners();
127
+ // Initialize TTS if enabled via PNP
128
+ if (this.isToolEnabled("pie-tool-text-to-speech")) {
129
+ this.initializeTTS();
130
+ }
131
+ }
132
+ initializeOrLoadTestSession() {
133
+ if (!this.attemptStorage) {
134
+ return;
135
+ }
136
+ const assessmentId = this.config.assessment?.id ||
137
+ this.config.assessment?._id;
138
+ if (!assessmentId) {
139
+ return;
140
+ }
141
+ const { testSessionIdentifier, seed } = createTestSessionIdentifier({
142
+ assessmentId,
143
+ assignmentId: this.config.assignmentId,
144
+ userId: this.config.userId,
145
+ storage: this.attemptStorage,
146
+ });
147
+ const existing = loadTestSession(this.attemptStorage, testSessionIdentifier);
148
+ const currentItemIdentifiers = this.questionRefs.map((q) => q.identifier);
149
+ if (existing) {
150
+ // Keep existing runtime state, but make sure realized order matches current definition
151
+ // (for now we don't implement selection/shuffle, so realization is the current flat order).
152
+ const needsUpdate = !existing.realization?.itemIdentifiers ||
153
+ existing.realization.itemIdentifiers.length !==
154
+ currentItemIdentifiers.length;
155
+ this.testSession = needsUpdate
156
+ ? {
157
+ ...existing,
158
+ realization: {
159
+ ...existing.realization,
160
+ seed: existing.realization?.seed || seed,
161
+ itemIdentifiers: currentItemIdentifiers,
162
+ },
163
+ }
164
+ : existing;
165
+ }
166
+ else {
167
+ this.testSession = createNewTestSession({
168
+ testSessionIdentifier,
169
+ assessmentId,
170
+ seed,
171
+ itemIdentifiers: currentItemIdentifiers,
172
+ });
173
+ }
174
+ // Persist immediately to ensure updatedAt/version exists
175
+ if (this.testSession) {
176
+ saveTestSession(this.attemptStorage, this.testSession);
177
+ }
178
+ }
179
+ /**
180
+ * Set up internal event listeners
181
+ */
182
+ setupEventListeners() {
183
+ // Listen to player events
184
+ this.eventBus.on("player:session-changed", async (e) => {
185
+ const { id, ...sessionData } = e.detail;
186
+ if (id) {
187
+ const existingEntry = this.sessionState.data.find((d) => d.id === id);
188
+ if (existingEntry) {
189
+ Object.assign(existingEntry, sessionData);
190
+ }
191
+ else {
192
+ this.sessionState.data.push({ id, ...sessionData });
193
+ }
194
+ }
195
+ // Notify session listeners
196
+ this.notifySessionListeners();
197
+ // Call product callback
198
+ if (this.config.onSessionChanged) {
199
+ await this.config.onSessionChanged(this.sessionState);
200
+ }
201
+ });
202
+ this.eventBus.on("nav:item-changed", async (e) => {
203
+ if (this.config.onItemChanged) {
204
+ await this.config.onItemChanged(e.detail.currentItemId, e.detail.itemIndex);
205
+ }
206
+ });
207
+ this.eventBus.on("nav:next-requested", async (e) => {
208
+ await this.navigateNext();
209
+ });
210
+ this.eventBus.on("nav:previous-requested", async (e) => {
211
+ await this.navigatePrevious();
212
+ });
213
+ }
214
+ /**
215
+ * Initialize TTS service
216
+ */
217
+ async initializeTTS() {
218
+ try {
219
+ // Initialize TTS with browser provider
220
+ const provider = new BrowserTTSProvider();
221
+ await this.ttsService.initialize(provider, {
222
+ organizationId: this.config.organizationId || undefined,
223
+ });
224
+ this.ttsInitialized = true;
225
+ // Listen to TTS state changes
226
+ this.ttsService.onStateChange("reference-player", (state) => {
227
+ this.ttsSpeaking = state === "playing";
228
+ this.ttsPaused = state === "paused";
229
+ this.notifyTTSStateListeners();
230
+ });
231
+ }
232
+ catch (error) {
233
+ console.error("[AssessmentPlayer] Failed to initialize TTS:", error);
234
+ }
235
+ }
236
+ /**
237
+ * Get event bus for external listeners
238
+ */
239
+ getEventBus() {
240
+ return this.eventBus;
241
+ }
242
+ /**
243
+ * Get current navigation state
244
+ */
245
+ getNavigationState() {
246
+ const totalItems = this.questionRefs.length;
247
+ // Find current section if we have navigation structure
248
+ let currentSection;
249
+ let totalSections = 0;
250
+ if (this.navigationStructure.length > 0 && this.currentItemIndex >= 0) {
251
+ // Count sections (filter out non-section nodes like testParts)
252
+ const sections = this.flattenSections(this.navigationStructure);
253
+ totalSections = sections.length;
254
+ // Find which section contains the current question
255
+ if (totalSections > 0) {
256
+ const currentQuestion = this.questionRefs[this.currentItemIndex];
257
+ if (currentQuestion) {
258
+ for (let i = 0; i < sections.length; i++) {
259
+ const section = sections[i];
260
+ // Check if this section contains the current question
261
+ if (this.sectionContainsQuestion(section, currentQuestion.identifier)) {
262
+ currentSection = {
263
+ id: section.id,
264
+ title: section.title,
265
+ index: i,
266
+ };
267
+ break;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+ return {
274
+ currentIndex: this.currentItemIndex,
275
+ totalItems,
276
+ canNext: this.currentItemIndex < totalItems - 1,
277
+ canPrevious: this.currentItemIndex > 0,
278
+ isLoading: this.isLoadingItem,
279
+ currentSection,
280
+ totalSections: totalSections > 0 ? totalSections : undefined,
281
+ };
282
+ }
283
+ /**
284
+ * Flatten navigation structure to get all sections
285
+ */
286
+ flattenSections(nodes) {
287
+ const sections = [];
288
+ for (const node of nodes) {
289
+ if (node.type === "section") {
290
+ sections.push(node);
291
+ }
292
+ if (node.children) {
293
+ sections.push(...this.flattenSections(node.children));
294
+ }
295
+ }
296
+ return sections;
297
+ }
298
+ /**
299
+ * Check if a section contains a specific question
300
+ */
301
+ sectionContainsQuestion(section, questionIdentifier) {
302
+ if (!section.children) {
303
+ return false;
304
+ }
305
+ for (const child of section.children) {
306
+ if (child.type === "question" &&
307
+ child.identifier === questionIdentifier) {
308
+ return true;
309
+ }
310
+ // Check nested sections
311
+ if (child.type === "section" &&
312
+ this.sectionContainsQuestion(child, questionIdentifier)) {
313
+ return true;
314
+ }
315
+ }
316
+ return false;
317
+ }
318
+ /**
319
+ * Get current item
320
+ */
321
+ getCurrentItem() {
322
+ return this.currentItem;
323
+ }
324
+ /**
325
+ * Get current item config
326
+ */
327
+ getCurrentItemConfig() {
328
+ return this.currentItem?.config;
329
+ }
330
+ /**
331
+ * Get session state
332
+ */
333
+ getSessionState() {
334
+ return this.sessionState;
335
+ }
336
+ /**
337
+ * Get the client-side QTI-like TestSession (attempt) state.
338
+ */
339
+ getTestSession() {
340
+ return this.testSession;
341
+ }
342
+ /**
343
+ * Get player environment
344
+ */
345
+ getEnv() {
346
+ return {
347
+ mode: this.config.mode || "gather",
348
+ };
349
+ }
350
+ /**
351
+ * Get ToolCoordinator for tool management
352
+ */
353
+ getToolCoordinator() {
354
+ return this.toolCoordinator;
355
+ }
356
+ /**
357
+ * Get HighlightCoordinator for annotation/TTS highlighting
358
+ */
359
+ getHighlightCoordinator() {
360
+ return this.highlightCoordinator;
361
+ }
362
+ /**
363
+ * Get ThemeProvider for accessibility theming
364
+ */
365
+ getThemeProvider() {
366
+ return this.themeProvider;
367
+ }
368
+ /**
369
+ * Get I18nService for internationalization
370
+ */
371
+ getI18nService() {
372
+ return this.i18nService;
373
+ }
374
+ /**
375
+ * Get Desmos calculator provider
376
+ */
377
+ getDesmosProvider() {
378
+ return this.desmosProvider;
379
+ }
380
+ /**
381
+ * Get TI calculator provider
382
+ */
383
+ getTIProvider() {
384
+ return this.tiProvider;
385
+ }
386
+ /**
387
+ * Navigate to specific item index
388
+ */
389
+ async navigate(index) {
390
+ if (index < 0 || index >= this.questionRefs.length) {
391
+ return;
392
+ }
393
+ // Stop TTS when navigating
394
+ if (this.ttsSpeaking) {
395
+ this.stopTTS();
396
+ }
397
+ this.isLoadingItem = true;
398
+ this.notifyLoadingListeners(true);
399
+ const previousItemId = this.currentItem?.id || null;
400
+ this.currentItemIndex = index;
401
+ const questionRef = this.questionRefs[index];
402
+ // Persist attempt navigation immediately (even if item fetch fails)
403
+ this.persistAttemptNavigation(questionRef?.identifier, index);
404
+ if (questionRef && questionRef.itemVId) {
405
+ try {
406
+ this.currentItem = await this.config.loadItem(questionRef.itemVId, {
407
+ organizationId: this.config.organizationId ?? null,
408
+ });
409
+ // Emit item-changed event - use questionRef.itemVId for bookmarkability
410
+ this.eventBus.emit("nav:item-changed", {
411
+ previousItemId,
412
+ currentItemId: questionRef.itemVId, // Use itemVId from assessment question for URL bookmarking
413
+ itemIndex: index,
414
+ totalItems: this.questionRefs.length,
415
+ timestamp: Date.now(),
416
+ });
417
+ // Notify item listeners
418
+ this.notifyItemListeners();
419
+ this.notifyNavigationListeners();
420
+ }
421
+ catch (error) {
422
+ console.error("[AssessmentPlayer] Error fetching item:", error);
423
+ this.currentItem = null;
424
+ }
425
+ }
426
+ else {
427
+ this.currentItem = null;
428
+ }
429
+ this.isLoadingItem = false;
430
+ this.notifyLoadingListeners(false);
431
+ this.notifyItemListeners();
432
+ this.notifyNavigationListeners();
433
+ }
434
+ /**
435
+ * Navigate to next item
436
+ */
437
+ async navigateNext() {
438
+ if (this.currentItemIndex < this.questionRefs.length - 1) {
439
+ // In linear mode, prevent skipping items
440
+ // (No additional restrictions needed here - just move to next)
441
+ await this.navigate(this.currentItemIndex + 1);
442
+ }
443
+ }
444
+ /**
445
+ * Navigate to previous item
446
+ */
447
+ async navigatePrevious() {
448
+ // In linear mode, allow going back to previous items
449
+ if (this.currentItemIndex > 0) {
450
+ await this.navigate(this.currentItemIndex - 1);
451
+ }
452
+ }
453
+ /**
454
+ * Start assessment (navigate to first item)
455
+ */
456
+ async start() {
457
+ const startIndexRaw = this.testSession?.navigationState?.currentItemIndex ?? 0;
458
+ const startIndex = startIndexRaw >= 0 ? startIndexRaw : 0;
459
+ await this.navigate(startIndex);
460
+ this.eventBus.emit("assessment:started", {
461
+ assessmentId: this.config.assessment.id || "",
462
+ studentId: "current-student", // TODO: Get from auth context
463
+ timestamp: Date.now(),
464
+ });
465
+ }
466
+ /**
467
+ * TTS: Read current question
468
+ */
469
+ async readQuestion(rootElement) {
470
+ if (!this.ttsInitialized)
471
+ return;
472
+ this.ttsRootElement = rootElement;
473
+ // Get the question prompt
474
+ const promptElement = rootElement.querySelector(".pie-question-prompt, [data-pie-prompt], .prompt");
475
+ if (!promptElement)
476
+ return;
477
+ // Extract text (using same extraction as existing implementation)
478
+ const text = this.extractText(promptElement);
479
+ if (!text.trim())
480
+ return;
481
+ try {
482
+ this.ttsSpeaking = true;
483
+ this.ttsPaused = false;
484
+ this.notifyTTSStateListeners();
485
+ // Set root for highlighting
486
+ this.ttsService.setRootElement?.(promptElement);
487
+ await this.ttsService.speak(text);
488
+ this.ttsSpeaking = false;
489
+ this.ttsPaused = false;
490
+ this.notifyTTSStateListeners();
491
+ }
492
+ catch (error) {
493
+ console.error("[AssessmentPlayer] TTS error:", error);
494
+ this.ttsSpeaking = false;
495
+ this.ttsPaused = false;
496
+ this.notifyTTSStateListeners();
497
+ }
498
+ }
499
+ /**
500
+ * TTS: Toggle play/pause
501
+ */
502
+ toggleTTS(rootElement) {
503
+ if (!this.ttsInitialized)
504
+ return;
505
+ if (this.ttsSpeaking) {
506
+ if (this.ttsPaused) {
507
+ this.ttsService.resume();
508
+ }
509
+ else {
510
+ this.ttsService.pause();
511
+ }
512
+ }
513
+ else if (rootElement) {
514
+ this.readQuestion(rootElement);
515
+ }
516
+ }
517
+ /**
518
+ * TTS: Stop reading
519
+ */
520
+ stopTTS() {
521
+ if (this.ttsSpeaking) {
522
+ this.ttsService.stop();
523
+ this.ttsSpeaking = false;
524
+ this.ttsPaused = false;
525
+ this.notifyTTSStateListeners();
526
+ }
527
+ }
528
+ /**
529
+ * TTS: Get state
530
+ */
531
+ getTTSState() {
532
+ return {
533
+ initialized: this.ttsInitialized,
534
+ speaking: this.ttsSpeaking,
535
+ paused: this.ttsPaused,
536
+ };
537
+ }
538
+ /**
539
+ * Apply theme
540
+ */
541
+ applyTheme(theme) {
542
+ if (!this.themeProvider) {
543
+ this.themeProvider = new ThemeProvider();
544
+ }
545
+ this.themeProvider.applyTheme(theme);
546
+ }
547
+ /**
548
+ * Handle PIE player session-changed event
549
+ * Call this from your component when you receive session-changed from pie-iife-player
550
+ */
551
+ handlePieSessionChanged(detail) {
552
+ this.eventBus.emit("player:session-changed", {
553
+ itemId: this.currentItem?.id || "",
554
+ component: detail.component || "",
555
+ complete: detail.complete || false,
556
+ session: this.sessionState,
557
+ timestamp: Date.now(),
558
+ });
559
+ // Update/persist TestSession itemSessions mapping (QTI item identifier -> PIE session id)
560
+ const pieSessionId = detail?.id;
561
+ const currentQuestion = this.questionRefs[this.currentItemIndex];
562
+ if (this.attemptStorage &&
563
+ this.testSession &&
564
+ currentQuestion?.identifier &&
565
+ pieSessionId) {
566
+ this.testSession = upsertItemSessionFromPieSessionChange(this.testSession, {
567
+ itemIdentifier: currentQuestion.identifier,
568
+ pieSessionId,
569
+ isCompleted: !!detail.complete,
570
+ });
571
+ saveTestSession(this.attemptStorage, this.testSession);
572
+ }
573
+ }
574
+ persistAttemptNavigation(itemIdentifier, index) {
575
+ if (!this.attemptStorage || !this.testSession || !itemIdentifier) {
576
+ return;
577
+ }
578
+ const currentSectionIdentifier = this.findSectionIdentifierForQuestion(itemIdentifier) || undefined;
579
+ this.testSession = setCurrentPosition(upsertVisitedItem(this.testSession, itemIdentifier), {
580
+ currentItemIndex: index,
581
+ currentSectionIdentifier,
582
+ });
583
+ saveTestSession(this.attemptStorage, this.testSession);
584
+ }
585
+ findSectionIdentifierForQuestion(questionIdentifier) {
586
+ const sections = this.flattenSections(this.navigationStructure);
587
+ for (const section of sections) {
588
+ if (this.sectionContainsQuestion(section, questionIdentifier)) {
589
+ return section.identifier;
590
+ }
591
+ }
592
+ return null;
593
+ }
594
+ /**
595
+ * Subscribe to navigation state changes
596
+ */
597
+ onNavigationChange(listener) {
598
+ this.navigationListeners.add(listener);
599
+ return () => this.navigationListeners.delete(listener);
600
+ }
601
+ /**
602
+ * Subscribe to item changes
603
+ */
604
+ onItemChange(listener) {
605
+ this.itemListeners.add(listener);
606
+ return () => this.itemListeners.delete(listener);
607
+ }
608
+ /**
609
+ * Subscribe to loading state changes
610
+ */
611
+ onLoadingChange(listener) {
612
+ this.loadingListeners.add(listener);
613
+ return () => this.loadingListeners.delete(listener);
614
+ }
615
+ /**
616
+ * Subscribe to session changes
617
+ */
618
+ onSessionChange(listener) {
619
+ this.sessionListeners.add(listener);
620
+ return () => this.sessionListeners.delete(listener);
621
+ }
622
+ /**
623
+ * Subscribe to TTS state changes
624
+ */
625
+ onTTSStateChange(listener) {
626
+ this.ttsStateListeners.add(listener);
627
+ return () => this.ttsStateListeners.delete(listener);
628
+ }
629
+ /**
630
+ * Notify listeners
631
+ */
632
+ notifyNavigationListeners() {
633
+ const state = this.getNavigationState();
634
+ this.navigationListeners.forEach((listener) => listener(state));
635
+ }
636
+ notifyItemListeners() {
637
+ this.itemListeners.forEach((listener) => listener(this.currentItem));
638
+ }
639
+ notifyLoadingListeners(isLoading) {
640
+ this.loadingListeners.forEach((listener) => listener(isLoading));
641
+ }
642
+ notifySessionListeners() {
643
+ this.sessionListeners.forEach((listener) => listener(this.sessionState));
644
+ }
645
+ notifyTTSStateListeners() {
646
+ this.ttsStateListeners.forEach((listener) => listener(this.ttsSpeaking, this.ttsPaused));
647
+ }
648
+ /**
649
+ * Extract text from element (simplified)
650
+ * Production would use extractTextFromElement from tts/utils
651
+ */
652
+ extractText(element) {
653
+ // This is simplified - production should use the actual extraction logic
654
+ return element.textContent || "";
655
+ }
656
+ /**
657
+ * Get current section's rubric blocks (passages, instructions)
658
+ */
659
+ getCurrentSectionRubricBlocks() {
660
+ const navState = this.getNavigationState();
661
+ if (!navState.currentSection || this.navigationStructure.length === 0) {
662
+ return [];
663
+ }
664
+ // Find the current section node
665
+ const sections = this.flattenSections(this.navigationStructure);
666
+ const currentSectionNode = sections[navState.currentSection.index];
667
+ if (!currentSectionNode) {
668
+ return [];
669
+ }
670
+ // Extract rubric blocks from the assessment structure
671
+ // This requires accessing the original assessment data
672
+ return this.extractRubricBlocksForSection(currentSectionNode.identifier);
673
+ }
674
+ /**
675
+ * Extract rubric blocks for a specific section identifier
676
+ */
677
+ extractRubricBlocksForSection(sectionIdentifier) {
678
+ const format = this.assessmentFormat;
679
+ if (format === "qti") {
680
+ // Type assertion needed for QTI fields
681
+ const qtiAssessment = this.config.assessment;
682
+ if (!qtiAssessment.testParts) {
683
+ return [];
684
+ }
685
+ // Search through testParts and sections
686
+ for (const testPart of qtiAssessment.testParts) {
687
+ const rubricBlocks = this.findRubricBlocksInSections(testPart.sections, sectionIdentifier);
688
+ if (rubricBlocks.length > 0) {
689
+ return rubricBlocks;
690
+ }
691
+ }
692
+ }
693
+ return [];
694
+ }
695
+ /**
696
+ * Recursively search for rubric blocks in sections
697
+ */
698
+ findRubricBlocksInSections(sections, targetIdentifier) {
699
+ if (!sections) {
700
+ return [];
701
+ }
702
+ for (const section of sections) {
703
+ if (section.identifier === targetIdentifier && section.rubricBlocks) {
704
+ return section.rubricBlocks;
705
+ }
706
+ // Check nested sections
707
+ if (section.sections) {
708
+ const rubricBlocks = this.findRubricBlocksInSections(section.sections, targetIdentifier);
709
+ if (rubricBlocks.length > 0) {
710
+ return rubricBlocks;
711
+ }
712
+ }
713
+ }
714
+ return [];
715
+ }
716
+ /**
717
+ * Navigate to a specific section (goes to first item in section)
718
+ */
719
+ async navigateToSection(sectionIndex) {
720
+ if (!this.config.allowSectionNavigation &&
721
+ this.config.allowSectionNavigation !== undefined) {
722
+ console.warn("[AssessmentPlayer] Section navigation is disabled");
723
+ return;
724
+ }
725
+ const sections = this.flattenSections(this.navigationStructure);
726
+ if (sectionIndex < 0 || sectionIndex >= sections.length) {
727
+ return;
728
+ }
729
+ const targetSection = sections[sectionIndex];
730
+ // Find the first question in this section
731
+ const firstQuestionInSection = this.findFirstQuestionInSection(targetSection);
732
+ if (firstQuestionInSection !== -1) {
733
+ await this.navigate(firstQuestionInSection);
734
+ }
735
+ }
736
+ /**
737
+ * Find the index of the first question in a section
738
+ */
739
+ findFirstQuestionInSection(section) {
740
+ if (!section.children) {
741
+ return -1;
742
+ }
743
+ // Look for first question node
744
+ for (const child of section.children) {
745
+ if (child.type === "question") {
746
+ // Find this question's index in questionRefs
747
+ const index = this.questionRefs.findIndex((q) => q.identifier === child.identifier);
748
+ if (index !== -1) {
749
+ return index;
750
+ }
751
+ }
752
+ // Check nested sections
753
+ if (child.type === "section") {
754
+ const index = this.findFirstQuestionInSection(child);
755
+ if (index !== -1) {
756
+ return index;
757
+ }
758
+ }
759
+ }
760
+ return -1;
761
+ }
762
+ /**
763
+ * Get navigation mode (linear or nonlinear)
764
+ */
765
+ getNavigationMode() {
766
+ return this.navigationMode;
767
+ }
768
+ /**
769
+ * Get all sections for display (e.g., in a section menu)
770
+ */
771
+ getAllSections() {
772
+ const sections = this.flattenSections(this.navigationStructure);
773
+ return sections.map((section, index) => ({
774
+ id: section.id,
775
+ title: section.title,
776
+ index,
777
+ }));
778
+ }
779
+ /**
780
+ * Get the authoring service (only available in author mode)
781
+ */
782
+ getAuthoringService() {
783
+ return this.authoringService;
784
+ }
785
+ /**
786
+ * Enable or disable authoring mode
787
+ */
788
+ setAuthoringMode(enabled) {
789
+ if (enabled && !this.authoringService) {
790
+ this.authoringService = new AssessmentAuthoringService(this.config.assessment, this.config.authoringCallbacks || {});
791
+ this.config.mode = "author";
792
+ }
793
+ else if (!enabled && this.authoringService) {
794
+ this.authoringService = null;
795
+ this.config.mode = "gather";
796
+ }
797
+ }
798
+ // ===== PROFILE-BASED CONFIGURATION =====
799
+ /**
800
+ * Apply an assessment context profile
801
+ */
802
+ /**
803
+ * Initialize tools from assessment PNP
804
+ */
805
+ initializeTools() {
806
+ const assessment = this.config.assessment;
807
+ // Resolve tools from PNP + settings
808
+ this.currentTools = this.pnpResolver.resolveTools(assessment);
809
+ // Register tools with coordinator
810
+ for (const tool of this.currentTools) {
811
+ if (tool.enabled) {
812
+ this.toolCoordinator.registerTool(tool.id, this.humanizeName(tool.id));
813
+ // Configure tool-specific settings
814
+ if (tool.id === "pie-tool-calculator" && tool.settings) {
815
+ this.configureCalculator(tool.settings);
816
+ }
817
+ }
818
+ }
819
+ }
820
+ /**
821
+ * Apply theme from assessment settings
822
+ */
823
+ applyAssessmentTheme() {
824
+ const settings = this.config.assessment.settings;
825
+ const themeConfig = settings?.themeConfig;
826
+ if (themeConfig) {
827
+ this.themeProvider =
828
+ this.config.services?.themeProvider ?? new ThemeProvider();
829
+ this.themeProvider.applyTheme(themeConfig);
830
+ }
831
+ }
832
+ /**
833
+ * Auto-activate tools marked in PNP
834
+ */
835
+ autoActivateTools() {
836
+ const autoActivate = this.pnpResolver.getAutoActivateTools(this.config.assessment);
837
+ for (const toolId of autoActivate) {
838
+ this.toolCoordinator.showTool(toolId);
839
+ }
840
+ }
841
+ /**
842
+ * Configure calculator with settings
843
+ */
844
+ configureCalculator(settings) {
845
+ // Calculator configuration logic
846
+ // (existing logic would be here)
847
+ }
848
+ /**
849
+ * Convert tool ID to human-readable name
850
+ */
851
+ humanizeName(toolId) {
852
+ return toolId
853
+ .replace(/^pie-tool-/, "")
854
+ .replace(/-/g, " ")
855
+ .replace(/\b\w/g, (c) => c.toUpperCase());
856
+ }
857
+ /**
858
+ * Get available tools (resolved from PNP)
859
+ */
860
+ getAvailableTools() {
861
+ return this.currentTools;
862
+ }
863
+ /**
864
+ * Check if a specific tool is enabled
865
+ */
866
+ isToolEnabled(toolId) {
867
+ return this.currentTools.some((t) => t.id === toolId && t.enabled);
868
+ }
869
+ /**
870
+ * Check if a specific tool is required
871
+ */
872
+ isToolRequired(toolId) {
873
+ const tool = this.currentTools.find((t) => t.id === toolId);
874
+ return tool?.required || tool?.alwaysAvailable || false;
875
+ }
876
+ /**
877
+ * Get configuration for a specific tool
878
+ */
879
+ getToolConfig(toolId) {
880
+ return this.currentTools.find((t) => t.id === toolId)?.settings;
881
+ }
882
+ /**
883
+ * Get layout preferences from settings
884
+ */
885
+ getLayoutPreferences() {
886
+ return this.config.assessment.settings?.themeConfig || {};
887
+ }
888
+ // ============================================================================
889
+ // Context Variables (QTI 3.0 Context Declarations)
890
+ // ============================================================================
891
+ /**
892
+ * Get context variable value
893
+ *
894
+ * @param identifier Variable identifier
895
+ * @returns Variable value or undefined
896
+ */
897
+ getContextVariable(identifier) {
898
+ return this.contextStore.get(identifier);
899
+ }
900
+ /**
901
+ * Set context variable value
902
+ *
903
+ * @param identifier Variable identifier
904
+ * @param value New value
905
+ */
906
+ setContextVariable(identifier, value) {
907
+ this.contextStore.set(identifier, value);
908
+ this.persistContextVariables();
909
+ }
910
+ /**
911
+ * Get all context variables as object
912
+ *
913
+ * @returns Object containing all context variables
914
+ */
915
+ getContextVariables() {
916
+ return this.contextStore.toObject();
917
+ }
918
+ /**
919
+ * Reset context variables to default values
920
+ */
921
+ resetContextVariables() {
922
+ this.contextStore.reset();
923
+ this.persistContextVariables();
924
+ }
925
+ /**
926
+ * Restore context variables from session storage
927
+ */
928
+ restoreContextVariables() {
929
+ if (this.testSession?.contextVariables) {
930
+ this.contextStore.fromObject(this.testSession.contextVariables);
931
+ }
932
+ }
933
+ /**
934
+ * Persist context variables to session storage
935
+ */
936
+ persistContextVariables() {
937
+ if (this.testSession && this.attemptStorage) {
938
+ this.testSession = {
939
+ ...this.testSession,
940
+ contextVariables: this.contextStore.toObject(),
941
+ };
942
+ saveTestSession(this.attemptStorage, this.testSession);
943
+ }
944
+ }
945
+ /**
946
+ * Cleanup
947
+ */
948
+ destroy() {
949
+ if (this.ttsInitialized) {
950
+ this.ttsService.offStateChange("reference-player", () => { });
951
+ }
952
+ if (this.themeProvider) {
953
+ this.themeProvider.destroy();
954
+ }
955
+ if (this.highlightCoordinator) {
956
+ this.highlightCoordinator.destroy();
957
+ }
958
+ this.navigationListeners.clear();
959
+ this.itemListeners.clear();
960
+ this.loadingListeners.clear();
961
+ this.sessionListeners.clear();
962
+ this.ttsStateListeners.clear();
963
+ }
964
+ }
965
+ //# sourceMappingURL=AssessmentPlayer.js.map