@pie-qti/item-player
v0.1.1
Published
Modern QTI player supporting QTI 2.x and 3.0 - Client-side with optional backend hooks
Readme
@pie-qti/item-player
A modern, QTI 2.x compliant assessment item player with role-based rendering and optional backend scoring support.
Features
- ✅ 21 QTI Interaction Types Supported - All QTI 2.2 interactions
- ✅ QTI 2.x Standard Roles - Candidate, scorer, author, tutor, proctor, testConstructor
- ✅ Adaptive Items - Multi-attempt workflow with progressive feedback
- ✅ Full Keyboard Accessibility (follows WCAG 2.2 Level AA guidelines)
- ✅ Client-Side Response Processing with 45/45 QTI operators
- ✅ Framework-Agnostic Core with a Web Component interaction layer (default implementations authored in Svelte)
- ✅ TypeScript with full type definitions
- ✅ Minimal Dependencies (node-html-parser for XML parsing)
- ✅ Iframe mode (reference) - Optional host helper + postMessage protocol for iframe-isolated deployments
Installation
bun add @pie-qti/item-playerAlternatively:
npm install @pie-qti/item-playerQuick Start
Core Player (Framework-Agnostic)
import { Player } from '@pie-qti/item-player';
// Load QTI XML with role
const qtiXml = `<assessmentItem ...>...</assessmentItem>`;
const player = new Player({
itemXml: qtiXml,
role: 'candidate' // QTI 2.x standard role
});
// Get interactions
const interactions = player.getInteractions();
// [{ type: 'choiceInteraction', responseIdentifier: 'RESPONSE', element: ... }]
// Set response
player.setResponse('RESPONSE', 'A');
// Score the item
const result = player.score();
console.log(result.score); // 0.0 to 1.0
console.log(result.outcomes); // { SCORE: 1.0, ... }Iframe mode (optional, reference)
For deployments that need stronger isolation for untrusted QTI, you can run the player inside an iframe and communicate via a versioned postMessage protocol.
- Docs:
docs/iframe-mode.md - Import:
@pie-qti/item-player/iframe(browser-only)
QTI 2.x Standard Roles
The player implements QTI 2.x standard roles to control rendering behavior:
| Role | Behavior |
|------|----------|
| candidate | Test-taker - inputs editable, no correct answers shown |
| scorer | Grader - inputs readonly, correct answers shown |
| author | Content author - inputs readonly, correct answers shown |
| tutor | Instructional mode - inputs readonly, correct answers shown |
| proctor | Test administrator - inputs readonly, limited feedback |
| testConstructor | Test developer - inputs readonly, correct answers shown |
Role-Based Rendering
// For test-takers (editable inputs)
const player = new Player({
itemXml: qtiXml,
role: 'candidate'
});
// For grading/review (readonly, show answers)
const player = new Player({
itemXml: qtiXml,
role: 'scorer'
});
// For instructional use (readonly, show answers)
const player = new Player({
itemXml: qtiXml,
role: 'tutor'
});Supported Interaction Types
| QTI Type | Component | Description |
|----------|-----------|-------------|
| choiceInteraction | Built-in | Single/multiple choice |
| textEntryInteraction | Built-in | Short text input |
| extendedTextInteraction | Built-in | Long-form text (essay) |
| inlineChoiceInteraction | InlineInteractionRenderer | Dropdown in text |
| orderInteraction | SortableList | Drag to reorder items |
| matchInteraction | MatchDragDrop | Match items between sets |
| associateInteraction | - | Create associations |
| gapMatchInteraction | - | Drag items into text gaps |
| graphicGapMatchInteraction | GraphicGapMatch | Drag labels onto image hotspots |
| sliderInteraction | Built-in | Slider input |
| hotspotInteraction | - | Click regions on image |
| uploadInteraction | FileUpload | Upload a file response |
| drawingInteraction | DrawingCanvas | Draw/annotate on a canvas |
| customInteraction | CustomInteractionFallback | Fallback display + optional manual response |
QTI Version Support
This player is designed for QTI 2.2 but provides best-effort support for QTI 2.0 and 2.1:
- QTI 2.2: Full support (recommended)
- QTI 2.1: Supported with CC2 template aliases (
cc2_match,cc2_map_response, etc.) - QTI 2.0: Limited support - core interactions work, but some features may be unsupported
Version Detection
The player automatically detects QTI version from:
- Namespace URI (
xmlns="http://www.imsglobal.org/xsd/imsqti_v2pX") - Version attribute (
<assessmentItem version="2.X">)
Warnings are logged for older versions or when version cannot be detected.
Template Processing
Response processing templates are namespace-agnostic. The player supports:
- Standard QTI 2.2 templates:
match_correct,map_response, etc. - QTI 2.1 CC2 aliases:
cc2_match,cc2_map_response, etc.
Strict Compliance Mode
For strict QTI 2.2 validation, enable strict compliance:
const player = new Player({
itemXml: qtiXml,
strictQtiCompliance: {
enabled: true, // Enable strict QTI 2.2 validation
rejectUnknownExtensions: true, // Throw errors on non-2.2 versions
logDeviations: true // Log warnings for spec deviations
}
});See QTI Compliance Documentation for details.
Architecture
Three-Layer Design
┌─────────────────────────────────────────┐
│ Interaction UI (Web Components) │
│ - default implementations authored in │
│ Svelte (`@pie-qti/default-components`) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Extraction │
│ - Player.getInteractionData() │
│ - ExtractionRegistry + extractors │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Core Player (Framework-Agnostic) │
│ - XML parsing │
│ - Response processing │
│ - Scoring engine │
│ - Role-based behavior │
└─────────────────────────────────────────┘API Reference
Player Class
interface PlayerConfig {
itemXml: string;
role?: QTIRole; // 'candidate' | 'scorer' | 'author' | 'tutor' | 'proctor' | 'testConstructor'
componentRegistry?: ComponentRegistry; // Custom web component registry
extractionRegistry?: ExtractionRegistry; // Custom extractors
plugins?: QTIPlugin[]; // Plugin-based extension point
}
class Player {
constructor(config: PlayerConfig)
// Get all interactions in the item
getInteractions(): Array<{
type: string;
responseIdentifier: string;
element: HTMLElement;
}>
// Get extracted interaction data (recommended API for UIs)
getInteractionData(): InteractionData[]
// Set a response value
setResponse(identifier: string, value: any): void
// Get a response value
getResponse(identifier: string): any
// Execute response processing and calculate score
score(): {
score: number; // 0.0 to 1.0
outcomes: Record<string, any>;
completed: boolean;
}
// Get item body HTML
getItemBodyHtml(): string
// Get rubric blocks for current role
getRubricBlocks(): RubricBlock[]
}Component Props
SortableList
interface Props {
items: Array<{ id: string; text: string }>;
orderedIds: string[];
orientation?: 'vertical' | 'horizontal';
disabled?: boolean;
onReorder: (newOrder: string[]) => void;
}MatchDragDrop
interface Props {
sourceSet: Array<{ identifier: string; text: string }>;
targetSet: Array<{ identifier: string; text: string }>;
pairs: string[]; // "sourceId targetId"
disabled?: boolean;
onPairsChange: (newPairs: string[]) => void;
}GraphicGapMatch
interface Props {
gapTexts: Array<{ identifier: string; text: string; matchMax: number }>;
hotspots: Array<{ identifier: string; shape: string; coords: string; matchMax: number }>;
pairs: string[]; // "gapTextId hotspotId"
imageData: { type: 'svg' | 'image'; content?: string; src?: string; width: string; height: string };
disabled?: boolean;
onPairsChange: (newPairs: string[]) => void;
}Extensibility
Use plugins/registries (QTIPlugin, ExtractionRegistry, ComponentRegistry) to support vendor extensions or custom rendering.
Important: interaction implementations are treated as web components by the player. The player creates elements by tag name and communicates via standard properties + the bubbling qti-change event. Svelte is an implementation detail of the default component package, not a requirement.
For details, see PLUGGABILITY.md.
Adaptive Items
The player fully supports QTI 2.2 adaptive items with multi-attempt workflows:
Basic Adaptive Item
import { Player } from '@pie-qti/item-player';
const player = new Player({
itemXml: qtiXml, // With adaptive="true"
role: 'candidate'
});
// Check if item is adaptive
if (player.isAdaptive()) {
// Submit first attempt
player.setResponses({ RESPONSE: 'A' });
const result = player.submitAttempt();
console.log(result.numAttempts); // 1
console.log(result.completionStatus); // 'unknown' or 'completed'
console.log(result.canContinue); // true (if not completed)
console.log(result.modalFeedback); // Progressive feedback/hints
// Continue submitting until completed
while (!player.isCompleted()) {
player.setResponses({ RESPONSE: nextGuess });
const nextResult = player.submitAttempt();
// Show feedback, allow retry
}
}Hint Buttons (countAttempt="false")
Adaptive items can have hint buttons that don't increment the attempt counter:
<endAttemptInteraction responseIdentifier="HINT"
title="Request Hint"
countAttempt="false"/>// Click hint button (doesn't increment attempts)
player.setResponses({ HINT: true });
const hint = player.submitAttempt(false); // Pass false for countAttempt
console.log(hint.numAttempts); // Unchanged
console.log(hint.modalFeedback); // Hint contentProgressive Feedback Example
<assessmentItem identifier="adaptive-item" adaptive="true">
<!-- ... interactions ... -->
<responseProcessing>
<responseCondition>
<responseIf>
<!-- First attempt: encourage -->
<lt>
<variable identifier="numAttempts"/>
<baseValue baseType="integer">2</baseValue>
</lt>
<setOutcomeValue identifier="FEEDBACK">
<baseValue baseType="identifier">tryagain</baseValue>
</setOutcomeValue>
</responseIf>
<responseElse>
<!-- Second+ attempt: show answer -->
<setOutcomeValue identifier="FEEDBACK">
<baseValue baseType="identifier">answer</baseValue>
</setOutcomeValue>
<setOutcomeValue identifier="completionStatus">
<baseValue baseType="identifier">completed</baseValue>
</setOutcomeValue>
</responseElse>
</responseCondition>
</responseProcessing>
<modalFeedback outcomeIdentifier="FEEDBACK" identifier="tryagain" showHide="show">
<p>Try again! You have one more attempt.</p>
</modalFeedback>
<modalFeedback outcomeIdentifier="FEEDBACK" identifier="answer" showHide="show">
<p>The correct answer is C. Here's why...</p>
</modalFeedback>
</assessmentItem>API Methods
| Method | Description |
| ---------------------------------------------------------------- | ----------------------------------------- |
| isAdaptive(): boolean | Check if item has adaptive="true" |
| isCompleted(): boolean | Check if completionStatus="completed" |
| getNumAttempts(): number | Get current attempt count |
| submitAttempt(countAttempt?: boolean): AdaptiveAttemptResult | Submit attempt with optional counting |
For complete documentation, see ADAPTIVE-ITEMS-DESIGN.md.
Accessibility
All components follow WCAG 2.2 Level AA guidelines:
- ✅ Full keyboard navigation (Tab, Space, Enter, Arrow keys, Escape)
- ✅ Screen reader support with ARIA labels and live regions
- ✅ Focus management with visible indicators
- ✅ Alternative to drag-and-drop for all interactions
Keyboard Shortcuts
| Component | Keys | Action | |-----------|------|--------| | SortableList | Space/Enter | Grab/drop item | | | Arrow keys | Move grabbed item | | | Escape | Cancel | | MatchDragDrop | Space/Enter | Select source | | | Tab → Space/Enter | Match to target | | GraphicGapMatch | Space/Enter | Select label | | | Tab → Space/Enter | Place on hotspot |
For detailed accessibility analysis, see WCAG-2.2-COMPLIANCE.md.
Backend Scoring (Optional)
The player supports optional backend scoring for items that require server-side processing:
// Check if backend scoring is available
if (player.getItemAttribute('adaptive') === 'true') {
// Send to backend for scoring
const response = await fetch('/api/score', {
method: 'POST',
body: JSON.stringify({
itemId: 'item-123',
responses: player.getAllResponses()
})
});
const result = await response.json();
// Apply backend scores
player.applyOutcomes(result.outcomes);
}For details, see SERVER-API.md.
Development
# Install dependencies
bun install
# Build
bun run build
# Run tests
bun test
# Type check
bun run typecheck
# Lint
bun run lintLicense
ISC
Related Packages
- @pie-qti/assessment-player - Multi-item test player
- @pie-qti/player-elements - Web components
- @pie-qti/example - Demo application
- @pie-qti/to-pie - QTI to PIE transformer
Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
