quiz-plugin.ts•15.6 kB
/**
* Quiz Plugin - Multiple Choice Questions
* Implements quiz-1 content element for assessment
*/
import { BaseContentElementPlugin, RecognitionPattern, EducationalMetadata } from './base-plugin.js';
import { Quiz1Widget, QuizQuestion, QuizChoice, QuizFeedback } from '../composer-widget-types.js';
// Internal quiz question format used by the plugin
export interface QuizQuestionInternal {
id: string;
question: string;
options: string[];
correctAnswer: number;
explanation?: string;
points?: number;
}
export interface QuizProperties {
title: string;
description?: string;
questions: QuizQuestionInternal[];
settings: {
showFeedback: boolean;
allowRetries: boolean;
randomizeQuestions: boolean;
randomizeOptions: boolean;
timeLimit?: number; // seconds
passingScore: number; // percentage
showCorrectAnswers: boolean;
};
styling?: {
theme: 'default' | 'modern' | 'minimal';
accentColor: string;
};
}
export class QuizPlugin extends BaseContentElementPlugin {
type = 'quiz-1' as const;
name = 'Multiple Choice Quiz';
description = 'Interactive multiple choice quiz for knowledge assessment';
version = '1.0.0';
recognitionPatterns: RecognitionPattern[] = [
{
pattern: /\b(quiz|test|exam|assessment)\b/i,
weight: 1.0,
context: ['assessment', 'practice'],
examples: ['Create a quiz about...', 'Test students on...', 'Assessment for...']
},
{
pattern: /\b(check|verify|assess|evaluate|measure)\s+(knowledge|understanding|learning)\b/i,
weight: 0.9,
context: ['assessment'],
examples: ['Check student knowledge', 'Assess understanding']
},
{
pattern: /\b(multiple\s+choice|select\s+the\s+correct|choose\s+the\s+best)\b/i,
weight: 0.8,
context: ['assessment'],
examples: ['Multiple choice questions', 'Select the correct answer']
},
{
pattern: /\b(question[s]?|answer[s]?|option[s]?)\b/i,
weight: 0.6,
context: ['assessment'],
examples: ['Create questions about...', 'Answer options for...']
},
{
pattern: /\b(grade|score|result|performance)\b/i,
weight: 0.5,
context: ['assessment'],
examples: ['Grade student performance', 'Score the quiz']
}
];
generateWidget(content: string, properties?: Partial<QuizProperties>): Quiz1Widget {
const quizProps = this.generateQuizProperties(content, properties);
return {
id: this.generateId('quiz'),
type: this.type,
content_title: null,
padding_top: 35,
padding_bottom: 35,
background_color: '#FFFFFF',
primary_color: quizProps.styling?.accentColor || '#2d7b45',
remake: quizProps.settings.allowRetries ? 'enable' : 'disable',
max_attempts: quizProps.settings.allowRetries ? 3 : 1,
utilization: {
enabled: false,
percentage: null
},
feedback: {
type: quizProps.settings.showFeedback ? 'default' : 'default'
},
questions: this.convertToComposerQuestions(quizProps.questions),
dam_assets: []
};
}
getEducationalValue(): EducationalMetadata {
return {
learningTypes: ['assessment', 'practice'],
complexity: 'intermediate',
interactivity: 'high',
timeEstimate: 10, // 10 minutes average
prerequisites: ['Content understanding'],
};
}
private generateQuizProperties(content: string, userProps?: Partial<QuizProperties>): QuizProperties {
const title = this.extractQuizTitle(content);
const description = this.extractDescription(content);
const questions = this.generateQuestionsFromContent(content);
// Determine quiz purpose and settings
const purpose = this.determineQuizPurpose(content);
const settings = this.generateSettings(purpose, userProps?.settings);
return {
title,
description,
questions,
settings,
styling: userProps?.styling || {
theme: 'default',
accentColor: '#007bff',
},
...userProps,
};
}
private extractQuizTitle(content: string): string {
// Look for explicit titles
const titlePatterns = [
/(?:quiz|test|assessment):\s*([^\n.!?]+)/i,
/title:\s*([^\n.!?]+)/i,
/^([^.!?]+(?:quiz|test|assessment))/i,
];
for (const pattern of titlePatterns) {
const match = content.match(pattern);
if (match && match[1]) {
return match[1].trim();
}
}
// Generate title from content topic
const firstSentence = content.split(/[.!?]/)[0];
const topic = this.extractMainTopic(firstSentence);
return topic ? `${topic} Quiz` : 'Knowledge Assessment';
}
private extractDescription(content: string): string {
// Look for description patterns
const descPatterns = [
/description:\s*([^\n]+)/i,
/about:\s*([^\n]+)/i,
/this\s+(?:quiz|test|assessment)\s+(?:covers|includes|tests):\s*([^\n.!?]+)/i,
];
for (const pattern of descPatterns) {
const match = content.match(pattern);
if (match && match[1]) {
return match[1].trim();
}
}
// Generate description from content
const topic = this.extractMainTopic(content);
return topic ? `Test your knowledge of ${topic}` : 'Assess your understanding of the material';
}
private generateQuestionsFromContent(content: string): QuizQuestionInternal[] {
const questions: QuizQuestionInternal[] = [];
// Strategy 1: Extract explicit questions from content
const explicitQuestions = this.extractExplicitQuestions(content);
questions.push(...explicitQuestions);
// Strategy 2: Generate questions from key statements
if (questions.length < 3) {
const generatedQuestions = this.generateQuestionsFromStatements(content);
questions.push(...generatedQuestions);
}
// Strategy 3: Create default questions if still insufficient
if (questions.length === 0) {
questions.push(...this.createDefaultQuestions(content));
}
// Ensure minimum of 3 questions, maximum of 10
return questions.slice(0, 10);
}
private extractExplicitQuestions(content: string): QuizQuestionInternal[] {
const questions: QuizQuestionInternal[] = [];
const questionPattern = /([^.!?]*\?)/g;
let match;
while ((match = questionPattern.exec(content)) !== null) {
const questionText = match[1].trim();
if (questionText.length > 10) {
questions.push({
id: this.generateId('question'),
question: questionText,
options: this.generateOptionsForQuestion(questionText, content),
correctAnswer: 0, // Default to first option
points: 1,
});
}
}
return questions;
}
private generateQuestionsFromStatements(content: string): QuizQuestionInternal[] {
const questions: QuizQuestionInternal[] = [];
const sentences = content
.split(/[.!]+/)
.filter(s => s.trim().length > 20)
.slice(0, 5); // Limit to first 5 meaningful sentences
sentences.forEach((sentence, index) => {
const cleanSentence = sentence.trim();
if (this.isFactualStatement(cleanSentence)) {
const question = this.convertStatementToQuestion(cleanSentence);
questions.push({
id: this.generateId('question'),
question,
options: this.generateOptionsForStatement(cleanSentence, content),
correctAnswer: 0,
explanation: `Based on the content: "${cleanSentence}"`,
points: 1,
});
}
});
return questions;
}
private createDefaultQuestions(content: string): QuizQuestionInternal[] {
const topic = this.extractMainTopic(content);
return [
{
id: this.generateId('question'),
question: `What is the main topic of this content?`,
options: [
topic || 'The primary subject',
'Secondary information',
'Background context',
'Additional details',
],
correctAnswer: 0,
points: 1,
},
{
id: this.generateId('question'),
question: `Which statement best describes the content?`,
options: [
this.extractSnippet(content, 80),
'Alternative description A',
'Alternative description B',
'Alternative description C',
],
correctAnswer: 0,
points: 1,
},
{
id: this.generateId('question'),
question: `What is the purpose of this information?`,
options: [
'To educate and inform',
'To entertain only',
'To sell products',
'To confuse readers',
],
correctAnswer: 0,
points: 1,
},
];
}
private generateOptionsForQuestion(question: string, context: string): string[] {
// For explicit questions, try to extract possible answers from context
const words = context.toLowerCase().split(/\s+/);
const questionWords = question.toLowerCase().split(/\s+/);
// Simple approach: find relevant nouns and concepts
const concepts = this.extractConcepts(context);
const options = concepts.slice(0, 4);
// Ensure we have 4 options
while (options.length < 4) {
options.push(`Option ${String.fromCharCode(65 + options.length)}`);
}
return options;
}
private generateOptionsForStatement(statement: string, context: string): string[] {
const concepts = this.extractConcepts(context);
const mainConcept = this.extractMainConcept(statement);
const options = [mainConcept]; // Correct answer first
// Add distractors from other concepts
const distractors = concepts.filter(c => c !== mainConcept).slice(0, 3);
options.push(...distractors);
// Fill remaining slots if needed
while (options.length < 4) {
options.push(`Alternative ${String.fromCharCode(65 + options.length - 1)}`);
}
return options;
}
private isFactualStatement(sentence: string): boolean {
const factualIndicators = [
/\bis\b/, /\bare\b/, /\bwas\b/, /\bwere\b/,
/\bhas\b/, /\bhave\b/, /\bcontains\b/, /\bincludes\b/,
/\bmeans\b/, /\brefers\s+to\b/, /\bdefines\b/,
];
return factualIndicators.some(pattern => pattern.test(sentence.toLowerCase()));
}
private convertStatementToQuestion(statement: string): string {
const lowerStatement = statement.toLowerCase().trim();
// Convert "X is Y" to "What is X?"
if (lowerStatement.includes(' is ')) {
const parts = lowerStatement.split(' is ');
return `What is ${parts[0].trim()}?`;
}
// Convert "X has Y" to "What does X have?"
if (lowerStatement.includes(' has ')) {
const parts = lowerStatement.split(' has ');
return `What does ${parts[0].trim()} have?`;
}
// Convert "X means Y" to "What does X mean?"
if (lowerStatement.includes(' means ')) {
const parts = lowerStatement.split(' means ');
return `What does ${parts[0].trim()} mean?`;
}
// Default conversion
return `Which statement is true about: ${this.extractSnippet(statement, 30)}?`;
}
private extractMainTopic(content: string): string {
// Simple topic extraction - can be enhanced with NLP
const words = content.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 3);
// Count word frequency
const frequency: Record<string, number> = {};
words.forEach(word => {
frequency[word] = (frequency[word] || 0) + 1;
});
// Find most frequent non-common word
const commonWords = ['the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'two', 'who', 'boy', 'did', 'man', 'run', 'say', 'she', 'too', 'use'];
const topWords = Object.entries(frequency)
.filter(([word]) => !commonWords.includes(word))
.sort(([,a], [,b]) => b - a);
return topWords.length > 0 ? topWords[0][0] : 'content';
}
private extractConcepts(content: string): string[] {
// Extract potential answer concepts from content
const sentences = content.split(/[.!?]+/);
const concepts: string[] = [];
sentences.forEach(sentence => {
// Look for noun phrases and important terms
const words = sentence.split(/\s+/)
.filter(word => word.length > 3)
.map(word => word.replace(/[^\w]/g, ''));
// Add capitalized words (likely proper nouns or important terms)
words.forEach(word => {
if (word.charAt(0) === word.charAt(0).toUpperCase() && word.length > 1) {
concepts.push(word);
}
});
});
// Remove duplicates and return unique concepts
return [...new Set(concepts)].slice(0, 8);
}
private extractMainConcept(statement: string): string {
const concepts = this.extractConcepts(statement);
return concepts.length > 0 ? concepts[0] : this.extractSnippet(statement, 20);
}
private determineQuizPurpose(content: string): 'assessment' | 'practice' | 'review' {
const lowerContent = content.toLowerCase();
if (lowerContent.includes('final') || lowerContent.includes('exam') || lowerContent.includes('grade')) {
return 'assessment';
}
if (lowerContent.includes('practice') || lowerContent.includes('exercise') || lowerContent.includes('drill')) {
return 'practice';
}
return 'review'; // Default
}
private generateSettings(purpose: string, userSettings?: Partial<QuizProperties['settings']>): QuizProperties['settings'] {
const baseSettings = {
assessment: {
showFeedback: false,
allowRetries: false,
randomizeQuestions: true,
randomizeOptions: true,
passingScore: 80,
showCorrectAnswers: false,
},
practice: {
showFeedback: true,
allowRetries: true,
randomizeQuestions: false,
randomizeOptions: false,
passingScore: 60,
showCorrectAnswers: true,
},
review: {
showFeedback: true,
allowRetries: true,
randomizeQuestions: false,
randomizeOptions: false,
passingScore: 70,
showCorrectAnswers: true,
},
};
return {
...baseSettings[purpose as keyof typeof baseSettings],
...userSettings,
};
}
private convertToComposerQuestions(questions: QuizQuestionInternal[]): QuizQuestion[] {
return questions.map(q => ({
id: q.id,
question: `<p><span style="font-size: 18px;">${q.question}</span></p>`,
image: null,
video: null,
answered: false,
feedback_default: {
text: null,
image: null,
video: null,
media_max_width: null
},
feedback_correct: {
text: q.explanation ? `<p>${q.explanation}</p>` : null,
image: null,
video: null,
media_max_width: null
},
feedback_incorrect: {
text: q.explanation ? `<p>${q.explanation}</p>` : null,
image: null,
video: null,
media_max_width: null
},
no_correct_answer: false,
no_feedback: false,
choices: q.options.map((option, index) => ({
id: `${q.id}-choice-${index}`,
correct: index === q.correctAnswer,
text: `<p><span style="font-size: 15px;">${option}</span></p>`
}))
}));
}
}