Spaces:
Runtime error
Runtime error
| import React, { useState } from 'react'; | |
| import { Sparkles, BookOpen, Clock, Target, Loader2, CheckCircle, ArrowRight, Settings, Zap, AlertCircle } from 'lucide-react'; | |
| import { AIEnhancedWikimedia } from '../utils/ai-enhanced-wikimedia'; | |
| import { AIProviderManager } from '../utils/ai-providers'; | |
| import { StudyPlan } from '../types'; | |
| interface AIStudyPlanGeneratorProps { | |
| onPlanGenerated: (plan: StudyPlan) => void; | |
| } | |
| const AIStudyPlanGenerator: React.FC<AIStudyPlanGeneratorProps> = ({ onPlanGenerated }) => { | |
| const [topic, setTopic] = useState(''); | |
| const [difficulty, setDifficulty] = useState<'beginner' | 'intermediate' | 'advanced'>('beginner'); | |
| const [loading, setLoading] = useState(false); | |
| const [generatedPlan, setGeneratedPlan] = useState<StudyPlan | null>(null); | |
| const [useAI, setUseAI] = useState(true); | |
| const [aiStatus, setAiStatus] = useState<'checking' | 'available' | 'unavailable'>('checking'); | |
| const [showAIConfig, setShowAIConfig] = useState(false); | |
| React.useEffect(() => { | |
| checkAIAvailability(); | |
| }, []); | |
| const checkAIAvailability = async () => { | |
| setAiStatus('checking'); | |
| try { | |
| const config = AIProviderManager.getConfig(); | |
| // Check if any provider is configured | |
| if (!config.selectedProvider) { | |
| setAiStatus('unavailable'); | |
| return; | |
| } | |
| // For providers that require API keys, check if key exists | |
| const provider = AIProviderManager.getProviderById(config.selectedProvider); | |
| if (provider?.requiresApiKey) { | |
| const hasApiKey = config.apiKeys && config.apiKeys[config.selectedProvider]; | |
| if (!hasApiKey) { | |
| setAiStatus('unavailable'); | |
| return; | |
| } | |
| } | |
| // Test actual connection | |
| const isAvailable = await AIProviderManager.testConnection(config.selectedProvider); | |
| setAiStatus(isAvailable ? 'available' : 'unavailable'); | |
| } catch (error) { | |
| console.error('AI availability check failed:', error); | |
| setAiStatus('unavailable'); | |
| } | |
| }; | |
| const handleGenerate = async () => { | |
| if (!topic.trim()) return; | |
| setLoading(true); | |
| try { | |
| let plan: StudyPlan; | |
| if (useAI && aiStatus === 'available') { | |
| // Use AI-enhanced generation | |
| plan = await AIEnhancedWikimedia.generateEnhancedStudyPlan(topic, difficulty); | |
| } else { | |
| // Use standard Wikimedia-based generation | |
| plan = await AIEnhancedWikimedia.generateStudyPlan(topic, difficulty); | |
| } | |
| setGeneratedPlan(plan); | |
| } catch (error) { | |
| console.error('Failed to generate study plan:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleAcceptPlan = () => { | |
| if (generatedPlan) { | |
| onPlanGenerated(generatedPlan); | |
| setGeneratedPlan(null); | |
| setTopic(''); | |
| } | |
| }; | |
| const getDifficultyColor = (level: string) => { | |
| switch (level) { | |
| case 'beginner': return 'bg-success-100 text-success-800'; | |
| case 'intermediate': return 'bg-warning-100 text-warning-800'; | |
| case 'advanced': return 'bg-error-100 text-error-800'; | |
| default: return 'bg-gray-100 text-gray-800'; | |
| } | |
| }; | |
| const getAIStatusMessage = () => { | |
| const config = AIProviderManager.getConfig(); | |
| const provider = AIProviderManager.getProviderById(config.selectedProvider); | |
| switch (aiStatus) { | |
| case 'checking': | |
| return 'Testing connection to configured AI provider...'; | |
| case 'available': | |
| return `Using ${provider?.name || 'AI'} for enhanced study plan generation`; | |
| case 'unavailable': | |
| if (!config.selectedProvider) { | |
| return 'No AI provider configured. Click "Configure AI" to set up open-source AI.'; | |
| } | |
| if (provider?.requiresApiKey && !config.apiKeys?.[config.selectedProvider]) { | |
| return `${provider.name} requires an API key. Click "Configure AI" to add it.`; | |
| } | |
| return `Cannot connect to ${provider?.name || 'AI provider'}. Using Wikimedia content analysis instead.`; | |
| default: | |
| return 'Checking AI availability...'; | |
| } | |
| }; | |
| return ( | |
| <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
| <div className="bg-gradient-to-br from-primary-50 to-secondary-50 rounded-2xl p-8 border border-primary-100"> | |
| <div className="flex items-center space-x-3 mb-6"> | |
| <div className="w-12 h-12 bg-gradient-to-r from-primary-500 to-secondary-500 rounded-xl flex items-center justify-center"> | |
| <Sparkles className="w-6 h-6 text-white" /> | |
| </div> | |
| <div> | |
| <h2 className="text-2xl font-bold text-gray-900">AI Study Plan Generator</h2> | |
| <p className="text-gray-600">Create personalized learning paths from Wikimedia content</p> | |
| </div> | |
| </div> | |
| {/* 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-start justify-between"> | |
| <div className="flex items-start space-x-3 flex-1"> | |
| {aiStatus === 'checking' && <Loader2 className="w-5 h-5 animate-spin text-gray-600 mt-0.5" />} | |
| {aiStatus === 'available' && <Zap className="w-5 h-5 text-green-600 mt-0.5" />} | |
| {aiStatus === 'unavailable' && <AlertCircle className="w-5 h-5 text-yellow-600 mt-0.5" />} | |
| <div className="flex-1"> | |
| <div className="font-medium text-gray-900 mb-1"> | |
| {aiStatus === 'checking' && 'Checking AI Availability...'} | |
| {aiStatus === 'available' && 'AI Enhancement Available'} | |
| {aiStatus === 'unavailable' && 'AI Enhancement Unavailable'} | |
| </div> | |
| <div className="text-sm text-gray-600"> | |
| {getAIStatusMessage()} | |
| </div> | |
| {aiStatus === 'unavailable' && ( | |
| <button | |
| onClick={() => setShowAIConfig(true)} | |
| className="mt-2 text-sm text-primary-600 hover:text-primary-700 font-medium underline" | |
| > | |
| Configure AI Provider | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex items-center space-x-3 ml-4"> | |
| {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> | |
| )} | |
| <button | |
| onClick={() => setShowAIConfig(true)} | |
| className="flex items-center space-x-1 px-3 py-1 bg-gray-100 text-gray-700 rounded-lg text-sm hover:bg-gray-200 transition-colors" | |
| > | |
| <Settings className="w-3 h-3" /> | |
| <span>Configure</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {!generatedPlan ? ( | |
| <div className="space-y-6"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| What would you like to learn? | |
| </label> | |
| <input | |
| type="text" | |
| value={topic} | |
| onChange={(e) => setTopic(e.target.value)} | |
| placeholder="e.g., Machine Learning, Ancient History, Climate Science..." | |
| className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent text-lg" | |
| onKeyDown={(e) => e.key === 'Enter' && handleGenerate()} | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-3"> | |
| Difficulty Level | |
| </label> | |
| <div className="grid grid-cols-3 gap-3"> | |
| {(['beginner', 'intermediate', 'advanced'] as const).map((level) => ( | |
| <button | |
| key={level} | |
| onClick={() => setDifficulty(level)} | |
| className={`p-4 rounded-xl border-2 transition-all ${ | |
| difficulty === level | |
| ? 'border-primary-500 bg-primary-50 text-primary-700' | |
| : 'border-gray-200 bg-white text-gray-700 hover:border-gray-300' | |
| }`} | |
| > | |
| <div className="text-center"> | |
| <div className="font-medium capitalize">{level}</div> | |
| <div className="text-sm text-gray-500 mt-1"> | |
| {level === 'beginner' && '3-5 topics'} | |
| {level === 'intermediate' && '6-8 topics'} | |
| {level === 'advanced' && '9-12 topics'} | |
| </div> | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <button | |
| onClick={handleGenerate} | |
| disabled={loading || !topic.trim()} | |
| className="w-full flex items-center justify-center space-x-2 px-6 py-4 bg-gradient-to-r from-primary-600 to-secondary-600 text-white rounded-xl hover:from-primary-700 hover:to-secondary-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 generating your study plan...' | |
| : 'Generating Study Plan...' | |
| } | |
| </span> | |
| </> | |
| ) : ( | |
| <> | |
| <Sparkles className="w-5 h-5" /> | |
| <span> | |
| {useAI && aiStatus === 'available' | |
| ? 'Generate AI-Enhanced Study Plan' | |
| : 'Generate Study Plan' | |
| } | |
| </span> | |
| </> | |
| )} | |
| </button> | |
| {/* Quick Setup Guide */} | |
| {aiStatus === 'unavailable' && ( | |
| <div className="bg-blue-50 border border-blue-200 rounded-xl p-4"> | |
| <h4 className="font-medium text-blue-900 mb-3">🚀 Quick AI Setup</h4> | |
| <div className="space-y-3 text-sm text-blue-800"> | |
| <div className="flex items-start space-x-2"> | |
| <span className="font-medium">Option 1 (Privacy):</span> | |
| <div> | |
| <p>Install Ollama locally for complete privacy</p> | |
| <p className="text-blue-600">• Download from ollama.ai</p> | |
| <p className="text-blue-600">• Run: ollama pull llama3.2</p> | |
| </div> | |
| </div> | |
| <div className="flex items-start space-x-2"> | |
| <span className="font-medium">Option 2 (Cloud):</span> | |
| <div> | |
| <p>Get free Hugging Face API key</p> | |
| <p className="text-blue-600">• Sign up at huggingface.co</p> | |
| <p className="text-blue-600">• Add key in AI configuration</p> | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| onClick={checkAIAvailability} | |
| className="mt-3 text-sm text-blue-600 hover:text-blue-700 font-medium" | |
| > | |
| Recheck AI Availability | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="space-y-6"> | |
| <div className="bg-white rounded-xl p-6 border border-gray-200"> | |
| <div className="flex items-start justify-between mb-4"> | |
| <div className="flex-1"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <h3 className="text-xl font-bold text-gray-900">{generatedPlan.title}</h3> | |
| {useAI && aiStatus === 'available' && ( | |
| <span className="px-2 py-1 bg-purple-100 text-purple-700 text-xs rounded-full font-medium"> | |
| AI Enhanced | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-gray-600 mb-4">{generatedPlan.description}</p> | |
| <div className="flex items-center space-x-4"> | |
| <span className={`px-3 py-1 rounded-full text-sm font-medium ${getDifficultyColor(generatedPlan.difficulty)}`}> | |
| {generatedPlan.difficulty} | |
| </span> | |
| <div className="flex items-center text-sm text-gray-600"> | |
| <Clock className="w-4 h-4 mr-1" /> | |
| {generatedPlan.estimatedTime} | |
| </div> | |
| <div className="flex items-center text-sm text-gray-600"> | |
| <BookOpen className="w-4 h-4 mr-1" /> | |
| {generatedPlan.topics.length} topics | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-3"> | |
| <h4 className="font-medium text-gray-900">Study Topics:</h4> | |
| {generatedPlan.topics.slice(0, 5).map((topic, index) => ( | |
| <div key={topic.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg"> | |
| <div className="w-6 h-6 bg-primary-100 rounded-full flex items-center justify-center text-primary-600 text-sm font-medium"> | |
| {index + 1} | |
| </div> | |
| <div className="flex-1"> | |
| <div className="font-medium text-gray-900">{topic.title}</div> | |
| <div className="text-sm text-gray-600">{topic.estimatedTime}</div> | |
| </div> | |
| <CheckCircle className="w-5 h-5 text-gray-300" /> | |
| </div> | |
| ))} | |
| {generatedPlan.topics.length > 5 && ( | |
| <div className="text-sm text-gray-500 text-center py-2"> | |
| +{generatedPlan.topics.length - 5} more topics... | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex space-x-3"> | |
| <button | |
| onClick={() => setGeneratedPlan(null)} | |
| className="flex-1 px-4 py-3 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors" | |
| > | |
| Generate New Plan | |
| </button> | |
| <button | |
| onClick={handleAcceptPlan} | |
| className="flex-1 flex items-center justify-center space-x-2 px-4 py-3 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors" | |
| > | |
| <span>Accept Plan</span> | |
| <ArrowRight className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default AIStudyPlanGenerator; |