Skip to main content
Glama
flashcards-plugin.ts21.9 kB
/** * Flashcards Plugin - Interactive Learning Cards * Implements flashcards-1 and flashcards-2 content elements for memorization */ import { BaseContentElementPlugin, RecognitionPattern, EducationalMetadata } from './base-plugin.js'; import { Flashcards1Widget, FlashcardItem } from '../composer-widget-types.js'; export interface FlashCard { id: string; front: string; back: string; category?: string; difficulty?: 'easy' | 'medium' | 'hard'; tags?: string[]; imageUrl?: string; audioUrl?: string; } export interface FlashcardsProperties { title: string; description?: string; cards: FlashCard[]; settings: { shuffle: boolean; showProgress: boolean; autoAdvance: boolean; flipAnimation: 'slide' | 'flip' | 'fade'; studyMode: 'linear' | 'random' | 'spaced'; showHints: boolean; enableAudio: boolean; }; learning: { masteryThreshold: number; // How many correct answers to mark as mastered reviewInterval: number; // Days between reviews spacedRepetition: boolean; adaptiveDifficulty: boolean; }; styling?: { cardTheme: 'default' | 'modern' | 'minimal' | 'colorful'; fontsize: 'small' | 'medium' | 'large'; accentColor: string; }; interaction?: { swipeGestures: boolean; keyboardShortcuts: boolean; clickToFlip: boolean; doubleClickMastery: boolean; }; } export class FlashcardsPlugin extends BaseContentElementPlugin { type = 'flashcards-1' as const; name = 'Interactive Flashcards'; description = 'Interactive flashcards for memorization and spaced repetition learning'; version = '1.0.0'; recognitionPatterns: RecognitionPattern[] = [ { pattern: /\b(flashcards?|flash\s+cards?|study\s+cards?)\b/i, weight: 1.0, context: ['memorization', 'practice'], examples: ['Create flashcards for...', 'Study cards about...', 'Flash cards for vocabulary'] }, { pattern: /\b(memorize|remember|recall|learn\s+by\s+heart)\b/i, weight: 0.9, context: ['memorization'], examples: ['Memorize the terms', 'Remember key concepts', 'Learn by heart'] }, { pattern: /\b(vocabulary|terms|definitions|concepts|facts)\b/i, weight: 0.8, context: ['memorization'], examples: ['Vocabulary words', 'Key terms', 'Important definitions'] }, { pattern: /\b(drill|practice|repetition|review)\b/i, weight: 0.7, context: ['practice', 'memorization'], examples: ['Drill the formulas', 'Practice vocabulary', 'Review concepts'] }, { pattern: /\b(front\s+and\s+back|question\s+and\s+answer|term\s+and\s+definition)\b/i, weight: 0.8, context: ['memorization'], examples: ['Front and back cards', 'Question and answer format'] }, { pattern: /\b(spaced\s+repetition|interval|mastery)\b/i, weight: 0.6, context: ['memorization'], examples: ['Spaced repetition learning', 'Mastery-based review'] } ]; generateWidget(content: string, properties?: Partial<FlashcardsProperties>): Flashcards1Widget { const flashcardsProps = this.generateFlashcardsProperties(content, properties); return { id: this.generateId('flashcards'), type: this.type, content_title: null, padding_top: 35, padding_bottom: 35, background_color: '#FFFFFF', card_height: 240, card_width: 240, border_color: flashcardsProps.styling?.accentColor || '#00643e', items: this.convertToComposerFlashcards(flashcardsProps.cards), dam_assets: [] }; } getEducationalValue(): EducationalMetadata { return { learningTypes: ['memorization', 'practice'], complexity: 'basic', interactivity: 'high', timeEstimate: 20, // 20 minutes average study session prerequisites: [], }; } private generateFlashcardsProperties(content: string, userProps?: Partial<FlashcardsProperties>): FlashcardsProperties { const title = this.extractFlashcardsTitle(content); const description = this.extractDescription(content); const cards = this.generateCardsFromContent(content); const purpose = this.determineFlashcardsPurpose(content); const settings = this.generateSettings(purpose, userProps?.settings); const learning = this.generateLearningSettings(purpose, userProps?.learning); const styling = this.generateStyling(purpose, userProps?.styling); const interaction = this.generateInteractionSettings(purpose, userProps?.interaction); return { title, description, cards, settings, learning, styling, interaction, ...userProps, }; } private extractFlashcardsTitle(content: string): string { // Look for explicit titles const titlePatterns = [ /(?:flashcards?|study\s+cards?):\s*([^\n.!?]+)/i, /(?:memorize|learn):\s*([^\n.!?]+)/i, /title:\s*([^\n.!?]+)/i, /^([^.!?]+(?:flashcards?|vocabulary|terms|concepts))/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 topic = this.extractMainTopic(content); return topic ? `${this.capitalizeFirst(topic)} Flashcards` : 'Study Cards'; } private extractDescription(content: string): string { // Look for description patterns const descPatterns = [ /description:\s*([^\n]+)/i, /about:\s*([^\n]+)/i, /these\s+(?:flashcards?|cards?)\s+(?:cover|include|help\s+with):\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); const purpose = this.determineFlashcardsPurpose(content); const purposeMap = { vocabulary: `Learn and memorize ${topic} vocabulary`, concepts: `Study key concepts in ${topic}`, definitions: `Master important ${topic} definitions`, facts: `Remember essential ${topic} facts`, formulas: `Practice ${topic} formulas and equations`, general: `Study ${topic} with interactive flashcards`, }; return purposeMap[purpose as keyof typeof purposeMap] || `Interactive flashcards for ${topic}`; } private generateCardsFromContent(content: string): FlashCard[] { const cards: FlashCard[] = []; // Strategy 1: Extract explicit card pairs const explicitCards = this.extractExplicitCards(content); cards.push(...explicitCards); // Strategy 2: Extract definition pairs if (cards.length < 3) { const definitionCards = this.extractDefinitionCards(content); cards.push(...definitionCards); } // Strategy 3: Extract Q&A pairs if (cards.length < 3) { const qaCards = this.extractQuestionAnswerCards(content); cards.push(...qaCards); } // Strategy 4: Generate cards from key sentences if (cards.length < 3) { const sentenceCards = this.generateCardsFromSentences(content); cards.push(...sentenceCards); } // Strategy 5: Create default cards if still insufficient if (cards.length === 0) { cards.push(...this.createDefaultCards(content)); } // Enhance cards with metadata return cards.map(card => this.enhanceCard(card, content)); } private extractExplicitCards(content: string): FlashCard[] { const cards: FlashCard[] = []; // Look for explicit front/back patterns const cardPatterns = [ /(?:front|question|term):\s*([^\n]+)\n(?:back|answer|definition):\s*([^\n]+)/gi, /([^|]+)\s*\|\s*([^|\n]+)/g, // Pipe-separated format /([^:]+):\s*([^\n]+)/g, // Colon-separated format ]; cardPatterns.forEach(pattern => { let match; while ((match = pattern.exec(content)) !== null && cards.length < 20) { const front = match[1].trim(); const back = match[2].trim(); if (front.length > 2 && back.length > 2 && front !== back) { cards.push({ id: this.generateId('card'), front, back, category: this.classifyCardContent(front, back), }); } } }); return cards; } private extractDefinitionCards(content: string): FlashCard[] { const cards: FlashCard[] = []; // Look for definition patterns: "X is Y", "X means Y", "X: Y" const definitionPatterns = [ /([^.!?]+?)\s+(?:is|are|means?|refers?\s+to|defines?)\s+([^.!?]+)/gi, /([^:]+):\s*([^.!?\n]+)/g, ]; definitionPatterns.forEach(pattern => { let match; while ((match = pattern.exec(content)) !== null && cards.length < 15) { const term = match[1].trim(); const definition = match[2].trim(); if (term.length > 2 && definition.length > 5 && !this.isCommonPhrase(term)) { cards.push({ id: this.generateId('card'), front: `What is ${term}?`, back: definition, category: 'definition', }); } } }); return cards; } private extractQuestionAnswerCards(content: string): FlashCard[] { const cards: FlashCard[] = []; // Look for question patterns const questionPattern = /([^.!]*\?)\s*([^.!?]+)/g; let match; while ((match = questionPattern.exec(content)) !== null && cards.length < 10) { const question = match[1].trim(); const answer = match[2].trim(); if (question.length > 5 && answer.length > 3) { cards.push({ id: this.generateId('card'), front: question, back: answer, category: 'qa', }); } } return cards; } private generateCardsFromSentences(content: string): FlashCard[] { const cards: FlashCard[] = []; const sentences = content .split(/[.!]+/) .filter(s => s.trim().length > 20) .slice(0, 8); sentences.forEach(sentence => { const cleanSentence = sentence.trim(); if (this.isFactualStatement(cleanSentence)) { const { front, back } = this.convertSentenceToCard(cleanSentence); cards.push({ id: this.generateId('card'), front, back, category: 'concept', }); } }); return cards; } private createDefaultCards(content: string): FlashCard[] { const topic = this.extractMainTopic(content); const snippet = this.extractSnippet(content, 100); return [ { id: this.generateId('card'), front: `What is the main topic?`, back: topic, category: 'general', }, { id: this.generateId('card'), front: `Key information about ${topic}`, back: snippet, category: 'general', }, { id: this.generateId('card'), front: `Why is ${topic} important?`, back: 'This topic is fundamental to understanding the subject matter.', category: 'general', }, ]; } private enhanceCard(card: FlashCard, content: string): FlashCard { return { ...card, difficulty: this.assessCardDifficulty(card), tags: this.generateCardTags(card, content), }; } private classifyCardContent(front: string, back: string): FlashCard['category'] { const frontLower = front.toLowerCase(); const backLower = back.toLowerCase(); if (frontLower.includes('what is') || frontLower.includes('define')) { return 'definition'; } if (frontLower.includes('?')) { return 'qa'; } if (backLower.includes('formula') || backLower.includes('equation') || /\d+/.test(back)) { return 'formula'; } if (this.isFactualStatement(back)) { return 'fact'; } return 'concept'; } 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 isCommonPhrase(text: string): boolean { const commonPhrases = ['the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by']; return commonPhrases.some(phrase => text.toLowerCase().trim() === phrase); } private convertSentenceToCard(sentence: string): { front: string; back: string } { const lowerSentence = sentence.toLowerCase(); // Convert "X is Y" to "What is X?" / "Y" if (lowerSentence.includes(' is ')) { const parts = sentence.split(/\s+is\s+/i); if (parts.length === 2) { return { front: `What is ${parts[0].trim()}?`, back: parts[1].trim(), }; } } // Convert "X has Y" to "What does X have?" / "Y" if (lowerSentence.includes(' has ')) { const parts = sentence.split(/\s+has\s+/i); if (parts.length === 2) { return { front: `What does ${parts[0].trim()} have?`, back: parts[1].trim(), }; } } // Default conversion - use first half as question const words = sentence.split(' '); const midpoint = Math.floor(words.length / 2); return { front: `Complete this statement: ${words.slice(0, midpoint).join(' ')}...`, back: words.slice(midpoint).join(' '), }; } private assessCardDifficulty(card: FlashCard): FlashCard['difficulty'] { const frontLength = card.front.length; const backLength = card.back.length; const complexity = frontLength + backLength; if (complexity < 50) return 'easy'; if (complexity < 150) return 'medium'; return 'hard'; } private generateCardTags(card: FlashCard, content: string): string[] { const tags: string[] = []; // Add category as tag if (card.category) { tags.push(card.category); } // Add topic-based tags const topic = this.extractMainTopic(content); if (topic) { tags.push(topic); } // Add content-based tags const cardText = `${card.front} ${card.back}`.toLowerCase(); const keywords = this.extractKeywords(cardText); tags.push(...keywords.slice(0, 3)); return [...new Set(tags)]; // Remove duplicates } private extractKeywords(text: string): string[] { const words = text .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 3); const frequency: Record<string, number> = {}; words.forEach(word => { frequency[word] = (frequency[word] || 0) + 1; }); return Object.entries(frequency) .sort(([,a], [,b]) => b - a) .slice(0, 5) .map(([word]) => word); } private determineFlashcardsPurpose(content: string): string { const lowerContent = content.toLowerCase(); if (lowerContent.includes('vocabulary') || lowerContent.includes('words') || lowerContent.includes('language')) { return 'vocabulary'; } if (lowerContent.includes('definition') || lowerContent.includes('define') || lowerContent.includes('meaning')) { return 'definitions'; } if (lowerContent.includes('formula') || lowerContent.includes('equation') || lowerContent.includes('calculation')) { return 'formulas'; } if (lowerContent.includes('fact') || lowerContent.includes('data') || lowerContent.includes('statistic')) { return 'facts'; } if (lowerContent.includes('concept') || lowerContent.includes('principle') || lowerContent.includes('theory')) { return 'concepts'; } return 'general'; } private generateSettings(purpose: string, userSettings?: Partial<FlashcardsProperties['settings']>): FlashcardsProperties['settings'] { const baseSettings = { vocabulary: { shuffle: true, showProgress: true, autoAdvance: false, flipAnimation: 'flip' as const, studyMode: 'spaced' as const, showHints: false, enableAudio: true, }, definitions: { shuffle: false, showProgress: true, autoAdvance: false, flipAnimation: 'slide' as const, studyMode: 'linear' as const, showHints: true, enableAudio: false, }, formulas: { shuffle: false, showProgress: true, autoAdvance: false, flipAnimation: 'fade' as const, studyMode: 'linear' as const, showHints: false, enableAudio: false, }, facts: { shuffle: true, showProgress: true, autoAdvance: false, flipAnimation: 'flip' as const, studyMode: 'random' as const, showHints: false, enableAudio: false, }, concepts: { shuffle: false, showProgress: true, autoAdvance: false, flipAnimation: 'slide' as const, studyMode: 'linear' as const, showHints: true, enableAudio: false, }, general: { shuffle: true, showProgress: true, autoAdvance: false, flipAnimation: 'flip' as const, studyMode: 'spaced' as const, showHints: true, enableAudio: false, }, }; return { ...baseSettings[purpose as keyof typeof baseSettings] || baseSettings.general, ...userSettings, }; } private generateLearningSettings(purpose: string, userLearning?: Partial<FlashcardsProperties['learning']>): FlashcardsProperties['learning'] { const baseLearning = { vocabulary: { masteryThreshold: 5, reviewInterval: 1, spacedRepetition: true, adaptiveDifficulty: true, }, definitions: { masteryThreshold: 3, reviewInterval: 2, spacedRepetition: false, adaptiveDifficulty: false, }, formulas: { masteryThreshold: 7, reviewInterval: 1, spacedRepetition: true, adaptiveDifficulty: true, }, facts: { masteryThreshold: 4, reviewInterval: 3, spacedRepetition: true, adaptiveDifficulty: false, }, concepts: { masteryThreshold: 3, reviewInterval: 2, spacedRepetition: false, adaptiveDifficulty: true, }, general: { masteryThreshold: 3, reviewInterval: 2, spacedRepetition: true, adaptiveDifficulty: true, }, }; return { ...baseLearning[purpose as keyof typeof baseLearning] || baseLearning.general, ...userLearning, }; } private generateStyling(purpose: string, userStyling?: Partial<FlashcardsProperties['styling']>): FlashcardsProperties['styling'] { const baseStyling = { vocabulary: { cardTheme: 'colorful' as const, fontsize: 'large' as const, accentColor: '#4CAF50', }, definitions: { cardTheme: 'minimal' as const, fontsize: 'medium' as const, accentColor: '#2196F3', }, formulas: { cardTheme: 'modern' as const, fontsize: 'large' as const, accentColor: '#FF9800', }, facts: { cardTheme: 'default' as const, fontsize: 'medium' as const, accentColor: '#9C27B0', }, concepts: { cardTheme: 'minimal' as const, fontsize: 'medium' as const, accentColor: '#607D8B', }, general: { cardTheme: 'default' as const, fontsize: 'medium' as const, accentColor: '#3F51B5', }, }; return { ...baseStyling[purpose as keyof typeof baseStyling] || baseStyling.general, ...userStyling, }; } private generateInteractionSettings(purpose: string, userInteraction?: Partial<FlashcardsProperties['interaction']>): FlashcardsProperties['interaction'] { return { swipeGestures: true, keyboardShortcuts: true, clickToFlip: true, doubleClickMastery: true, ...userInteraction, }; } private extractMainTopic(content: string): string { // Same topic extraction as other plugins const words = content.toLowerCase() .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 3); const frequency: Record<string, number> = {}; words.forEach(word => { frequency[word] = (frequency[word] || 0) + 1; }); 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', 'this', 'that', 'with', 'have', 'from', 'they', 'know', 'want', 'been', 'good', 'much', 'some', 'time', 'very', 'when', 'come', 'here', 'just', 'like', 'long', 'make', 'many', 'over', 'such', 'take', 'than', 'them', 'well', 'were']; const topWords = Object.entries(frequency) .filter(([word]) => !commonWords.includes(word)) .sort(([,a], [,b]) => b - a); return topWords.length > 0 ? topWords[0][0] : 'content'; } private capitalizeFirst(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } private convertToComposerFlashcards(cards: FlashCard[]): FlashcardItem[] { return cards.map(card => ({ id: card.id, front_card: { text: card.front, centered_image: card.imageUrl || null, fullscreen_image: null }, back_card: { text: card.back, centered_image: null, fullscreen_image: null }, opened: false })); } }

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