Spaces:
Runtime error
Runtime error
| import { WikimediaProject, SearchResult, StudyPlan } from '../types'; | |
| export const WIKIMEDIA_PROJECTS: WikimediaProject[] = [ | |
| { | |
| id: 'wikipedia', | |
| name: 'Wikipedia', | |
| description: 'The free encyclopedia', | |
| apiUrl: 'https://en.wikipedia.org/w/api.php', | |
| color: 'bg-primary-500', | |
| icon: 'Book' | |
| }, | |
| { | |
| id: 'wikibooks', | |
| name: 'Wikibooks', | |
| description: 'Free textbooks and manuals', | |
| apiUrl: 'https://en.wikibooks.org/w/api.php', | |
| color: 'bg-secondary-500', | |
| icon: 'BookOpen' | |
| }, | |
| { | |
| id: 'wikiquote', | |
| name: 'Wikiquote', | |
| description: 'Collection of quotations', | |
| apiUrl: 'https://en.wikiquote.org/w/api.php', | |
| color: 'bg-accent-500', | |
| icon: 'Quote' | |
| }, | |
| { | |
| id: 'wikiversity', | |
| name: 'Wikiversity', | |
| description: 'Free learning resources', | |
| apiUrl: 'https://en.wikiversity.org/w/api.php', | |
| color: 'bg-success-500', | |
| icon: 'GraduationCap' | |
| }, | |
| { | |
| id: 'wiktionary', | |
| name: 'Wiktionary', | |
| description: 'Free dictionary', | |
| apiUrl: 'https://en.wiktionary.org/w/api.php', | |
| color: 'bg-warning-500', | |
| icon: 'Languages' | |
| }, | |
| { | |
| id: 'wikisource', | |
| name: 'Wikisource', | |
| description: 'Free library of source texts', | |
| apiUrl: 'https://en.wikisource.org/w/api.php', | |
| color: 'bg-error-500', | |
| icon: 'FileText' | |
| } | |
| ]; | |
| export class WikimediaAPI { | |
| private static async makeRequest(apiUrl: string, params: Record<string, string>): Promise<any> { | |
| const url = new URL(apiUrl); | |
| Object.entries(params).forEach(([key, value]) => { | |
| url.searchParams.append(key, value); | |
| }); | |
| try { | |
| const response = await fetch(url.toString()); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('API request failed:', error); | |
| throw error; | |
| } | |
| } | |
| static async search(query: string, project: string = 'wikipedia', limit: number = 10): Promise<SearchResult[]> { | |
| const projectData = WIKIMEDIA_PROJECTS.find(p => p.id === project); | |
| if (!projectData) { | |
| throw new Error(`Unknown project: ${project}`); | |
| } | |
| const params = { | |
| action: 'query', | |
| format: 'json', | |
| list: 'search', | |
| srsearch: query, | |
| srlimit: limit.toString(), | |
| srprop: 'snippet|titlesnippet|size|timestamp', | |
| origin: '*' | |
| }; | |
| try { | |
| const data = await this.makeRequest(projectData.apiUrl, params); | |
| return data.query?.search?.map((result: any) => ({ | |
| title: result.title, | |
| pageid: result.pageid, | |
| size: result.size, | |
| snippet: result.snippet || 'No snippet available', | |
| timestamp: result.timestamp, | |
| project: project, | |
| url: this.buildProjectUrl(project, result.title) | |
| })) || []; | |
| } catch (error) { | |
| console.error(`Search failed for ${project}:`, error); | |
| return []; | |
| } | |
| } | |
| static async searchMultipleProjects(query: string, projects: string[] = ['wikipedia', 'wikibooks', 'wikiversity'], limit: number = 5): Promise<SearchResult[]> { | |
| const searchPromises = projects.map(project => this.search(query, project, limit)); | |
| try { | |
| const results = await Promise.allSettled(searchPromises); | |
| const allResults: SearchResult[] = []; | |
| results.forEach((result) => { | |
| if (result.status === 'fulfilled') { | |
| allResults.push(...result.value); | |
| } | |
| }); | |
| return allResults.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); | |
| } catch (error) { | |
| console.error('Multi-project search failed:', error); | |
| return []; | |
| } | |
| } | |
| static async getPageContent(title: string, project: string = 'wikipedia'): Promise<string> { | |
| const projectData = WIKIMEDIA_PROJECTS.find(p => p.id === project); | |
| if (!projectData) { | |
| throw new Error(`Unknown project: ${project}`); | |
| } | |
| // Handle language-specific Wikipedia projects | |
| let apiUrl = projectData.apiUrl; | |
| if (project.includes('-wikipedia')) { | |
| const langCode = project.split('-')[0]; | |
| apiUrl = `https://${langCode}.wikipedia.org/w/api.php`; | |
| } | |
| // Try multiple approaches to get the best content | |
| const approaches = [ | |
| // Approach 1: Get full extract | |
| { | |
| action: 'query', | |
| format: 'json', | |
| titles: title, | |
| prop: 'extracts', | |
| exintro: 'false', | |
| explaintext: 'true', | |
| exsectionformat: 'plain', | |
| origin: '*' | |
| }, | |
| // Approach 2: Get intro only if full content fails | |
| { | |
| action: 'query', | |
| format: 'json', | |
| titles: title, | |
| prop: 'extracts', | |
| exintro: 'true', | |
| explaintext: 'true', | |
| exsentences: '10', | |
| origin: '*' | |
| } | |
| ]; | |
| for (const params of approaches) { | |
| try { | |
| const data = await this.makeRequest(apiUrl, params); | |
| const pages = data.query?.pages || {}; | |
| const pageId = Object.keys(pages)[0]; | |
| if (pageId !== '-1' && pages[pageId]?.extract) { | |
| const content = pages[pageId].extract; | |
| if (content.length > 200) { | |
| return content; | |
| } | |
| } | |
| } catch (error) { | |
| console.log(`Approach failed for ${title}:`, error); | |
| continue; | |
| } | |
| } | |
| // Fallback: Return a helpful message | |
| const projectName = projectData.name; | |
| return `This article exists but the content could not be fully loaded through the API. | |
| The article "${title}" is available on ${projectName}, but due to API limitations or the article's structure, we cannot display the full content here. | |
| This might happen because: | |
| - The article is a disambiguation page | |
| - The content is primarily in tables, lists, or special formatting | |
| - The article has restricted content access | |
| - There are temporary API limitations | |
| For the complete article with all formatting, images, and references, please visit the original source using the "View Original" button.`; | |
| } | |
| static async getRandomArticles(project: string = 'wikipedia', count: number = 5): Promise<SearchResult[]> { | |
| const projectData = WIKIMEDIA_PROJECTS.find(p => p.id === project); | |
| if (!projectData) { | |
| throw new Error(`Unknown project: ${project}`); | |
| } | |
| const params = { | |
| action: 'query', | |
| format: 'json', | |
| list: 'random', | |
| rnnamespace: '0', | |
| rnlimit: count.toString(), | |
| origin: '*' | |
| }; | |
| try { | |
| const data = await this.makeRequest(projectData.apiUrl, params); | |
| const randomPages = data.query?.random || []; | |
| // Get snippets for random articles | |
| const results: SearchResult[] = []; | |
| for (const page of randomPages) { | |
| try { | |
| const snippet = await this.getPageSnippet(page.title, project); | |
| results.push({ | |
| title: page.title, | |
| pageid: page.id, | |
| size: 0, | |
| snippet: snippet, | |
| timestamp: new Date().toISOString(), | |
| project: project, | |
| url: this.buildProjectUrl(project, page.title) | |
| }); | |
| } catch (error) { | |
| // If we can't get snippet, still include the article | |
| results.push({ | |
| title: page.title, | |
| pageid: page.id, | |
| size: 0, | |
| snippet: `Random article from ${projectData.name}`, | |
| timestamp: new Date().toISOString(), | |
| project: project, | |
| url: this.buildProjectUrl(project, page.title) | |
| }); | |
| } | |
| } | |
| return results; | |
| } catch (error) { | |
| console.error(`Failed to get random articles from ${project}:`, error); | |
| return []; | |
| } | |
| } | |
| private static async getPageSnippet(title: string, project: string): Promise<string> { | |
| const projectData = WIKIMEDIA_PROJECTS.find(p => p.id === project); | |
| if (!projectData) { | |
| return 'No description available'; | |
| } | |
| const params = { | |
| action: 'query', | |
| format: 'json', | |
| titles: title, | |
| prop: 'extracts', | |
| exintro: 'true', | |
| explaintext: 'true', | |
| exsentences: '2', | |
| origin: '*' | |
| }; | |
| try { | |
| const data = await this.makeRequest(projectData.apiUrl, params); | |
| const pages = data.query?.pages || {}; | |
| const pageId = Object.keys(pages)[0]; | |
| if (pageId === '-1') { | |
| return 'No description available'; | |
| } | |
| const extract = pages[pageId]?.extract || 'No description available'; | |
| return extract.length > 200 ? extract.substring(0, 200) + '...' : extract; | |
| } catch (error) { | |
| return 'No description available'; | |
| } | |
| } | |
| static async generateStudyPlan(topic: string, difficulty: 'beginner' | 'intermediate' | 'advanced' = 'beginner'): Promise<StudyPlan> { | |
| try { | |
| // Search across multiple projects for comprehensive content | |
| const searchResults = await this.searchMultipleProjects(topic, ['wikipedia', 'wikibooks', 'wikiversity'], 15); | |
| if (searchResults.length === 0) { | |
| throw new Error('No content found for this topic'); | |
| } | |
| // Filter and organize results | |
| const topicCount = difficulty === 'beginner' ? 5 : difficulty === 'intermediate' ? 8 : 12; | |
| const selectedResults = searchResults.slice(0, topicCount); | |
| // Generate study plan structure with real data | |
| const studyPlan: StudyPlan = { | |
| id: `plan-${Date.now()}`, | |
| title: `${topic} Study Plan`, | |
| description: `Comprehensive ${difficulty} level study plan for ${topic} using real Wikimedia content`, | |
| difficulty, | |
| estimatedTime: this.estimateStudyTime(selectedResults.length, difficulty), | |
| created: new Date().toISOString(), | |
| topics: await this.generateTopicsFromResults(selectedResults, difficulty) | |
| }; | |
| return studyPlan; | |
| } catch (error) { | |
| console.error('Failed to generate study plan:', error); | |
| throw error; | |
| } | |
| } | |
| private static async generateTopicsFromResults(results: SearchResult[], difficulty: string): Promise<any[]> { | |
| const topics = []; | |
| for (let i = 0; i < results.length; i++) { | |
| const result = results[i]; | |
| // Get more detailed content for each topic | |
| let detailedContent = result.snippet; | |
| try { | |
| const fullContent = await this.getPageSnippet(result.title, result.project); | |
| if (fullContent && fullContent !== 'No description available') { | |
| detailedContent = fullContent; | |
| } | |
| } catch (error) { | |
| console.log(`Could not get detailed content for ${result.title}`); | |
| } | |
| topics.push({ | |
| id: `topic-${Date.now()}-${i}`, | |
| title: result.title, | |
| description: detailedContent.replace(/<[^>]*>/g, '').substring(0, 200) + '...', | |
| content: detailedContent, | |
| completed: false, | |
| estimatedTime: `${Math.ceil(Math.random() * 2 + 1)} hours`, | |
| resources: [ | |
| { | |
| title: result.title, | |
| url: result.url, | |
| type: 'article' as const, | |
| project: result.project | |
| } | |
| ] | |
| }); | |
| } | |
| return topics; | |
| } | |
| private static buildProjectUrl(project: string, title: string): string { | |
| const baseUrls: Record<string, string> = { | |
| wikipedia: 'https://en.wikipedia.org/wiki/', | |
| wikibooks: 'https://en.wikibooks.org/wiki/', | |
| wikiquote: 'https://en.wikiquote.org/wiki/', | |
| wikiversity: 'https://en.wikiversity.org/wiki/', | |
| wiktionary: 'https://en.wiktionary.org/wiki/', | |
| wikisource: 'https://en.wikisource.org/wiki/', | |
| }; | |
| const baseUrl = baseUrls[project] || baseUrls.wikipedia; | |
| return baseUrl + encodeURIComponent(title.replace(/ /g, '_')); | |
| } | |
| private static estimateStudyTime(contentCount: number, difficulty: string): string { | |
| const baseHours = contentCount * 1.5; // More realistic time estimate | |
| const multiplier = difficulty === 'beginner' ? 1 : difficulty === 'intermediate' ? 1.5 : 2; | |
| const totalHours = Math.ceil(baseHours * multiplier); | |
| if (totalHours < 24) { | |
| return `${totalHours} hours`; | |
| } else { | |
| const weeks = Math.ceil(totalHours / 10); // Assuming 10 hours per week | |
| return `${weeks} weeks`; | |
| } | |
| } | |
| // Real-time progress tracking methods | |
| static getStoredProgress(): any { | |
| try { | |
| const stored = localStorage.getItem('wikistro-progress'); | |
| return stored ? JSON.parse(stored) : this.getDefaultProgress(); | |
| } catch (error) { | |
| return this.getDefaultProgress(); | |
| } | |
| } | |
| static saveProgress(progressData: any): void { | |
| try { | |
| localStorage.setItem('wikistro-progress', JSON.stringify(progressData)); | |
| } catch (error) { | |
| console.error('Failed to save progress:', error); | |
| } | |
| } | |
| static getStoredStudyPlans(): StudyPlan[] { | |
| try { | |
| const stored = localStorage.getItem('wikistro-study-plans'); | |
| return stored ? JSON.parse(stored) : []; | |
| } catch (error) { | |
| return []; | |
| } | |
| } | |
| static saveStudyPlans(plans: StudyPlan[]): void { | |
| try { | |
| localStorage.setItem('wikistro-study-plans', JSON.stringify(plans)); | |
| } catch (error) { | |
| console.error('Failed to save study plans:', error); | |
| } | |
| } | |
| private static getDefaultProgress() { | |
| return { | |
| studyStreak: 0, | |
| topicsCompleted: 0, | |
| totalStudyTime: 0, | |
| achievements: 0, | |
| weeklyGoal: { current: 0, target: 12 }, | |
| recentActivity: [], | |
| completedTopics: [], | |
| lastStudyDate: null | |
| }; | |
| } | |
| } |