Skip to main content
Glama
quiz-plugin.ts15.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>` })) })); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkm097git/euconquisto-composer-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server