Spaces:
Runtime error
Runtime error
| import React, { useState } from 'react'; | |
| import { Wand2, FileText, List, BookOpen, MessageSquare, Loader2, Settings, Zap } from 'lucide-react'; | |
| import { AIEnhancedWikimedia } from '../utils/ai-enhanced-wikimedia'; | |
| import { AIProviderManager } from '../utils/ai-providers'; | |
| const ContentTransformer: React.FC = () => { | |
| const [inputUrl, setInputUrl] = useState(''); | |
| const [transformationType, setTransformationType] = useState<'summary' | 'quiz' | 'outline' | 'flashcards'>('summary'); | |
| const [loading, setLoading] = useState(false); | |
| const [transformedContent, setTransformedContent] = useState<any>(null); | |
| const [selectedAnswers, setSelectedAnswers] = useState<Record<number, number>>({}); | |
| const [useAI, setUseAI] = useState(true); | |
| const [aiStatus, setAiStatus] = useState<'checking' | 'available' | 'unavailable'>('checking'); | |
| React.useEffect(() => { | |
| checkAIAvailability(); | |
| }, []); | |
| const checkAIAvailability = async () => { | |
| try { | |
| const config = AIProviderManager.getConfig(); | |
| const isAvailable = await AIProviderManager.testConnection(config.selectedProvider); | |
| setAiStatus(isAvailable ? 'available' : 'unavailable'); | |
| } catch (error) { | |
| setAiStatus('unavailable'); | |
| } | |
| }; | |
| const transformationTypes = [ | |
| { | |
| id: 'summary' as const, | |
| name: 'Summary', | |
| description: 'Create concise summaries', | |
| icon: FileText, | |
| color: 'bg-primary-500' | |
| }, | |
| { | |
| id: 'quiz' as const, | |
| name: 'Quiz', | |
| description: 'Generate practice questions', | |
| icon: MessageSquare, | |
| color: 'bg-secondary-500' | |
| }, | |
| { | |
| id: 'outline' as const, | |
| name: 'Study Outline', | |
| description: 'Structured learning outline', | |
| icon: List, | |
| color: 'bg-accent-500' | |
| }, | |
| { | |
| id: 'flashcards' as const, | |
| name: 'Flashcards', | |
| description: 'Key concepts for memorization', | |
| icon: BookOpen, | |
| color: 'bg-success-500' | |
| } | |
| ]; | |
| const extractTitleFromUrl = (url: string): string => { | |
| try { | |
| const urlObj = new URL(url); | |
| const pathParts = urlObj.pathname.split('/'); | |
| const title = pathParts[pathParts.length - 1]; | |
| return decodeURIComponent(title.replace(/_/g, ' ')); | |
| } catch { | |
| return ''; | |
| } | |
| }; | |
| const detectProject = (url: string): string => { | |
| if (url.includes('wikipedia.org')) return 'wikipedia'; | |
| if (url.includes('wikibooks.org')) return 'wikibooks'; | |
| if (url.includes('wikiversity.org')) return 'wikiversity'; | |
| if (url.includes('wikiquote.org')) return 'wikiquote'; | |
| if (url.includes('wiktionary.org')) return 'wiktionary'; | |
| if (url.includes('wikisource.org')) return 'wikisource'; | |
| return 'wikipedia'; | |
| }; | |
| const handleTransform = async () => { | |
| if (!inputUrl.trim()) return; | |
| setLoading(true); | |
| setSelectedAnswers({}); | |
| try { | |
| const title = extractTitleFromUrl(inputUrl); | |
| const project = detectProject(inputUrl); | |
| const content = await AIEnhancedWikimedia.getPageContent(title, project); | |
| let transformed; | |
| if (useAI && aiStatus === 'available') { | |
| // Use AI-enhanced transformation | |
| switch (transformationType) { | |
| case 'summary': | |
| transformed = await AIEnhancedWikimedia.generateSummaryFromContent(content, title); | |
| break; | |
| case 'quiz': | |
| transformed = await AIEnhancedWikimedia.generateQuizFromContent(content, title); | |
| break; | |
| case 'outline': | |
| transformed = await AIEnhancedWikimedia.generateStudyOutline(content, title); | |
| break; | |
| case 'flashcards': | |
| transformed = await AIEnhancedWikimedia.generateFlashcards(content, title); | |
| break; | |
| } | |
| } else { | |
| // Use fallback rule-based transformation | |
| switch (transformationType) { | |
| case 'summary': | |
| transformed = generateSummary(content, title); | |
| break; | |
| case 'quiz': | |
| transformed = generateQuiz(content, title); | |
| break; | |
| case 'outline': | |
| transformed = generateOutline(content, title); | |
| break; | |
| case 'flashcards': | |
| transformed = generateFlashcards(content, title); | |
| break; | |
| } | |
| } | |
| setTransformedContent(transformed); | |
| } catch (error) { | |
| console.error('Content transformation failed:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| // Fallback generation methods (existing code) | |
| const generateSummary = (content: string, title: string) => { | |
| const sentences = content.split('.').filter(s => s.trim().length > 20); | |
| const keySentences = sentences.slice(0, 5); | |
| return { | |
| type: 'summary', | |
| title: `Summary: ${title}`, | |
| content: keySentences.join('. ') + '.', | |
| keyPoints: sentences.slice(5, 10).map(s => s.trim()).filter(s => s.length > 0) | |
| }; | |
| }; | |
| const generateQuiz = (content: string, title: string) => { | |
| const questions = [ | |
| { | |
| question: `What is the main topic discussed in the article about ${title}?`, | |
| options: [ | |
| `The fundamental concepts and principles of ${title}`, | |
| `The historical development of ${title}`, | |
| `The practical applications of ${title}`, | |
| `The criticism and controversies surrounding ${title}` | |
| ], | |
| correct: 0 | |
| } | |
| ]; | |
| return { | |
| type: 'quiz', | |
| title: `Quiz: ${title}`, | |
| questions | |
| }; | |
| }; | |
| const generateOutline = (content: string, title: string) => { | |
| return { | |
| type: 'outline', | |
| title: `Study Outline: ${title}`, | |
| sections: [ | |
| { | |
| title: 'Introduction', | |
| points: [ | |
| `Overview and definition of ${title}`, | |
| 'Historical context and background', | |
| 'Key terminology and concepts' | |
| ] | |
| }, | |
| { | |
| title: 'Main Concepts', | |
| points: [ | |
| 'Core principles and theories', | |
| 'Important characteristics and features', | |
| 'Fundamental mechanisms and processes' | |
| ] | |
| } | |
| ] | |
| }; | |
| }; | |
| const generateFlashcards = (content: string, title: string) => { | |
| const cards = [ | |
| { | |
| front: `What is ${title}?`, | |
| back: `${title} is a comprehensive topic that encompasses various concepts, principles, and applications within its field of study.` | |
| }, | |
| { | |
| front: 'Key characteristics', | |
| back: `The main features include fundamental principles, practical applications, and significant impact on related areas.` | |
| } | |
| ]; | |
| return { | |
| type: 'flashcards', | |
| title: `Flashcards: ${title}`, | |
| cards | |
| }; | |
| }; | |
| const handleAnswerSelect = (questionIndex: number, answerIndex: number) => { | |
| setSelectedAnswers(prev => ({ | |
| ...prev, | |
| [questionIndex]: answerIndex | |
| })); | |
| }; | |
| const renderTransformedContent = () => { | |
| if (!transformedContent) return null; | |
| switch (transformedContent.type) { | |
| case 'summary': | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="prose max-w-none"> | |
| <p className="text-gray-700 leading-relaxed">{transformedContent.content}</p> | |
| </div> | |
| {transformedContent.keyPoints && ( | |
| <div> | |
| <h4 className="font-medium text-gray-900 mb-3">Key Points:</h4> | |
| <ul className="space-y-2"> | |
| {transformedContent.keyPoints.map((point: string, index: number) => ( | |
| <li key={index} className="flex items-start space-x-2"> | |
| <div className="w-2 h-2 bg-primary-500 rounded-full mt-2 flex-shrink-0" /> | |
| <span className="text-gray-700">{point}</span> | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| case 'quiz': | |
| return ( | |
| <div className="space-y-6"> | |
| {transformedContent.questions.map((q: any, index: number) => ( | |
| <div key={index} className="p-6 bg-gray-50 rounded-xl"> | |
| <h4 className="font-medium text-gray-900 mb-4 text-lg"> | |
| {index + 1}. {q.question} | |
| </h4> | |
| <div className="space-y-3"> | |
| {q.options.map((option: string, optIndex: number) => ( | |
| <label | |
| key={optIndex} | |
| className={`flex items-start space-x-3 cursor-pointer p-3 rounded-lg border-2 transition-all ${ | |
| selectedAnswers[index] === optIndex | |
| ? selectedAnswers[index] === q.correct | |
| ? 'border-success-500 bg-success-50' | |
| : 'border-error-500 bg-error-50' | |
| : 'border-gray-200 hover:border-gray-300 bg-white' | |
| }`} | |
| > | |
| <input | |
| type="radio" | |
| name={`q${index}`} | |
| value={optIndex} | |
| checked={selectedAnswers[index] === optIndex} | |
| onChange={() => handleAnswerSelect(index, optIndex)} | |
| className="mt-1 text-primary-600 focus:ring-primary-500" | |
| /> | |
| <span className="text-gray-700 flex-1">{option}</span> | |
| {selectedAnswers[index] !== undefined && optIndex === q.correct && ( | |
| <span className="text-success-600 font-medium text-sm">✓ Correct</span> | |
| )} | |
| </label> | |
| ))} | |
| </div> | |
| {selectedAnswers[index] !== undefined && q.explanation && ( | |
| <div className="mt-4 p-3 bg-blue-50 rounded-lg"> | |
| <p className="text-sm text-blue-800"> | |
| <strong>Explanation:</strong> {q.explanation} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| case 'outline': | |
| return ( | |
| <div className="space-y-6"> | |
| {transformedContent.sections.map((section: any, index: number) => ( | |
| <div key={index} className="border-l-4 border-primary-500 pl-6"> | |
| <h4 className="font-semibold text-gray-900 mb-3 text-lg">{section.title}</h4> | |
| <ul className="space-y-2"> | |
| {section.points.map((point: string, pointIndex: number) => ( | |
| <li key={pointIndex} className="flex items-start space-x-2"> | |
| <div className="w-1.5 h-1.5 bg-primary-500 rounded-full mt-2 flex-shrink-0" /> | |
| <span className="text-gray-700">{point}</span> | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| case 'flashcards': | |
| return ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {transformedContent.cards.map((card: any, index: number) => ( | |
| <div key={index} className="group perspective-1000"> | |
| <div className="relative w-full h-48 transition-transform duration-500 transform-style-preserve-3d group-hover:rotate-y-180"> | |
| <div className="absolute inset-0 w-full h-full backface-hidden bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl p-6 flex items-center justify-center text-white"> | |
| <div className="text-center"> | |
| <h4 className="font-semibold text-lg mb-2">Question {index + 1}</h4> | |
| <p className="text-primary-100">{card.front}</p> | |
| </div> | |
| </div> | |
| <div className="absolute inset-0 w-full h-full backface-hidden rotate-y-180 bg-white border-2 border-primary-200 rounded-xl p-6 flex items-center justify-center"> | |
| <div className="text-center"> | |
| <p className="text-gray-700 leading-relaxed">{card.back}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| default: | |
| return null; | |
| } | |
| }; | |
| return ( | |
| <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
| <div className="mb-8"> | |
| <div className="flex items-center space-x-3 mb-4"> | |
| <div className="w-12 h-12 bg-gradient-to-r from-accent-500 to-warning-500 rounded-xl flex items-center justify-center"> | |
| <Wand2 className="w-6 h-6 text-white" /> | |
| </div> | |
| <div> | |
| <h1 className="text-3xl font-bold text-gray-900">Content Transformer</h1> | |
| <p className="text-gray-600">Transform Wikimedia content into interactive learning materials</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm mb-8"> | |
| {/* AI Status Banner */} | |
| <div className={`mb-6 p-4 rounded-xl border ${ | |
| aiStatus === 'available' | |
| ? 'bg-green-50 border-green-200' | |
| : aiStatus === 'unavailable' | |
| ? 'bg-yellow-50 border-yellow-200' | |
| : 'bg-gray-50 border-gray-200' | |
| }`}> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-3"> | |
| {aiStatus === 'checking' && <Loader2 className="w-5 h-5 animate-spin text-gray-600" />} | |
| {aiStatus === 'available' && <Zap className="w-5 h-5 text-green-600" />} | |
| {aiStatus === 'unavailable' && <Settings className="w-5 h-5 text-yellow-600" />} | |
| <div> | |
| <div className="font-medium text-gray-900"> | |
| {aiStatus === 'available' && 'AI Enhancement Available'} | |
| {aiStatus === 'unavailable' && 'AI Enhancement Unavailable'} | |
| {aiStatus === 'checking' && 'Checking AI availability...'} | |
| </div> | |
| <div className="text-sm text-gray-600"> | |
| {aiStatus === 'available' && 'Using open-source AI for intelligent content transformation'} | |
| {aiStatus === 'unavailable' && 'Using rule-based transformation (still creates useful content!)'} | |
| </div> | |
| </div> | |
| </div> | |
| {aiStatus === 'available' && ( | |
| <label className="flex items-center space-x-2"> | |
| <input | |
| type="checkbox" | |
| checked={useAI} | |
| onChange={(e) => setUseAI(e.target.checked)} | |
| className="rounded border-gray-300 text-primary-600 focus:ring-primary-500" | |
| /> | |
| <span className="text-sm font-medium text-gray-700">Use AI Enhancement</span> | |
| </label> | |
| )} | |
| </div> | |
| </div> | |
| <div className="space-y-6"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Wikimedia Article URL | |
| </label> | |
| <input | |
| type="url" | |
| value={inputUrl} | |
| onChange={(e) => setInputUrl(e.target.value)} | |
| placeholder="https://en.wikipedia.org/wiki/Machine_Learning" | |
| className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-3"> | |
| Transformation Type | |
| </label> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-3"> | |
| {transformationTypes.map((type) => { | |
| const Icon = type.icon; | |
| return ( | |
| <button | |
| key={type.id} | |
| onClick={() => setTransformationType(type.id)} | |
| className={`p-4 rounded-xl border-2 transition-all ${ | |
| transformationType === type.id | |
| ? 'border-primary-500 bg-primary-50' | |
| : 'border-gray-200 bg-white hover:border-gray-300' | |
| }`} | |
| > | |
| <div className="text-center"> | |
| <div className={`w-10 h-10 ${type.color} rounded-lg flex items-center justify-center mx-auto mb-2`}> | |
| <Icon className="w-5 h-5 text-white" /> | |
| </div> | |
| <div className="font-medium text-gray-900">{type.name}</div> | |
| <div className="text-xs text-gray-500 mt-1">{type.description}</div> | |
| </div> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| <button | |
| onClick={handleTransform} | |
| disabled={loading || !inputUrl.trim()} | |
| className="w-full flex items-center justify-center space-x-2 px-6 py-4 bg-gradient-to-r from-accent-600 to-warning-600 text-white rounded-xl hover:from-accent-700 hover:to-warning-700 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| {loading ? ( | |
| <> | |
| <Loader2 className="w-5 h-5 animate-spin" /> | |
| <span> | |
| {useAI && aiStatus === 'available' | |
| ? 'AI is transforming content...' | |
| : 'Transforming Content...' | |
| } | |
| </span> | |
| </> | |
| ) : ( | |
| <> | |
| <Wand2 className="w-5 h-5" /> | |
| <span> | |
| {useAI && aiStatus === 'available' | |
| ? 'AI Transform Content' | |
| : 'Transform Content' | |
| } | |
| </span> | |
| </> | |
| )} | |
| </button> | |
| </div> | |
| </div> | |
| {transformedContent && ( | |
| <div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <h2 className="text-xl font-bold text-gray-900">{transformedContent.title}</h2> | |
| {useAI && aiStatus === 'available' && ( | |
| <span className="px-3 py-1 bg-purple-100 text-purple-700 text-sm rounded-full font-medium"> | |
| AI Enhanced | |
| </span> | |
| )} | |
| </div> | |
| {renderTransformedContent()} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default ContentTransformer; |