wiki-project / src /components /AIStudyPlanGenerator.tsx
Nagi15's picture
Add codebase
fcb5a67
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;