@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.
- package/README.md +520 -0
- package/dist/attempt/TestSession.d.ts +90 -0
- package/dist/attempt/TestSession.d.ts.map +1 -0
- package/dist/attempt/TestSession.js +174 -0
- package/dist/attempt/TestSession.js.map +1 -0
- package/dist/core/TypedEventBus.d.ts +48 -0
- package/dist/core/TypedEventBus.d.ts.map +1 -0
- package/dist/core/TypedEventBus.js +63 -0
- package/dist/core/TypedEventBus.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/item-loader.d.ts +45 -0
- package/dist/item-loader.d.ts.map +1 -0
- package/dist/item-loader.js +62 -0
- package/dist/item-loader.js.map +1 -0
- package/dist/player/AssessmentPlayer.d.ts +388 -0
- package/dist/player/AssessmentPlayer.d.ts.map +1 -0
- package/dist/player/AssessmentPlayer.js +965 -0
- package/dist/player/AssessmentPlayer.js.map +1 -0
- package/dist/player/index.d.ts +17 -0
- package/dist/player/index.d.ts.map +1 -0
- package/dist/player/index.js +15 -0
- package/dist/player/index.js.map +1 -0
- package/dist/player/navigation-types.d.ts +63 -0
- package/dist/player/navigation-types.d.ts.map +1 -0
- package/dist/player/navigation-types.js +7 -0
- package/dist/player/navigation-types.js.map +1 -0
- package/dist/player/qti-navigation.d.ts +29 -0
- package/dist/player/qti-navigation.d.ts.map +1 -0
- package/dist/player/qti-navigation.js +160 -0
- package/dist/player/qti-navigation.js.map +1 -0
- package/dist/reference-layout/index.d.ts +19 -0
- package/dist/reference-layout/index.d.ts.map +1 -0
- package/dist/reference-layout/index.js +20 -0
- package/dist/reference-layout/index.js.map +1 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +141 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts.map +1 -0
- package/dist/services/AccessibilityCatalogResolver.js +249 -0
- package/dist/services/AccessibilityCatalogResolver.js.map +1 -0
- package/dist/services/AssessmentAuthoringService.d.ts +60 -0
- package/dist/services/AssessmentAuthoringService.d.ts.map +1 -0
- package/dist/services/AssessmentAuthoringService.js +183 -0
- package/dist/services/AssessmentAuthoringService.js.map +1 -0
- package/dist/services/ContextVariableStore.d.ts +114 -0
- package/dist/services/ContextVariableStore.d.ts.map +1 -0
- package/dist/services/ContextVariableStore.js +207 -0
- package/dist/services/ContextVariableStore.js.map +1 -0
- package/dist/services/ElementToolStateStore.d.ts +125 -0
- package/dist/services/ElementToolStateStore.d.ts.map +1 -0
- package/dist/services/ElementToolStateStore.js +200 -0
- package/dist/services/ElementToolStateStore.js.map +1 -0
- package/dist/services/HighlightCoordinator.d.ts +179 -0
- package/dist/services/HighlightCoordinator.d.ts.map +1 -0
- package/dist/services/HighlightCoordinator.js +446 -0
- package/dist/services/HighlightCoordinator.js.map +1 -0
- package/dist/services/I18nService.d.ts +121 -0
- package/dist/services/I18nService.d.ts.map +1 -0
- package/dist/services/I18nService.js +276 -0
- package/dist/services/I18nService.js.map +1 -0
- package/dist/services/PNPMapper.d.ts +71 -0
- package/dist/services/PNPMapper.d.ts.map +1 -0
- package/dist/services/PNPMapper.js +98 -0
- package/dist/services/PNPMapper.js.map +1 -0
- package/dist/services/PNPToolResolver.d.ts +127 -0
- package/dist/services/PNPToolResolver.d.ts.map +1 -0
- package/dist/services/PNPToolResolver.js +222 -0
- package/dist/services/PNPToolResolver.js.map +1 -0
- package/dist/services/SSMLExtractor.d.ts +61 -0
- package/dist/services/SSMLExtractor.d.ts.map +1 -0
- package/dist/services/SSMLExtractor.js +181 -0
- package/dist/services/SSMLExtractor.js.map +1 -0
- package/dist/services/TTSService.d.ts +162 -0
- package/dist/services/TTSService.d.ts.map +1 -0
- package/dist/services/TTSService.js +465 -0
- package/dist/services/TTSService.js.map +1 -0
- package/dist/services/ThemeProvider.d.ts +95 -0
- package/dist/services/ThemeProvider.d.ts.map +1 -0
- package/dist/services/ThemeProvider.js +299 -0
- package/dist/services/ThemeProvider.js.map +1 -0
- package/dist/services/ToolConfigResolver.d.ts +162 -0
- package/dist/services/ToolConfigResolver.d.ts.map +1 -0
- package/dist/services/ToolConfigResolver.js +230 -0
- package/dist/services/ToolConfigResolver.js.map +1 -0
- package/dist/services/ToolCoordinator.d.ts +137 -0
- package/dist/services/ToolCoordinator.d.ts.map +1 -0
- package/dist/services/ToolCoordinator.js +318 -0
- package/dist/services/ToolCoordinator.js.map +1 -0
- package/dist/services/ToolkitCoordinator.d.ts +188 -0
- package/dist/services/ToolkitCoordinator.d.ts.map +1 -0
- package/dist/services/ToolkitCoordinator.js +252 -0
- package/dist/services/ToolkitCoordinator.js.map +1 -0
- package/dist/services/interfaces.d.ts +391 -0
- package/dist/services/interfaces.d.ts.map +1 -0
- package/dist/services/interfaces.js +12 -0
- package/dist/services/interfaces.js.map +1 -0
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +103 -0
- package/dist/services/tool-providers/DesmosToolProvider.d.ts.map +1 -0
- package/dist/services/tool-providers/DesmosToolProvider.js +127 -0
- package/dist/services/tool-providers/DesmosToolProvider.js.map +1 -0
- package/dist/services/tool-providers/IToolProvider.d.ts +137 -0
- package/dist/services/tool-providers/IToolProvider.d.ts.map +1 -0
- package/dist/services/tool-providers/IToolProvider.js +13 -0
- package/dist/services/tool-providers/IToolProvider.js.map +1 -0
- package/dist/services/tool-providers/TIToolProvider.d.ts +107 -0
- package/dist/services/tool-providers/TIToolProvider.d.ts.map +1 -0
- package/dist/services/tool-providers/TIToolProvider.js +123 -0
- package/dist/services/tool-providers/TIToolProvider.js.map +1 -0
- package/dist/services/tool-providers/TTSToolProvider.d.ts +143 -0
- package/dist/services/tool-providers/TTSToolProvider.d.ts.map +1 -0
- package/dist/services/tool-providers/TTSToolProvider.js +168 -0
- package/dist/services/tool-providers/TTSToolProvider.js.map +1 -0
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +165 -0
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -0
- package/dist/services/tool-providers/ToolProviderRegistry.js +240 -0
- package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -0
- package/dist/services/tool-providers/index.d.ts +18 -0
- package/dist/services/tool-providers/index.d.ts.map +1 -0
- package/dist/services/tool-providers/index.js +15 -0
- package/dist/services/tool-providers/index.js.map +1 -0
- package/dist/services/tts/browser-provider.d.ts +24 -0
- package/dist/services/tts/browser-provider.d.ts.map +1 -0
- package/dist/services/tts/browser-provider.js +177 -0
- package/dist/services/tts/browser-provider.js.map +1 -0
- package/dist/services/tts/provider-interface.d.ts +131 -0
- package/dist/services/tts/provider-interface.d.ts.map +1 -0
- package/dist/services/tts/provider-interface.js +10 -0
- package/dist/services/tts/provider-interface.js.map +1 -0
- package/dist/tools/calculators/desmos-provider.d.ts +54 -0
- package/dist/tools/calculators/desmos-provider.d.ts.map +1 -0
- package/dist/tools/calculators/desmos-provider.js +384 -0
- package/dist/tools/calculators/desmos-provider.js.map +1 -0
- package/dist/tools/calculators/mathjs-provider.d.ts +50 -0
- package/dist/tools/calculators/mathjs-provider.d.ts.map +1 -0
- package/dist/tools/calculators/mathjs-provider.js +660 -0
- package/dist/tools/calculators/mathjs-provider.js.map +1 -0
- package/dist/tools/calculators/ti-provider.d.ts +122 -0
- package/dist/tools/calculators/ti-provider.d.ts.map +1 -0
- package/dist/tools/calculators/ti-provider.js +587 -0
- package/dist/tools/calculators/ti-provider.js.map +1 -0
- package/dist/tools/client.d.ts +17 -0
- package/dist/tools/client.d.ts.map +1 -0
- package/dist/tools/client.js +26 -0
- package/dist/tools/client.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/library-loader.d.ts +63 -0
- package/dist/tools/library-loader.d.ts.map +1 -0
- package/dist/tools/library-loader.js +292 -0
- package/dist/tools/library-loader.js.map +1 -0
- package/dist/tools/response-discovery.d.ts +72 -0
- package/dist/tools/response-discovery.d.ts.map +1 -0
- package/dist/tools/response-discovery.js +183 -0
- package/dist/tools/response-discovery.js.map +1 -0
- package/dist/tools/tool-coordinator.d.ts +76 -0
- package/dist/tools/tool-coordinator.d.ts.map +1 -0
- package/dist/tools/tool-coordinator.js +197 -0
- package/dist/tools/tool-coordinator.js.map +1 -0
- package/dist/tools/types.d.ts +427 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +13 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/variant-resolver.d.ts +48 -0
- package/dist/tools/variant-resolver.d.ts.map +1 -0
- package/dist/tools/variant-resolver.js +214 -0
- package/dist/tools/variant-resolver.js.map +1 -0
- package/dist/types/events.d.ts +157 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +11 -0
- package/dist/types/events.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +71 -0
- package/src/README.md +626 -0
- package/src/components/QuestionToolBar.svelte +406 -0
- package/src/player/AssessmentLayout.svelte +68 -0
- package/src/player/README.md +154 -0
- package/src/reference-layout/README.md +135 -0
- package/src/reference-layout/ReferenceLayout.svelte +209 -0
- package/src/reference-layout/components/AssessmentContent.svelte +259 -0
- package/src/reference-layout/components/AssessmentFooter.svelte +89 -0
- package/src/reference-layout/components/AssessmentHeader.svelte +250 -0
- package/src/reference-layout/components/AssessmentNavigation.svelte +134 -0
- package/src/reference-layout/components/AssessmentToolsBar.svelte +176 -0
- package/src/reference-layout/components/ItemPanel.svelte +200 -0
- package/src/reference-layout/components/NotesPanel.svelte +179 -0
- package/src/reference-layout/components/PassagePanel.svelte +76 -0
- package/src/tools/README.md +616 -0
- 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
|