wiki-project / src /utils /wikimedia-api.ts
Nagi15's picture
Add codebase
fcb5a67
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
};
}
}