Skip to main content

Question Types

TypeScript types and interfaces for all question types and configurations in the PicoLMS library.

Overview

The question types module provides:

  • Question Type Definitions: All supported question types
  • Base Configuration: Shared properties across all question types
  • Type-Specific Configs: Specialized configurations for each question type
  • Answer Types: Type-safe answer structures
  • Validation: Question validation configuration
  • Submission Tracking: Status and grading information

Import

import type {
QuestionType,
QuestionConfig,
BaseQuestionConfig,
QuestionAnswer,
MultipleChoiceConfig,
TrueOrFalseConfig,
ShortAnswerConfig,
EssayConfig,
FillInBlankConfig,
MatchingConfig,
SubmissionStatus,
QuestionSubmission,
QuestionState,
} from '@scinforma/picolms';

Types

QuestionType

Enumeration of all supported question types.

type QuestionType =
| 'multiple-choice'
| 'true-false'
| 'short-answer'
| 'essay'
| 'fill-in-blank'
| 'matching'
| 'ordering'
| 'file-upload';

Values

TypeDescriptionUse Case
'multiple-choice'Single or multiple selection from optionsObjective assessment, quizzes
'true-false'Binary true/false selectionQuick checks, fact verification
'short-answer'Brief text responseDefinitions, calculations
'essay'Extended text responseDetailed explanations, analysis
'fill-in-blank'Complete text with missing wordsVocabulary, comprehension
'matching'Match items from two columnsAssociations, relationships
'ordering'Arrange items in sequenceProcess steps, chronology
'file-upload'Upload document or fileAssignments, projects

Example

import { QuestionType } from '@scinforma/picolms';

const questionType: QuestionType = 'multiple-choice';

// Type guard
function isMultipleChoice(type: QuestionType): boolean {
return type === 'multiple-choice';
}

// Filter by type
const mcQuestions = allQuestions.filter(
q => q.type === 'multiple-choice'
);

SubmissionStatus

Status of question submission and grading.

type SubmissionStatus = 'not-started' | 'in-progress' | 'submitted' | 'graded';

Values

StatusDescriptionTypical UI State
'not-started'Question hasn't been interacted withEnable all controls
'in-progress'User is answeringAllow editing
'submitted'Answer has been submittedDisable editing, show feedback
'graded'Answer has been gradedShow score and detailed feedback

Example

import { SubmissionStatus } from '@scinforma/picolms';

function QuestionStatus({ status }: { status: SubmissionStatus }) {
const statusConfig = {
'not-started': { label: 'Not Started', color: 'gray' },
'in-progress': { label: 'In Progress', color: 'blue' },
'submitted': { label: 'Submitted', color: 'green' },
'graded': { label: 'Graded', color: 'purple' },
};

const config = statusConfig[status];

return (
<span className={`badge badge-${config.color}`}>
{config.label}
</span>
);
}

Base Interfaces

BaseQuestionConfig

Base configuration interface that all question types extend.

interface BaseQuestionConfig {
id: string;
type: QuestionType;
title?: string;
question: string;
instructions?: string;
points: number;
required?: boolean;
difficulty?: DifficultyLevel;
tags?: string[];
category?: string;
media?: MediaAttachment[];
feedback?: {
correct?: Feedback;
incorrect?: Feedback;
partial?: Feedback;
hints?: string[];
};
timeLimit?: number;
maxAttempts?: number;
accessibility?: AccessibilityConfig;
validation?: QuestionValidationConfig;
metadata?: BaseMetadata;
}

Properties

PropertyTypeRequiredDescription
idstringYesUnique identifier for the question
typeQuestionTypeYesType of question
titlestringNoOptional title (separate from question text)
questionstringYesMain question text (supports HTML)
instructionsstringNoAdditional instructions for answering
pointsnumberYesPoint value for correct answer
requiredbooleanNoWhether answering is required
difficultyDifficultyLevelNoDifficulty level classification
tagsstring[]NoTags for categorization
categorystringNoCategory grouping
mediaMediaAttachment[]NoAttached media files
feedbackobjectNoFeedback configuration
timeLimitnumberNoTime limit in seconds
maxAttemptsnumberNoMaximum number of attempts allowed
accessibilityAccessibilityConfigNoAccessibility configuration
validationQuestionValidationConfigNoValidation rules
metadataBaseMetadataNoAdditional metadata

Example

import { BaseQuestionConfig } from '@scinforma/picolms';

const baseConfig: BaseQuestionConfig = {
id: 'q1',
type: 'short-answer',
title: 'Newton\'s First Law',
question: 'Explain Newton\'s First Law of Motion in your own words.',
instructions: 'Write at least 50 words.',
points: 10,
required: true,
difficulty: 'beginner',
tags: ['physics', 'mechanics', 'newton'],
category: 'Classical Mechanics',
timeLimit: 300, // 5 minutes
maxAttempts: 3,
feedback: {
correct: {
type: 'correct',
message: 'Excellent explanation!',
showAfter: 'submission',
},
incorrect: {
type: 'incorrect',
message: 'Review the concept of inertia.',
showAfter: 'submission',
},
hints: [
'Think about objects at rest',
'Consider the role of force',
],
},
};

QuestionAnswer<T>

Generic answer state for any question type.

interface QuestionAnswer<T = any> {
questionId: string;
value: T;
isAnswered: boolean;
attemptNumber: number;
timeSpent?: number;
timestamp?: string;
}

Properties

PropertyTypeRequiredDescription
questionIdstringYesID of the associated question
valueTYesThe answer value (type varies by question type)
isAnsweredbooleanYesWhether the question has been answered
attemptNumbernumberYesCurrent attempt number (starts at 1)
timeSpentnumberNoTime spent on question in seconds
timestampstringNoISO timestamp of last update

Type Parameter

The generic type T represents the answer value type:

  • string for short-answer and essay
  • boolean for true-false
  • string | string[] for multiple-choice
  • Record<string, string> for fill-in-blank and matching

Example

import { QuestionAnswer } from '@scinforma/picolms';

// Short answer
const shortAnswer: QuestionAnswer<string> = {
questionId: 'q1',
value: 'An object at rest stays at rest unless acted upon by a force.',
isAnswered: true,
attemptNumber: 1,
timeSpent: 45,
timestamp: '2024-12-29T10:30:00Z',
};

// Multiple choice (single)
const mcAnswer: QuestionAnswer<string> = {
questionId: 'q2',
value: 'option-b',
isAnswered: true,
attemptNumber: 1,
timeSpent: 15,
timestamp: '2024-12-29T10:31:00Z',
};

// Multiple choice (multiple)
const multiSelectAnswer: QuestionAnswer<string[]> = {
questionId: 'q3',
value: ['option-a', 'option-c', 'option-d'],
isAnswered: true,
attemptNumber: 2,
timeSpent: 60,
timestamp: '2024-12-29T10:32:00Z',
};

// Fill in blank
const fillInBlankAnswer: QuestionAnswer<Record<string, string>> = {
questionId: 'q4',
value: {
'blank-1': 'inertia',
'blank-2': 'force',
'blank-3': 'acceleration',
},
isAnswered: true,
attemptNumber: 1,
timeSpent: 90,
timestamp: '2024-12-29T10:33:00Z',
};

ValidationRule

Individual validation rule configuration.

interface ValidationRule {
type: 'required' | 'minLength' | 'maxLength' | 'pattern' | 'custom';
value?: any;
message: string;
validate?: (value: any) => boolean | Promise<boolean>;
}

Properties

PropertyTypeRequiredDescription
typestringYesType of validation rule
valueanyNoRule-specific value (e.g., min length number)
messagestringYesError message to display when validation fails
validatefunctionNoCustom validation function (for type: 'custom')

Validation Types

TypeDescriptionValue TypeExample
'required'Field must not be emptyN/ARequired field
'minLength'Minimum string lengthnumberAt least 10 characters
'maxLength'Maximum string lengthnumberNo more than 500 characters
'pattern'Regex pattern matchstringEmail format validation
'custom'Custom validation functionfunctionComplex business logic

Example

import { ValidationRule } from '@scinforma/picolms';

// Required validation
const requiredRule: ValidationRule = {
type: 'required',
message: 'This field is required',
};

// Min length validation
const minLengthRule: ValidationRule = {
type: 'minLength',
value: 50,
message: 'Answer must be at least 50 characters',
};

// Max length validation
const maxLengthRule: ValidationRule = {
type: 'maxLength',
value: 500,
message: 'Answer must not exceed 500 characters',
};

// Pattern validation (email)
const emailRule: ValidationRule = {
type: 'pattern',
value: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
message: 'Please enter a valid email address',
};

// Custom validation
const customRule: ValidationRule = {
type: 'custom',
message: 'Answer must contain at least one number',
validate: (value: string) => /\d/.test(value),
};

// Async custom validation
const asyncRule: ValidationRule = {
type: 'custom',
message: 'This username is already taken',
validate: async (value: string) => {
const response = await fetch(`/api/check-username?username=${value}`);
const data = await response.json();
return data.available;
},
};

QuestionValidationConfig

Configuration for question validation behavior.

interface QuestionValidationConfig {
rules?: ValidationRule[];
validateOnChange?: boolean;
validateOnBlur?: boolean;
}

Properties

PropertyTypeRequiredDefaultDescription
rulesValidationRule[]No[]Array of validation rules to apply
validateOnChangebooleanNofalseRun validation on every change
validateOnBlurbooleanNotrueRun validation when field loses focus

Example

import { QuestionValidationConfig } from '@scinforma/picolms';

const validationConfig: QuestionValidationConfig = {
rules: [
{
type: 'required',
message: 'Answer is required',
},
{
type: 'minLength',
value: 50,
message: 'Answer must be at least 50 characters',
},
{
type: 'custom',
message: 'Answer must mention Newton',
validate: (value: string) => value.toLowerCase().includes('newton'),
},
],
validateOnChange: false,
validateOnBlur: true,
};

// Use in question config
const questionConfig: ShortAnswerConfig = {
id: 'q1',
type: 'short-answer',
question: 'Explain Newton\'s First Law',
points: 10,
validation: validationConfig,
};

Question Type-Specific Interfaces

Multiple Choice

MultipleChoiceOption

Individual option in a multiple choice question.

interface MultipleChoiceOption {
id: string;
text: string;
isCorrect: boolean;
feedback?: string;
media?: MediaAttachment;
}

Properties:

PropertyTypeRequiredDescription
idstringYesUnique identifier for the option
textstringYesOption text to display
isCorrectbooleanYesWhether this is a correct answer
feedbackstringNoFeedback specific to this option
mediaMediaAttachmentNoMedia attachment for this option

MultipleChoiceConfig

Configuration for multiple choice questions.

interface MultipleChoiceConfig extends BaseQuestionConfig {
type: 'multiple-choice';
options: MultipleChoiceOption[];
allowMultiple?: boolean;
shuffleOptions?: boolean;
displayAs?: 'radio' | 'checkbox' | 'buttons' | 'dropdown';
minSelections?: number;
maxSelections?: number;
}

Additional Properties:

PropertyTypeRequiredDefaultDescription
optionsMultipleChoiceOption[]Yes-Array of answer options
allowMultiplebooleanNofalseAllow selecting multiple answers
shuffleOptionsbooleanNofalseRandomize option order
displayAsstringNo'radio'UI display style
minSelectionsnumberNo-Minimum selections required (multi-select)
maxSelectionsnumberNo-Maximum selections allowed (multi-select)

MultipleChoiceAnswer

type MultipleChoiceAnswer = string | string[];

Single option ID (string) for single-select, or array of option IDs (string[]) for multi-select.

Example

import { MultipleChoiceConfig, MultipleChoiceAnswer } from '@scinforma/picolms';

// Single select question
const singleSelectConfig: MultipleChoiceConfig = {
id: 'mc-1',
type: 'multiple-choice',
question: 'What is the capital of France?',
points: 5,
options: [
{ id: 'opt-a', text: 'London', isCorrect: false },
{ id: 'opt-b', text: 'Paris', isCorrect: true, feedback: 'Correct!' },
{ id: 'opt-c', text: 'Berlin', isCorrect: false },
{ id: 'opt-d', text: 'Madrid', isCorrect: false },
],
displayAs: 'radio',
shuffleOptions: true,
};

const singleAnswer: MultipleChoiceAnswer = 'opt-b';

// Multi-select question
const multiSelectConfig: MultipleChoiceConfig = {
id: 'mc-2',
type: 'multiple-choice',
question: 'Which of the following are primary colors?',
points: 10,
allowMultiple: true,
minSelections: 1,
maxSelections: 3,
options: [
{ id: 'opt-a', text: 'Red', isCorrect: true },
{ id: 'opt-b', text: 'Blue', isCorrect: true },
{ id: 'opt-c', text: 'Yellow', isCorrect: true },
{ id: 'opt-d', text: 'Green', isCorrect: false },
{ id: 'opt-e', text: 'Purple', isCorrect: false },
],
displayAs: 'checkbox',
};

const multiAnswer: MultipleChoiceAnswer = ['opt-a', 'opt-b', 'opt-c'];

// With media
const withMediaConfig: MultipleChoiceConfig = {
id: 'mc-3',
type: 'multiple-choice',
question: 'Which diagram shows a parallel circuit?',
points: 5,
options: [
{
id: 'opt-a',
text: 'Diagram A',
isCorrect: false,
media: {
id: 'img-a',
type: 'image',
url: '/images/circuit-a.png',
alt: 'Circuit diagram A',
},
},
{
id: 'opt-b',
text: 'Diagram B',
isCorrect: true,
media: {
id: 'img-b',
type: 'image',
url: '/images/circuit-b.png',
alt: 'Circuit diagram B',
},
},
],
displayAs: 'buttons',
};

True/False

TrueOrFalseConfig

Configuration for true/false questions.

interface TrueOrFalseConfig extends BaseQuestionConfig {
type: 'true-false';
correctAnswer: boolean;
displayAs?: 'radio' | 'buttons' | 'toggle';
}

Additional Properties:

PropertyTypeRequiredDefaultDescription
correctAnswerbooleanYes-The correct answer (true or false)
displayAsstringNo'radio'UI display style

TrueOrFalseAnswer

type TrueOrFalseAnswer = boolean;

Example

import { TrueOrFalseConfig, TrueOrFalseAnswer } from '@scinforma/picolms';

const tfConfig: TrueOrFalseConfig = {
id: 'tf-1',
type: 'true-false',
question: 'The Earth revolves around the Sun.',
points: 2,
correctAnswer: true,
displayAs: 'buttons',
feedback: {
correct: {
type: 'correct',
message: 'Correct! The Earth orbits the Sun.',
showAfter: 'immediate',
},
incorrect: {
type: 'incorrect',
message: 'Incorrect. The Earth revolves around the Sun, not vice versa.',
showAfter: 'immediate',
},
},
};

const answer: TrueOrFalseAnswer = true;

Short Answer

ShortAnswerConfig

Configuration for short answer questions.

interface ShortAnswerConfig extends BaseQuestionConfig {
type: 'short-answer';
correctAnswers?: string[];
caseSensitive?: boolean;
trimWhitespace?: boolean;
maxLength?: number;
minLength?: number;
pattern?: string;
placeholder?: string;
validation?: QuestionValidationConfig;
}

Additional Properties:

PropertyTypeRequiredDefaultDescription
correctAnswersstring[]No-Multiple acceptable answers for auto-grading
caseSensitivebooleanNofalseWhether answer matching is case-sensitive
trimWhitespacebooleanNotrueTrim whitespace before validation
maxLengthnumberNo-Maximum character length
minLengthnumberNo-Minimum character length
patternstringNo-Regex pattern for validation
placeholderstringNo-Placeholder text for input

ShortAnswerAnswer

type ShortAnswerAnswer = string;

Example

import { ShortAnswerConfig, ShortAnswerAnswer } from '@scinforma/picolms';

const shortAnswerConfig: ShortAnswerConfig = {
id: 'sa-1',
type: 'short-answer',
question: 'What is the chemical symbol for water?',
points: 3,
correctAnswers: ['H2O', 'H₂O'],
caseSensitive: false,
trimWhitespace: true,
maxLength: 10,
placeholder: 'Enter chemical formula',
validation: {
rules: [
{
type: 'required',
message: 'Answer is required',
},
{
type: 'maxLength',
value: 10,
message: 'Answer too long',
},
],
validateOnBlur: true,
},
};

const answer: ShortAnswerAnswer = 'H2O';

// With pattern validation
const emailConfig: ShortAnswerConfig = {
id: 'sa-2',
type: 'short-answer',
question: 'Enter your email address',
points: 0,
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
placeholder: 'you@example.com',
};

Essay

EssayConfig

Configuration for essay questions.

interface EssayConfig extends BaseQuestionConfig {
type: 'essay';
minWords?: number;
maxWords?: number;
minCharacters?: number;
maxCharacters?: number;
placeholder?: string;
enableRichText?: boolean;
}

Additional Properties:

PropertyTypeRequiredDefaultDescription
minWordsnumberNo-Minimum word count
maxWordsnumberNo-Maximum word count
minCharactersnumberNo-Minimum character count
maxCharactersnumberNo-Maximum character count
placeholderstringNo-Placeholder text for textarea
enableRichTextbooleanNofalseEnable rich text editor

EssayAnswer

type EssayAnswer = string;

Example

import { EssayConfig, EssayAnswer } from '@scinforma/picolms';

const essayConfig: EssayConfig = {
id: 'essay-1',
type: 'essay',
question: 'Discuss the impact of the Industrial Revolution on society.',
instructions: 'Write a comprehensive essay addressing economic, social, and technological changes.',
points: 50,
minWords: 300,
maxWords: 1000,
minCharacters: 1500,
placeholder: 'Begin your essay here...',
enableRichText: true,
timeLimit: 3600, // 1 hour
};

const answer: EssayAnswer = `The Industrial Revolution, which began in the late 18th century...`;

// Word count helper
function countWords(text: string): number {
return text.trim().split(/\s+/).filter(Boolean).length;
}

// Validation
function validateEssay(answer: string, config: EssayConfig): boolean {
const wordCount = countWords(answer);

if (config.minWords && wordCount < config.minWords) return false;
if (config.maxWords && wordCount > config.maxWords) return false;

return true;
}

Fill in the Blank

FillInBlankSegment

Individual segment in a fill-in-blank question.

interface FillInBlankSegment {
type: 'text' | 'blank';
content?: string;
id?: string;
correctAnswers?: string[];
caseSensitive?: boolean;
placeholder?: string;
}

Properties:

PropertyTypeRequiredDescription
type'text' | 'blank'YesType of segment
contentstringNoText content (for type: 'text')
idstringNoUnique ID (for type: 'blank')
correctAnswersstring[]NoAcceptable answers (for type: 'blank')
caseSensitivebooleanNoCase-sensitive matching (for type: 'blank')
placeholderstringNoPlaceholder text (for type: 'blank')

FillInBlankConfig

Configuration for fill-in-blank questions.

interface FillInBlankConfig extends BaseQuestionConfig {
type: 'fill-in-blank';
segments: FillInBlankSegment[];
}

Additional Properties:

PropertyTypeRequiredDescription
segmentsFillInBlankSegment[]YesArray of text and blank segments

FillInBlankAnswer

type FillInBlankAnswer = Record<string, string>;

Map of blank ID to answer string.

Example

import {
FillInBlankConfig,
FillInBlankAnswer,
FillInBlankSegment,
} from '@scinforma/picolms';

const fillInBlankConfig: FillInBlankConfig = {
id: 'fib-1',
type: 'fill-in-blank',
question: 'Complete the sentence',
points: 10,
segments: [
{
type: 'text',
content: 'The capital of France is ',
},
{
type: 'blank',
id: 'blank-1',
correctAnswers: ['Paris', 'paris'],
caseSensitive: false,
placeholder: 'city name',
},
{
type: 'text',
content: ' and the capital of Germany is ',
},
{
type: 'blank',
id: 'blank-2',
correctAnswers: ['Berlin', 'berlin'],
caseSensitive: false,
placeholder: 'city name',
},
{
type: 'text',
content: '.',
},
],
};

const answer: FillInBlankAnswer = {
'blank-1': 'Paris',
'blank-2': 'Berlin',
};

// Render function
function FillInBlankRenderer({ segments }: { segments: FillInBlankSegment[] }) {
return (
<div>
{segments.map((segment, index) => {
if (segment.type === 'text') {
return <span key={index}>{segment.content}</span>;
}

return (
<input
key={segment.id}
type="text"
placeholder={segment.placeholder}
aria-label={`Blank ${index + 1}`}
/>
);
})}
</div>
);
}

Matching

MatchingPair

Individual matching pair.

interface MatchingPair {
id: string;
left: string;
right: string;
leftMedia?: MediaAttachment;
rightMedia?: MediaAttachment;
}

Properties:

PropertyTypeRequiredDescription
idstringYesUnique identifier for the pair
leftstringYesText for left column item
rightstringYesText for right column item (correct match)
leftMediaMediaAttachmentNoMedia for left item
rightMediaMediaAttachmentNoMedia for right item

MatchingConfig

Configuration for matching questions.

interface MatchingConfig extends BaseQuestionConfig {
type: 'matching';
pairs: MatchingPair[];
randomizeLeft?: boolean;
randomizeRight?: boolean;
}

Additional Properties:

PropertyTypeRequiredDefaultDescription
pairsMatchingPair[]Yes-Array of matching pairs
randomizeLeftbooleanNofalseRandomize left column order
randomizeRightbooleanNotrueRandomize right column order

MatchingAnswer

type MatchingAnswer = Record<string, string>;

Map of left item ID to right item ID.

Example

import { MatchingConfig, MatchingAnswer, MatchingPair } from '@scinforma/picolms';

const matchingConfig: MatchingConfig = {
id: 'match-1',
type: 'matching',
question: 'Match each country with its capital',
points: 15,
pairs: [
{ id: 'pair-1', left: 'France', right: 'Paris' },
{ id: 'pair-2', left: 'Germany', right: 'Berlin' },
{ id: 'pair-3', left: 'Italy', right: 'Rome' },
{ id: 'pair-4', left: 'Spain', right: 'Madrid' },
{ id: 'pair-5', left: 'Portugal', right: 'Lisbon' },
],
randomizeLeft: false,
randomizeRight: true,
};

const answer: MatchingAnswer = {
'pair-1': 'pair-1', // France -> Paris
'pair-2': 'pair-2', // Germany -> Berlin
'pair-3': 'pair-3', // Italy -> Rome
'pair-4': 'pair-4', // Spain -> Madrid
'pair-5': 'pair-5', // Portugal -> Lisbon
};

// With media
const matchingWithMedia: MatchingConfig = {
id: 'match-2',
type: 'matching',
question: 'Match each flag with its country',
points: 20,
pairs: [
{
id: 'pair-1',
left: 'Flag A',
right: 'France',
leftMedia: {
id: 'flag-fr',
type: 'image',
url: '/flags/france.png',
alt: 'French flag',
},
},
{
id: 'pair-2',
left: 'Flag B',
right: 'Germany',
leftMedia: {
id: 'flag-de',
type: 'image',
url: '/flags/germany.png',
alt: 'German flag',
},
},
],
randomizeRight: true,
};

Union Types

QuestionConfig

Union type of all question configuration types.

type QuestionConfig =
| MultipleChoiceConfig
| TrueOrFalseConfig
| ShortAnswerConfig
| EssayConfig
| FillInBlankConfig
| MatchingConfig;

Example

import { QuestionConfig } from '@scinforma/picolms';

// Type guard functions
function isMultipleChoice(
config: QuestionConfig
): config is MultipleChoiceConfig {
return config.type === 'multiple-choice';
}

function isEssay(config: QuestionConfig): config is EssayConfig {
return config.type === 'essay';
}

// Usage
function renderQuestion(config: QuestionConfig) {
if (isMultipleChoice(config)) {
// TypeScript knows config is MultipleChoiceConfig
return <MultipleChoiceQuestion options={config.options} />;
}

if (isEssay(config)) {
// TypeScript knows config is EssayConfig
return <EssayQuestion minWords={config.minWords} />;
}

// ... handle other types
}

// Generic component
function Question({ config }: { config: QuestionConfig }) {
switch (config.type) {
case 'multiple-choice':
return <MultipleChoiceQuestion config={config} />;
case 'true-false':
return <TrueOrFalseQuestion config={config} />;
case 'short-answer':
return <ShortAnswerQuestion config={config} />;
case 'essay':
return <EssayQuestion config={config} />;
case 'fill-in-blank':
return <FillInBlankQuestion config={config} />;
case 'matching':
return <MatchingQuestion config={config} />;
default:
return <div>Unknown question type</div>;
}
}

Submission and State

QuestionSubmission

Complete submission record for a question.

interface QuestionSubmission {
questionId: string;
answer: QuestionAnswer;
status: SubmissionStatus;
score?: number;
maxScore: number;
feedback?: Feedback[];
submittedAt?: string;
gradedAt?: string;
gradedBy?: string;
}

Properties

PropertyTypeRequiredDescription
questionIdstringYesID of the question
answerQuestionAnswerYesThe submitted answer
statusSubmissionStatusYesCurrent submission status
scorenumberNoPoints earned
maxScorenumberYesMaximum possible points
feedbackFeedback[]NoArray of feedback items
submittedAtstringNoISO timestamp of submission
gradedAtstringNoISO timestamp of grading
gradedBystringNoID of grader (instructor, auto-grader, etc.)

Example

import { QuestionSubmission } from '@scinforma/picolms';

const submission: QuestionSubmission = {
questionId: 'q1',
answer: {
questionId: 'q1',
value: 'Paris',
isAnswered: true,
attemptNumber: 1,
timeSpent: 30,
timestamp: '2024-12-29T10:30:00Z',
},
status: 'graded',
score: 10,
maxScore: 10,
feedback: [
{
type: 'correct',
message: 'Perfect answer!',
showAfter: 'grading',
},
],
submittedAt: '2024-12-29T10:30:15Z',
gradedAt: '2024-12-29T10:32:00Z',
gradedBy: 'instructor-123',
};

// Calculate percentage
const percentage = (submission.score! / submission.maxScore) * 100;

// Display submission
function SubmissionCard({ submission }: { submission: QuestionSubmission }) {
return (
<div className="submission-card">
<h4>Question {submission.questionId}</h4>
<p>Status: {submission.status}</p>
{submission.score !== undefined && (
<p>
Score: {submission.score} / {submission.maxScore} (
{((submission.score / submission.maxScore) * 100).toFixed(1)}%)
</p>
)}
{submission.feedback?.map((fb, i) => (
<div key={i} className={`feedback-${fb.type}`}>
{fb.message}
</div>
))}
</div>
);
}

QuestionState<T>

Internal component state for questions.

interface QuestionState<T = any> {
config: QuestionConfig;
answer: QuestionAnswer<T>;
validation: {
isValid: boolean;
errors: string[];
warnings: string[];
};
status: SubmissionStatus;
timeSpent: number;
attemptNumber: number;
isLocked: boolean;
}

Properties

PropertyTypeDescription
configQuestionConfigQuestion configuration
answerQuestionAnswer<T>Current answer state
validationobjectValidation state
validation.isValidbooleanWhether current answer is valid
validation.errorsstring[]Validation error messages
validation.warningsstring[]Validation warning messages
statusSubmissionStatusCurrent submission status
timeSpentnumberTime spent in seconds
attemptNumbernumberCurrent attempt number
isLockedbooleanWhether question is locked (submitted/graded)

Example

import { QuestionState } from '@scinforma/picolms';

// Initial state
const initialState: QuestionState<string> = {
config: shortAnswerConfig,
answer: {
questionId: 'q1',
value: '',
isAnswered: false,
attemptNumber: 1,
timeSpent: 0,
},
validation: {
isValid: false,
errors: ['Answer is required'],
warnings: [],
},
status: 'not-started',
timeSpent: 0,
attemptNumber: 1,
isLocked: false,
};

// After answering
const answeredState: QuestionState<string> = {
...initialState,
answer: {
questionId: 'q1',
value: 'Paris',
isAnswered: true,
attemptNumber: 1,
timeSpent: 45,
timestamp: '2024-12-29T10:30:00Z',
},
validation: {
isValid: true,
errors: [],
warnings: [],
},
status: 'in-progress',
timeSpent: 45,
};

// After submission
const submittedState: QuestionState<string> = {
...answeredState,
status: 'submitted',
isLocked: true,
};

Complete Examples

Building a Quiz

import {
QuestionConfig,
MultipleChoiceConfig,
TrueOrFalseConfig,
ShortAnswerConfig,
} from '@scinforma/picolms';

const quiz: QuestionConfig[] = [
// Multiple choice
{
id: 'q1',
type: 'multiple-choice',
question: 'What is the capital of France?',
points: 5,
options: [
{ id: 'a', text: 'London', isCorrect: false },
{ id: 'b', text: 'Paris', isCorrect: true },
{ id: 'c', text: 'Berlin', isCorrect: false },
{ id: 'd', text: 'Madrid', isCorrect: false },
],
shuffleOptions: true,
},

// True/False
{
id: 'q2',
type: 'true-false',
question: 'The Earth is flat.',
points: 2,
correctAnswer: false,
displayAs: 'buttons',
},

// Short answer
{
id: 'q3',
type: 'short-answer',
question: 'What is the chemical symbol for gold?',
points: 3,
correctAnswers: ['Au', 'AU', 'au'],
caseSensitive: false,
maxLength: 10,
},
];

// Render quiz
function Quiz({ questions }: { questions: QuestionConfig[] }) {
return (
<div className="quiz">
{questions.map((config, index) => (
<div key={config.id} className="question-container">
<h3>Question {index + 1}</h3>
<Question config={config} />
</div>
))}
</div>
);
}

TypeScript

All types are fully typed and support discriminated unions:

// Import types
import type {
QuestionConfig,
QuestionAnswer,
MultipleChoiceConfig,
} from '@scinforma/picolms';

// Type guards
function isMultipleChoice(
config: QuestionConfig
): config is MultipleChoiceConfig {
return config.type === 'multiple-choice';
}

// Generic functions
function createAnswer<T>(
questionId: string,
value: T
): QuestionAnswer<T> {
return {
questionId,
value,
isAnswered: value != null,
attemptNumber: 1,
timeSpent: 0,
timestamp: new Date().toISOString(),
};
}

// Usage
const mcAnswer = createAnswer<string>('q1', 'option-a');
const fibAnswer = createAnswer<Record<string, string>>('q2', {
'blank-1': 'answer',
});

Best Practices

  1. Always Provide IDs: Ensure all questions and options have unique IDs
  2. Set Points: Always specify point values for questions
  3. Use Validation: Configure validation rules for better UX
  4. Provide Feedback: Include helpful feedback for both correct and incorrect answers
  5. Consider Accessibility: Configure accessibility settings for all questions
  6. Type Your Answers: Use TypeScript generics for type-safe answer handling
  7. Handle Time Limits: Set appropriate time limits for timed assessments
  8. Test Validation: Thoroughly test validation rules before deployment

  • Common Types: Base types and interfaces
  • useQuestionState: Hook for managing question state
  • BaseQuestion: Base question component
  • Question Components: Type-specific question components

Notes

  • All timestamp fields should use ISO 8601 format
  • Question IDs must be unique within a quiz/assessment
  • Option IDs must be unique within a question
  • Validation runs according to validateOnChange and validateOnBlur settings
  • Time limits are enforced at the component level
  • Maximum attempts are tracked per question