@pie-players/pie-section-player 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 ADDED
@@ -0,0 +1,599 @@
1
+ # @pie-players/pie-section-player
2
+
3
+ A web component for rendering QTI 3.0 assessment sections with passages and items.
4
+
5
+ ## Live Demos
6
+
7
+ 🎯 **[View Interactive Demos](./demos/)** - Working examples with full code
8
+
9
+ - **[TTS Integration Demo](./demos/tts-integration-demo.html)** 🆕 - Assessment Toolkit TTS service integration
10
+ - **[Paired Passages Demo](./demos/paired-passages-urban-gardens.html)** - Complete QTI 3.0 paired passages with PIE elements
11
+ - **[Basic Demo](./demos/basic-demo.html)** - Simple introduction with one passage and three items
12
+ - **[Original Demo](./demo.html)** - Proof-of-concept from initial development
13
+
14
+ ## Installation
15
+
16
+ ### Option 1: CDN (No Build Step)
17
+
18
+ Load directly from a CDN in your browser - no npm install or build step required:
19
+
20
+ ```html
21
+ <!DOCTYPE html>
22
+ <html>
23
+ <head>
24
+ <script type="module">
25
+ // Import from jsdelivr CDN
26
+ import 'https://cdn.jsdelivr.net/npm/@pie-players/pie-section-player/dist/pie-section-player.js';
27
+ import 'https://cdn.jsdelivr.net/npm/@pie-players/pie-esm-player/dist/pie-esm-player.js';
28
+ </script>
29
+ </head>
30
+ <body>
31
+ <pie-section-player id="player"></pie-section-player>
32
+
33
+ <script type="module">
34
+ const player = document.getElementById('player');
35
+ player.section = { /* your section data */ };
36
+ player.mode = 'gather';
37
+ </script>
38
+ </body>
39
+ </html>
40
+ ```
41
+
42
+ **Alternative CDNs:**
43
+
44
+ - **unpkg**: `https://unpkg.com/@pie-players/pie-section-player@latest/dist/pie-section-player.js`
45
+ - **esm.sh**: `https://esm.sh/@pie-players/pie-section-player` (auto-resolves dependencies)
46
+
47
+ ### Option 2: NPM (For Build Tools)
48
+
49
+ Install via npm for use with build tools (Vite, Webpack, etc.):
50
+
51
+ ```bash
52
+ npm install @pie-players/pie-section-player
53
+ # or
54
+ bun add @pie-players/pie-section-player
55
+ ```
56
+
57
+ **Note:** The section player requires the following peer dependencies for tool integration:
58
+
59
+ - `@pie-players/pie-tool-answer-eliminator` - Answer eliminator tool for test-taking strategies
60
+ - `@pie-players/pie-tool-tts-inline` - Inline TTS tool for accessibility
61
+
62
+ These are automatically resolved when using the section player as a library dependency.
63
+
64
+ ## Usage
65
+
66
+ ### As Web Component (Vanilla JS/HTML)
67
+
68
+ ```html
69
+ <!DOCTYPE html>
70
+ <html>
71
+ <head>
72
+ <script type="module">
73
+ import '@pie-players/pie-section-player';
74
+ import '@pie-players/pie-esm-player'; // Required for rendering passages/items
75
+ </script>
76
+ </head>
77
+ <body>
78
+ <pie-section-player id="player"></pie-section-player>
79
+
80
+ <script type="module">
81
+ const player = document.getElementById('player');
82
+
83
+ // Set section data
84
+ player.section = {
85
+ identifier: 'section-1',
86
+ keepTogether: true, // Page mode: all items visible
87
+ rubricBlocks: [
88
+ {
89
+ view: 'candidate',
90
+ use: 'passage',
91
+ passage: {
92
+ id: 'passage-1',
93
+ name: 'Reading Passage',
94
+ config: {
95
+ markup: '<reading-passage id="p1"></reading-passage>',
96
+ elements: {
97
+ 'reading-passage': '@pie-element/reading-passage@latest'
98
+ },
99
+ models: [{
100
+ id: 'p1',
101
+ element: 'reading-passage',
102
+ content: '<p>Your passage content...</p>'
103
+ }]
104
+ }
105
+ }
106
+ }
107
+ ],
108
+ assessmentItemRefs: [
109
+ {
110
+ identifier: 'q1',
111
+ item: {
112
+ id: 'item-1',
113
+ name: 'Question 1',
114
+ config: {
115
+ markup: '<multiple-choice id="mc1"></multiple-choice>',
116
+ elements: {
117
+ 'multiple-choice': '@pie-element/multiple-choice@latest'
118
+ },
119
+ models: [{
120
+ id: 'mc1',
121
+ element: 'multiple-choice',
122
+ prompt: 'What is 2 + 2?',
123
+ choices: [/*...*/]
124
+ }]
125
+ }
126
+ }
127
+ }
128
+ ]
129
+ };
130
+
131
+ // Set mode
132
+ player.mode = 'gather'; // or 'view', 'evaluate', 'author'
133
+
134
+ // Listen to events
135
+ player.addEventListener('section-loaded', (e) => {
136
+ console.log('Section loaded:', e.detail);
137
+ });
138
+
139
+ player.addEventListener('item-changed', (e) => {
140
+ console.log('Item changed:', e.detail);
141
+ });
142
+ </script>
143
+ </body>
144
+ </html>
145
+ ```
146
+
147
+ ### As Svelte Component
148
+
149
+ ```svelte
150
+ <script>
151
+ import PieSectionPlayer from '@pie-players/pie-section-player';
152
+
153
+ let section = {
154
+ identifier: 'section-1',
155
+ keepTogether: false, // Item mode: one at a time
156
+ assessmentItemRefs: [/* items */]
157
+ };
158
+ </script>
159
+
160
+ <PieSectionPlayer
161
+ {section}
162
+ mode="gather"
163
+ view="candidate"
164
+ bundleHost="https://cdn.pie.org"
165
+ />
166
+ ```
167
+
168
+ ### In React
169
+
170
+ ```jsx
171
+ import '@pie-players/pie-section-player';
172
+
173
+ function AssessmentSection({ section }) {
174
+ const playerRef = useRef(null);
175
+
176
+ useEffect(() => {
177
+ if (playerRef.current) {
178
+ playerRef.current.section = section;
179
+ playerRef.current.mode = 'gather';
180
+ }
181
+ }, [section]);
182
+
183
+ return <pie-section-player ref={playerRef} />;
184
+ }
185
+ ```
186
+
187
+ ## Props/Attributes
188
+
189
+ | Prop | Type | Default | Description |
190
+ |------|------|---------|-------------|
191
+ | `section` | `QtiAssessmentSection` | `null` | Section data with passages and items |
192
+ | `mode` | `'gather' \| 'view' \| 'evaluate' \| 'author'` | `'gather'` | Player mode |
193
+ | `view` | `'candidate' \| 'scorer' \| 'author' \| ...` | `'candidate'` | Current view (filters rubricBlocks) |
194
+ | `item-sessions` | `Record<string, any>` | `{}` | Item sessions for restoration |
195
+ | `bundle-host` | `string` | `''` | CDN host for PIE bundles |
196
+ | `esm-cdn-url` | `string` | `'https://esm.sh'` | ESM CDN URL |
197
+ | `custom-classname` | `string` | `''` | Custom CSS class |
198
+ | `debug` | `string \| boolean` | `''` | Debug mode |
199
+
200
+ ## Events
201
+
202
+ ### `section-loaded`
203
+
204
+ Fired when the section is loaded and ready.
205
+
206
+ ```javascript
207
+ player.addEventListener('section-loaded', (e) => {
208
+ console.log(e.detail);
209
+ // {
210
+ // sectionId: 'section-1',
211
+ // itemCount: 3,
212
+ // passageCount: 2,
213
+ // isPageMode: true
214
+ // }
215
+ });
216
+ ```
217
+
218
+ ### `item-changed`
219
+
220
+ Fired when the current item changes (item mode only).
221
+
222
+ ```javascript
223
+ player.addEventListener('item-changed', (e) => {
224
+ console.log(e.detail);
225
+ // {
226
+ // previousItemId: 'item-1',
227
+ // currentItemId: 'item-2',
228
+ // itemIndex: 1,
229
+ // totalItems: 3,
230
+ // timestamp: 1234567890
231
+ // }
232
+ });
233
+ ```
234
+
235
+ ### `section-complete`
236
+
237
+ Fired when all items in the section are completed.
238
+
239
+ ### `player-error`
240
+
241
+ Fired when an error occurs.
242
+
243
+ ## Performance Optimization
244
+
245
+ The section player automatically optimizes element loading for sections with multiple items using the same PIE elements.
246
+
247
+ ### Element Aggregation
248
+
249
+ **Problem**: When rendering multiple items that use the same PIE elements (e.g., multiple multiple-choice questions), the old approach would load the same element bundle multiple times.
250
+
251
+ **Solution**: The section player now:
252
+ 1. Aggregates all unique elements from all items before rendering
253
+ 2. Loads each element bundle once
254
+ 3. Items initialize from the pre-loaded registry
255
+
256
+ **Benefits:**
257
+ - **50%+ faster loading** for sections with repeated elements
258
+ - **Fewer network requests** - one bundle request per unique element (not per item)
259
+ - **Automatic** - no configuration needed, works out of the box
260
+ - **Backward compatible** - existing code works without changes
261
+
262
+ ### Example Performance
263
+
264
+ Section with 5 items (3 multiple-choice, 2 hotspot):
265
+
266
+ **Before**: 5 loader calls → 2 unique + 3 cached = ~550ms
267
+ **After**: 1 loader call → 2 unique = ~250ms
268
+
269
+ **50% faster load time**
270
+
271
+ ### Element Version Conflicts
272
+
273
+ If items require different versions of the same element, an error is thrown:
274
+
275
+ ```
276
+ Element version conflict: pie-multiple-choice requires both
277
+ @pie-element/multiple-choice@10.0.0 and @pie-element/multiple-choice@11.0.1
278
+ ```
279
+
280
+ **Solution**: Normalize element versions across items in your content authoring system.
281
+
282
+ ### Technical Details
283
+
284
+ For implementation details and architecture, see:
285
+ - [Element Loader Design](../../docs/architecture/ELEMENT_LOADER_DESIGN.md)
286
+ - [Generalized Loader Architecture](../../docs/architecture/GENERALIZED_LOADER_ARCHITECTURE.md)
287
+
288
+ ## Rendering Modes
289
+
290
+ ### Page Mode (`keepTogether: true`)
291
+
292
+ All items and passages are visible simultaneously. Ideal for:
293
+ - Paired passages
294
+ - Multi-item pages
295
+ - Print assessments
296
+
297
+ ```javascript
298
+ section.keepTogether = true;
299
+ ```
300
+
301
+ ### Item Mode (`keepTogether: false`)
302
+
303
+ One item at a time with navigation controls. Ideal for:
304
+ - Linear assessments
305
+ - Adaptive tests
306
+ - Item-by-item delivery
307
+
308
+ ```javascript
309
+ section.keepTogether = false;
310
+ ```
311
+
312
+ ## Methods
313
+
314
+ ### `navigateNext()`
315
+
316
+ Navigate to the next item (item mode only).
317
+
318
+ ```javascript
319
+ player.navigateNext();
320
+ ```
321
+
322
+ ### `navigatePrevious()`
323
+
324
+ Navigate to the previous item (item mode only).
325
+
326
+ ```javascript
327
+ player.navigatePrevious();
328
+ ```
329
+
330
+ ### `getNavigationState()`
331
+
332
+ Get the current navigation state.
333
+
334
+ ```javascript
335
+ const state = player.getNavigationState();
336
+ // {
337
+ // currentIndex: 0,
338
+ // totalItems: 3,
339
+ // canNext: true,
340
+ // canPrevious: false,
341
+ // isLoading: false
342
+ // }
343
+ ```
344
+
345
+ ## Passage Handling
346
+
347
+ The section player extracts passages from two sources:
348
+
349
+ 1. **Section-level passages** (QTI 3.0 style) - `rubricBlocks` where `use="passage"`
350
+ 2. **Item-linked passages** (legacy PIE) - `item.passage`
351
+
352
+ Passages are automatically deduplicated by ID.
353
+
354
+ ## Assessment Toolkit Integration
355
+
356
+ The section player integrates with the [PIE Assessment Toolkit](../assessment-toolkit/) for centralized service management via **ToolkitCoordinator**.
357
+
358
+ ### Using ToolkitCoordinator (Recommended)
359
+
360
+ The **ToolkitCoordinator** provides a single entry point for all toolkit services, simplifying initialization:
361
+
362
+ ```javascript
363
+ import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
364
+
365
+ // Create coordinator with configuration
366
+ const coordinator = new ToolkitCoordinator({
367
+ assessmentId: 'my-assessment',
368
+ tools: {
369
+ tts: { enabled: true, defaultVoice: 'en-US' },
370
+ answerEliminator: { enabled: true }
371
+ },
372
+ accessibility: {
373
+ catalogs: assessment.accessibilityCatalogs || [],
374
+ language: 'en-US'
375
+ }
376
+ });
377
+
378
+ // Pass to section player as JavaScript property
379
+ const player = document.getElementById('player');
380
+ player.toolkitCoordinator = coordinator;
381
+ player.section = mySection;
382
+ ```
383
+
384
+ **Benefits:**
385
+ - **Single initialization point**: One coordinator instead of 5+ services
386
+ - **Centralized configuration**: Tool settings in one place
387
+ - **Automatic service wiring**: Services work together automatically
388
+ - **Element-level tool state**: Answer eliminations, highlights tracked per element
389
+ - **State separation**: Tool state (ephemeral) separate from session data (persistent)
390
+
391
+ ### What the Coordinator Provides
392
+
393
+ The coordinator owns and orchestrates all toolkit services:
394
+
395
+ ```typescript
396
+ coordinator.ttsService // Text-to-speech service
397
+ coordinator.toolCoordinator // Tool visibility and z-index management
398
+ coordinator.highlightCoordinator // TTS and annotation highlights
399
+ coordinator.elementToolStateStore // Element-level tool state (answer eliminator, etc.)
400
+ coordinator.catalogResolver // QTI 3.0 accessibility catalogs for SSML
401
+ ```
402
+
403
+ ### Automatic Features
404
+
405
+ When using the coordinator, the section player automatically:
406
+
407
+ 1. **Extracts services** from the coordinator
408
+ 2. **Generates section ID** (from `section.identifier` or auto-generated)
409
+ 3. **Passes IDs + services** through component hierarchy
410
+ 4. **Extracts SSML** from embedded `<speak>` tags
411
+ 5. **Manages catalog lifecycle** (add on item load, clear on navigation)
412
+ 6. **Renders TTS tools** in passage/item headers
413
+ 7. **Tracks element-level state** with global uniqueness
414
+
415
+ ### Standalone Sections (No Coordinator)
416
+
417
+ If no coordinator is provided, the section player creates a default one:
418
+
419
+ ```javascript
420
+ // No coordinator provided - section player creates default
421
+ const player = document.getElementById('player');
422
+ player.section = mySection;
423
+
424
+ // Internally creates:
425
+ // new ToolkitCoordinator({
426
+ // assessmentId: 'anon_...', // auto-generated
427
+ // tools: { tts: { enabled: true }, answerEliminator: { enabled: true } }
428
+ // })
429
+ ```
430
+
431
+ ### Advanced: Manual Service Integration (Legacy)
432
+
433
+ For backward compatibility or advanced scenarios, you can still pass services individually:
434
+
435
+ ```javascript
436
+ import {
437
+ TTSService,
438
+ BrowserTTSProvider,
439
+ ToolCoordinator,
440
+ HighlightCoordinator,
441
+ AccessibilityCatalogResolver,
442
+ ElementToolStateStore
443
+ } from '@pie-players/pie-assessment-toolkit';
444
+
445
+ // Initialize each service
446
+ const ttsService = new TTSService();
447
+ const toolCoordinator = new ToolCoordinator();
448
+ const highlightCoordinator = new HighlightCoordinator();
449
+ const elementToolStateStore = new ElementToolStateStore();
450
+ const catalogResolver = new AccessibilityCatalogResolver([], 'en-US');
451
+
452
+ await ttsService.initialize(new BrowserTTSProvider());
453
+ ttsService.setCatalogResolver(catalogResolver);
454
+
455
+ // Pass services individually
456
+ const player = document.getElementById('player');
457
+ player.ttsService = ttsService;
458
+ player.toolCoordinator = toolCoordinator;
459
+ player.highlightCoordinator = highlightCoordinator;
460
+ player.elementToolStateStore = elementToolStateStore;
461
+ player.catalogResolver = catalogResolver;
462
+ ```
463
+
464
+ **Note:** Using ToolkitCoordinator is recommended for most use cases.
465
+
466
+ ### SSML Extraction
467
+
468
+ ✅ **Automatic SSML Extraction**: The section player automatically extracts embedded `<speak>` tags from item and passage content, converting them into QTI 3.0 accessibility catalogs at runtime.
469
+
470
+ **Benefits:**
471
+ - Authors embed SSML directly in content (no separate catalog files)
472
+ - Proper pronunciation of technical terms and math expressions
473
+ - Emphasis and pacing control via SSML
474
+ - Automatic catalog generation and registration
475
+
476
+ **Example:**
477
+
478
+ Authors create content with embedded SSML:
479
+ ```typescript
480
+ {
481
+ config: {
482
+ models: [{
483
+ prompt: `<div>
484
+ <speak>Solve <prosody rate="slow">x squared, plus two x</prosody>.</speak>
485
+ <p>Solve x² + 2x = 0</p>
486
+ </div>`
487
+ }]
488
+ }
489
+ }
490
+ ```
491
+
492
+ At runtime, the section player:
493
+ 1. Extracts SSML and generates catalog entry
494
+ 2. Cleans visual markup (removes SSML tags)
495
+ 3. Adds `data-catalog-id` attribute for TTS lookup
496
+ 4. Registers catalog with AccessibilityCatalogResolver
497
+
498
+ **Result:** TTS uses proper math pronunciation while visual display shows clean HTML.
499
+
500
+ See [TTS-INTEGRATION.md](./TTS-INTEGRATION.md) for complete details.
501
+
502
+ ### Element-Level Tool State
503
+
504
+ Tool state (answer eliminations, highlights, etc.) is tracked at the **element level** using globally unique composite keys:
505
+
506
+ **Format**: `${assessmentId}:${sectionId}:${itemId}:${elementId}`
507
+
508
+ **Example**: `"demo-assessment:section-1:question-1:mc1"`
509
+
510
+ **Benefits:**
511
+ - Each PIE element has independent tool state
512
+ - No cross-item contamination
513
+ - Persists across section navigation
514
+ - Separate from PIE session data (not sent to server)
515
+
516
+ ### Service Flow
517
+
518
+ Services are passed through the component hierarchy with IDs:
519
+
520
+ ```
521
+ SectionPlayer (coordinator → services + assessmentId + sectionId)
522
+ ↓
523
+ PageModeLayout / ItemModeLayout (passes assessmentId + sectionId + services)
524
+ ↓
525
+ PassageRenderer / ItemRenderer (uses item.id, passes assessmentId + sectionId + services)
526
+ ↓
527
+ QuestionToolBar (generates globalElementId)
528
+ ↓
529
+ Tool web components (use globalElementId for state)
530
+ ```
531
+
532
+ ### Demos
533
+
534
+ See the [section-demos](../../apps/section-demos/) for complete examples:
535
+
536
+ - **Three Questions Demo**: Element-level answer eliminator with ToolkitCoordinator
537
+ - **TTS Integration Demo**: Coordinator with TTS service
538
+ - **Paired Passages Demo**: Multi-section with cross-section state persistence
539
+
540
+ ## Styling
541
+
542
+ The web component uses Shadow DOM mode `'none'`, so you can style it with global CSS:
543
+
544
+ ```css
545
+ .pie-section-player {
546
+ max-width: 1200px;
547
+ margin: 0 auto;
548
+ }
549
+
550
+ .pie-section-player .section-passages {
551
+ background: #f5f5f5;
552
+ padding: 1rem;
553
+ }
554
+
555
+ .pie-section-player .item-container {
556
+ border: 1px solid #ddd;
557
+ margin-bottom: 1rem;
558
+ }
559
+ ```
560
+
561
+ ## CDN Usage
562
+
563
+ ```html
564
+ <script type="module">
565
+ import 'https://cdn.jsdelivr.net/npm/@pie-players/pie-section-player/dist/pie-section-player.js';
566
+ </script>
567
+
568
+ <pie-section-player id="player"></pie-section-player>
569
+ ```
570
+
571
+ ## TypeScript
572
+
573
+ Full TypeScript support included:
574
+
575
+ ```typescript
576
+ import type { QtiAssessmentSection } from '@pie-players/pie-players-shared/types';
577
+ import PieSectionPlayer from '@pie-players/pie-section-player';
578
+
579
+ const section: QtiAssessmentSection = {
580
+ identifier: 'section-1',
581
+ keepTogether: true,
582
+ // ...
583
+ };
584
+
585
+ const player = document.querySelector('pie-section-player') as PieSectionPlayer;
586
+ player.section = section;
587
+ ```
588
+
589
+ ## Browser Support
590
+
591
+ - Chrome/Edge 90+
592
+ - Firefox 88+
593
+ - Safari 14+
594
+
595
+ Requires ES2020+ support (native ES modules, optional chaining, nullish coalescing).
596
+
597
+ ## License
598
+
599
+ MIT
@@ -0,0 +1,2 @@
1
+ export * from './pie-section-player'
2
+ export {}
@@ -0,0 +1,2 @@
1
+ export { default as PieSectionPlayer } from './PieSectionPlayer.svelte';
2
+ //# sourceMappingURL=pie-section-player.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pie-section-player.d.ts","sourceRoot":"","sources":["../src/pie-section-player.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAIxE,OAAO,2BAA2B,CAAC;AAInC,OAAO,yCAAyC,CAAC;AACjD,OAAO,kCAAkC,CAAC;AAC1C,OAAO,yCAAyC,CAAC;AACjD,OAAO,kCAAkC,CAAC"}