flashcards-plugin.ts•21.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
}));
}
}