Spaces:
Runtime error
Runtime error
| import React, { useState } from 'react'; | |
| import { ArrowLeft, ExternalLink, BookOpen, Clock, Share2, Bookmark, Eye, Sparkles, Wand2, Loader2 } from 'lucide-react'; | |
| import { WikimediaAPI } from '../utils/wikimedia-api'; | |
| interface ArticleViewerProps { | |
| title: string; | |
| project: string; | |
| content: string; | |
| onBack: () => void; | |
| onCreateStudyPlan?: (topic: string) => void; | |
| onTransformContent?: (title: string, content: string) => void; | |
| onViewArticle?: (title: string, project: string, content: string) => void; | |
| } | |
| const ArticleViewer: React.FC<ArticleViewerProps> = ({ | |
| title, | |
| project, | |
| content, | |
| onBack, | |
| onCreateStudyPlan, | |
| onTransformContent, | |
| onViewArticle | |
| }) => { | |
| const [fontSize, setFontSize] = useState('text-base'); | |
| const [readingTime, setReadingTime] = useState(0); | |
| const [relatedArticles, setRelatedArticles] = useState<any[]>([]); | |
| const [loadingRelated, setLoadingRelated] = useState(false); | |
| const [showRelated, setShowRelated] = useState(false); | |
| React.useEffect(() => { | |
| // Calculate reading time (average 200 words per minute) | |
| const wordCount = content.split(' ').length; | |
| const time = Math.ceil(wordCount / 200); | |
| setReadingTime(time); | |
| }, [content]); | |
| const formatContent = (text: string) => { | |
| // Split content into paragraphs and format | |
| const paragraphs = text.split('\n').filter(p => p.trim().length > 0); | |
| return paragraphs.map((paragraph, index) => { | |
| // Check if it's a heading (starts with ==) | |
| if (paragraph.startsWith('==') && paragraph.endsWith('==')) { | |
| const headingText = paragraph.replace(/=/g, '').trim(); | |
| return ( | |
| <h2 key={index} className="text-2xl font-bold text-gray-900 mt-8 mb-4 first:mt-0"> | |
| {headingText} | |
| </h2> | |
| ); | |
| } | |
| // Regular paragraph | |
| return ( | |
| <p key={index} className={`${fontSize} text-gray-700 leading-relaxed mb-4`}> | |
| {paragraph} | |
| </p> | |
| ); | |
| }); | |
| }; | |
| const getProjectUrl = () => { | |
| const baseUrls: Record<string, string> = { | |
| wikipedia: 'https://en.wikipedia.org/wiki/', | |
| 'en-wikipedia': 'https://en.wikipedia.org/wiki/', | |
| 'es-wikipedia': 'https://es.wikipedia.org/wiki/', | |
| 'fr-wikipedia': 'https://fr.wikipedia.org/wiki/', | |
| 'de-wikipedia': 'https://de.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, '_')); | |
| }; | |
| const handleShare = async () => { | |
| if (navigator.share) { | |
| try { | |
| await navigator.share({ | |
| title: title, | |
| text: `Check out this article about ${title}`, | |
| url: window.location.href, | |
| }); | |
| } catch (error) { | |
| console.log('Error sharing:', error); | |
| } | |
| } else { | |
| // Fallback: copy to clipboard | |
| navigator.clipboard.writeText(window.location.href); | |
| } | |
| }; | |
| const handleCreateStudyPlan = () => { | |
| if (onCreateStudyPlan) { | |
| onCreateStudyPlan(title); | |
| } | |
| }; | |
| const handleTransformContent = () => { | |
| if (onTransformContent) { | |
| onTransformContent(title, content); | |
| } | |
| }; | |
| const handleFindRelated = async () => { | |
| setLoadingRelated(true); | |
| setShowRelated(true); | |
| try { | |
| // Search for related articles using keywords from the title | |
| const searchTerms = title.split(' ').filter(term => term.length > 3); | |
| const relatedResults = []; | |
| // Search for each term and collect results | |
| for (const term of searchTerms.slice(0, 2)) { | |
| const results = await WikimediaAPI.search(term, 'wikipedia', 3); | |
| relatedResults.push(...results.filter(r => r.title !== title)); | |
| } | |
| // Remove duplicates and limit to 6 results | |
| const uniqueResults = relatedResults.filter((result, index, self) => | |
| index === self.findIndex(r => r.title === result.title) | |
| ).slice(0, 6); | |
| setRelatedArticles(uniqueResults); | |
| } catch (error) { | |
| console.error('Failed to find related articles:', error); | |
| setRelatedArticles([]); | |
| } finally { | |
| setLoadingRelated(false); | |
| } | |
| }; | |
| const handleViewRelated = async (article: any) => { | |
| try { | |
| const content = await WikimediaAPI.getPageContent(article.title, article.project); | |
| if (onViewArticle) { | |
| onViewArticle(article.title, article.project, content); | |
| } | |
| } catch (error) { | |
| console.error('Failed to load related article:', error); | |
| } | |
| }; | |
| return ( | |
| <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
| {/* Header */} | |
| <div className="mb-8"> | |
| <button | |
| onClick={onBack} | |
| className="flex items-center space-x-2 text-primary-600 hover:text-primary-700 font-medium mb-6 transition-colors" | |
| > | |
| <ArrowLeft className="w-4 h-4" /> | |
| <span>Back</span> | |
| </button> | |
| <div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm"> | |
| <div className="flex items-start justify-between mb-4"> | |
| <div className="flex-1"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <span className="px-3 py-1 bg-primary-100 text-primary-700 text-sm rounded-full font-medium capitalize"> | |
| {project.replace('-wikipedia', '')} | |
| </span> | |
| <div className="flex items-center text-sm text-gray-600"> | |
| <Clock className="w-4 h-4 mr-1" /> | |
| <span>{readingTime} min read</span> | |
| </div> | |
| </div> | |
| <h1 className="text-3xl font-bold text-gray-900 mb-2">{title}</h1> | |
| </div> | |
| </div> | |
| {/* Article Controls */} | |
| <div className="flex items-center justify-between pt-4 border-t border-gray-200"> | |
| <div className="flex items-center space-x-4"> | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-sm text-gray-600">Font size:</span> | |
| <select | |
| value={fontSize} | |
| onChange={(e) => setFontSize(e.target.value)} | |
| className="text-sm border border-gray-300 rounded px-2 py-1" | |
| > | |
| <option value="text-sm">Small</option> | |
| <option value="text-base">Medium</option> | |
| <option value="text-lg">Large</option> | |
| <option value="text-xl">Extra Large</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <button | |
| onClick={handleShare} | |
| className="flex items-center space-x-1 px-3 py-2 text-gray-600 hover:text-gray-900 transition-colors" | |
| > | |
| <Share2 className="w-4 h-4" /> | |
| <span className="text-sm">Share</span> | |
| </button> | |
| <button className="flex items-center space-x-1 px-3 py-2 text-gray-600 hover:text-gray-900 transition-colors"> | |
| <Bookmark className="w-4 h-4" /> | |
| <span className="text-sm">Save</span> | |
| </button> | |
| <a | |
| href={getProjectUrl()} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center space-x-1 px-3 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors" | |
| > | |
| <ExternalLink className="w-4 h-4" /> | |
| <span className="text-sm">View Original</span> | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Article Content */} | |
| <div className="bg-white rounded-2xl p-8 border border-gray-200 shadow-sm"> | |
| <div className="prose max-w-none"> | |
| {formatContent(content)} | |
| </div> | |
| {content.length < 500 && ( | |
| <div className="mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg"> | |
| <p className="text-yellow-800 text-sm"> | |
| This appears to be a short excerpt. For the complete article with images, references, and full content, | |
| please visit the original source. | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Related Actions */} | |
| <div className="mt-8 bg-white rounded-2xl p-6 border border-gray-200 shadow-sm"> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-4">What's Next?</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <button | |
| onClick={handleCreateStudyPlan} | |
| className="flex items-center space-x-3 p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors group" | |
| > | |
| <Sparkles className="w-6 h-6 text-primary-600 group-hover:scale-110 transition-transform" /> | |
| <div className="text-left"> | |
| <div className="font-medium text-gray-900">Create Study Plan</div> | |
| <div className="text-sm text-gray-600">Generate a learning path from this topic</div> | |
| </div> | |
| </button> | |
| <button | |
| onClick={handleFindRelated} | |
| disabled={loadingRelated} | |
| className="flex items-center space-x-3 p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors group disabled:opacity-50" | |
| > | |
| {loadingRelated ? ( | |
| <Loader2 className="w-6 h-6 text-primary-600 animate-spin" /> | |
| ) : ( | |
| <Eye className="w-6 h-6 text-primary-600 group-hover:scale-110 transition-transform" /> | |
| )} | |
| <div className="text-left"> | |
| <div className="font-medium text-gray-900">Find Related</div> | |
| <div className="text-sm text-gray-600"> | |
| {loadingRelated ? 'Searching...' : 'Discover similar articles'} | |
| </div> | |
| </div> | |
| </button> | |
| <button | |
| onClick={handleTransformContent} | |
| className="flex items-center space-x-3 p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors group" | |
| > | |
| <Wand2 className="w-6 h-6 text-primary-600 group-hover:scale-110 transition-transform" /> | |
| <div className="text-left"> | |
| <div className="font-medium text-gray-900">Transform Content</div> | |
| <div className="text-sm text-gray-600">Create quiz or summary</div> | |
| </div> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Related Articles Section */} | |
| {showRelated && ( | |
| <div className="mt-8 bg-white rounded-2xl p-6 border border-gray-200 shadow-sm"> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-4">Related Articles</h3> | |
| {loadingRelated ? ( | |
| <div className="flex items-center justify-center py-8"> | |
| <Loader2 className="w-6 h-6 animate-spin text-primary-600" /> | |
| <span className="ml-2 text-gray-600">Finding related articles...</span> | |
| </div> | |
| ) : relatedArticles.length > 0 ? ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {relatedArticles.map((article, index) => ( | |
| <div | |
| key={index} | |
| className="p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors cursor-pointer" | |
| onClick={() => handleViewRelated(article)} | |
| > | |
| <h4 className="font-medium text-gray-900 mb-2">{article.title}</h4> | |
| <p className="text-sm text-gray-600 mb-2"> | |
| {article.snippet.replace(/<[^>]*>/g, '').substring(0, 100)}... | |
| </p> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs text-primary-600 bg-primary-100 px-2 py-1 rounded-full"> | |
| {article.project} | |
| </span> | |
| <button className="text-sm text-primary-600 hover:text-primary-700"> | |
| Read more → | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="text-center py-8"> | |
| <p className="text-gray-600">No related articles found. Try searching for specific topics.</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default ArticleViewer; |