import React, { useState, useEffect, useRef } from 'react';
import { BookOpen, CheckSquare, MessageSquare, Clock, Settings, Zap, Trash2, Plus, Play, Pause, RotateCcw, Save, X, Brain, ChevronRight, GraduationCap, Download, Upload, AlertCircle, Edit3 } from 'lucide-react';
/**
* STUDY NEXUS v2.1 - Production Ready Student OS
* Fixes: Updated Model ID for compatibility, Added Custom Model setting.
*/
// --- UTILS & API ---
// Default to the preview model which is currently supported in this environment
const DEFAULT_MODEL = "gemini-2.5-flash-preview-09-2025";
const callGemini = async (apiKey, modelId, prompt, systemInstruction = "") => {
if (!apiKey) throw new Error("API Key is missing. Go to Settings to add it.");
// Use the user's custom model or fallback to default
const model = modelId || DEFAULT_MODEL;
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
const payload = {
contents: [{ role: "user", parts: [{ text: prompt }] }],
systemInstruction: { parts: [{ text: systemInstruction }] },
generationConfig: {
temperature: 0.7,
maxOutputTokens: 1000,
}
};
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
const errorMsg = data.error?.message || "Unknown API Error";
if (errorMsg.includes("not found")) throw new Error(`Model '${model}' not found. Try 'gemini-2.5-flash-preview-09-2025' in Settings.`);
if (errorMsg.includes("API key")) throw new Error("Invalid API Key. Please check settings.");
throw new Error(errorMsg);
}
if (!data.candidates || !data.candidates[0]?.content?.parts?.[0]?.text) {
throw new Error("AI returned an empty response. Try rephrasing.");
}
return data.candidates[0].content.parts[0].text;
} catch (error) {
console.error("Gemini API Error:", error);
throw error;
}
};
// --- HOOKS ---
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error("Storage Error:", error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error("Storage Save Error:", error);
}
};
return [storedValue, setValue];
};
// --- COMPONENTS ---
const Navigation = ({ activeTab, setActiveTab }) => {
const tabs = [
{ id: 'dashboard', icon: , label: 'Nexus' },
{ id: 'chat', icon: , label: 'AI Guru' },
{ id: 'tasks', icon: , label: 'Tasks' },
{ id: 'flashcards', icon: , label: 'Cards' },
{ id: 'focus', icon: , label: 'Focus' },
{ id: 'settings', icon: , label: 'Settings' },
];
return (
);
};
// 1. DASHBOARD
const Dashboard = ({ userName, stats, setActiveTab }) => (
setActiveTab('tasks')} className="bg-slate-800/50 p-6 rounded-2xl border border-slate-700 hover:border-cyan-500/50 transition-all cursor-pointer group hover:bg-slate-800">
Pending Tasks
setActiveTab('flashcards')} className="bg-slate-800/50 p-6 rounded-2xl border border-slate-700 hover:border-purple-500/50 transition-all cursor-pointer group hover:bg-slate-800">
Study Decks
setActiveTab('focus')} className="bg-slate-800/50 p-6 rounded-2xl border border-slate-700 hover:border-emerald-500/50 transition-all cursor-pointer group hover:bg-slate-800">
Mins Focused
setActiveTab('chat')} className="bg-gradient-to-br from-cyan-600/20 to-purple-600/20 p-6 rounded-2xl border border-slate-700 hover:border-cyan-400/50 transition-all cursor-pointer group relative overflow-hidden">
Ask AI Guru
Hinglish Tutor.
);
// 2. CHAT COMPONENT (GURU)
const AIChat = ({ apiKey, modelId, userName }) => {
const [messages, setMessages] = useState([
{ role: 'ai', text: `Namaste ${userName || 'Student'}! I am Guru. Study mein kya help chahiye aaj? I can explain any topic in Hinglish or English.` }
]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(scrollToBottom, [messages]);
const handleSend = async () => {
if (!input.trim()) return;
if (!apiKey) {
setMessages(prev => [...prev, { role: 'user', text: input }, { role: 'system', text: "⚠️ Error: No API Key found. Please go to Settings to add it." }]);
setInput('');
return;
}
const userMsg = input;
setMessages(prev => [...prev, { role: 'user', text: userMsg }]);
setInput('');
setLoading(true);
const systemPrompt = `You are "Guru", a smart and friendly student mentor for Indian students.
User Name: ${userName || 'Student'}
Language Preference: Mix of English and Hindi (Hinglish) unless requested otherwise.
Instructions:
1. Explain concepts simply and clearly.
2. Use Hinglish naturally (e.g., "Concept bohot simple hai," "Basically...").
3. If the user asks for a "Practice Sheet" or "Questions", generate 3-5 practice questions relevant to the topic.
4. If the user previously made a mistake (in conversation context), gently correct them.
5. Be encouraging.
Example: "Electron nucleus ke around ghumta hai, jaise planets sun ke around."`;
try {
const history = messages.slice(-10).map(m => `${m.role === 'user' ? 'Student' : 'Guru'}: ${m.text}`).join('\n');
const fullPrompt = `${history}\nStudent: ${userMsg}\nGuru:`;
const response = await callGemini(apiKey, modelId, fullPrompt, systemPrompt);
setMessages(prev => [...prev, { role: 'ai', text: response }]);
} catch (error) {
setMessages(prev => [...prev, { role: 'system', text: `❌ Error: ${error.message}` }]);
} finally {
setLoading(false);
}
};
return (
{messages.map((msg, idx) => (
{msg.role === 'ai' &&
GURU
}
{msg.text}
))}
{loading && (
Guru is thinking...
)}
);
};
// 3. TASK BOARD
const TaskBoard = ({ tasks, setTasks }) => {
const [newTask, setNewTask] = useState('');
const [filter, setFilter] = useState('all');
const addTask = () => {
if (!newTask.trim()) return;
const task = {
id: Date.now(),
text: newTask,
status: 'todo',
createdAt: new Date().toISOString()
};
setTasks(prev => [task, ...prev]);
setNewTask('');
};
const toggleTask = (id) => {
setTasks(prev => prev.map(t => t.id === id ? { ...t, status: t.status === 'todo' ? 'done' : 'todo' } : t));
};
const deleteTask = (id) => {
setTasks(prev => prev.filter(t => t.id !== id));
};
const filteredTasks = tasks.filter(t => filter === 'all' ? true : t.status === filter);
return (
Assignments
{['all', 'todo', 'done'].map(f => (
))}
{filteredTasks.length === 0 ? (
) : (
filteredTasks.map(task => (
{task.text}
))
)}
);
};
// 4. FLASHCARDS
const Flashcards = ({ apiKey, modelId, decks, setDecks }) => {
const [mode, setMode] = useState('list');
const [inputText, setInputText] = useState('');
const [generating, setGenerating] = useState(false);
const [activeDeck, setActiveDeck] = useState(null);
const [cardIndex, setCardIndex] = useState(0);
const [flipped, setFlipped] = useState(false);
const [error, setError] = useState(null);
const generateCards = async () => {
if (!inputText.trim()) return;
if (!apiKey) {
setError("Please add an API key in Settings first.");
return;
}
setGenerating(true);
setError(null);
try {
const prompt = `Create a list of 5 to 10 flashcards from this text.
OUTPUT JSON ONLY. Format: [{"front": "Question", "back": "Answer"}].
Text: "${inputText.substring(0, 4000)}"`;
let response = await callGemini(apiKey, modelId, prompt, "You are a JSON generator.");
const cleanJson = response.replace(/```json/g, '').replace(/```/g, '').trim();
const cards = JSON.parse(cleanJson);
if (!Array.isArray(cards)) throw new Error("AI format error.");
const newDeck = {
id: Date.now(),
title: `Study Set ${new Date().toLocaleDateString()}`,
cards: cards,
createdAt: new Date().toISOString()
};
setDecks(prev => [newDeck, ...prev]);
setMode('list');
setInputText('');
} catch (err) {
setError(err.message);
} finally {
setGenerating(false);
}
};
const deleteDeck = (id, e) => {
e.stopPropagation();
if(confirm("Delete this deck?")) setDecks(prev => prev.filter(d => d.id !== id));
};
if (mode === 'create') {
return (
New Deck
Paste your notes below. The AI will extract key points into flashcards.
);
}
if (mode === 'study' && activeDeck) {
const card = activeDeck.cards[cardIndex];
return (
{activeDeck.title}
{cardIndex + 1}/{activeDeck.cards.length}
setFlipped(!flipped)}
className="w-full max-w-xl aspect-[3/2] perspective-1000 cursor-pointer group"
>
Question
{card.front}
Tap to flip
);
}
return (
Your Decks
{decks.length === 0 ? (
) : (
decks.map(deck => (
{ setActiveDeck(deck); setMode('study'); }}
className="bg-slate-800/50 border border-slate-700 hover:border-purple-500 p-6 rounded-2xl cursor-pointer group transition-all relative hover:bg-slate-800"
>
{deck.title}
{deck.cards.length} cards
))
)}
);
};
// 5. SETTINGS
const SettingsComp = ({ apiKey, setApiKey, userName, setUserName, modelId, setModelId, deleteData, exportData, importData }) => {
const fileInputRef = useRef(null);
const [showAdvanced, setShowAdvanced] = useState(false);
return (
Settings
{/* Profile */}
{/* API Section */}
AI Setup
setApiKey(e.target.value)}
placeholder="AIzaSy..."
className="flex-1 bg-slate-900 border border-slate-700 rounded-xl p-3 text-white focus:border-cyan-500 focus:outline-none transition-colors"
/>
{showAdvanced && (
)}
{/* Data */}
{/* Danger */}
Danger Zone
);
};
// --- MAIN APP ---
const App = () => {
const [activeTab, setActiveTab] = useState('dashboard');
const [apiKey, setApiKey] = useLocalStorage('sn_apiKey', '');
const [modelId, setModelId] = useLocalStorage('sn_modelId', DEFAULT_MODEL); // New setting
const [userName, setUserName] = useLocalStorage('sn_username', '');
const [tasks, setTasks] = useLocalStorage('sn_tasks', []);
const [decks, setDecks] = useLocalStorage('sn_decks', []);
const [stats, setStats] = useLocalStorage('sn_stats', { minutesFocused: 0 });
const dashboardStats = {
pendingTasks: tasks.filter(t => t.status === 'todo').length,
decks: decks.length,
minutesFocused: stats.minutesFocused
};
const clearData = () => {
setTasks([]);
setDecks([]);
setStats({ minutesFocused: 0 });
alert("Data reset.");
};
const exportData = () => {
const data = { version: 1, date: new Date().toISOString(), userName, tasks, decks, stats };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `studynexus_backup.json`;
a.click();
URL.revokeObjectURL(url);
};
const importData = (file) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (data.tasks) setTasks(data.tasks);
if (data.decks) setDecks(data.decks);
if (data.stats) setStats(data.stats);
if (data.userName) setUserName(data.userName);
alert("Backup restored!");
} catch (err) {
alert("Invalid file.");
}
};
reader.readAsText(file);
};
const FocusTimer = ({ stats, setStats }) => {
const [timeLeft, setTimeLeft] = useState(25 * 60);
const [isActive, setIsActive] = useState(false);
const [mode, setMode] = useState('focus'); // focus, break
useEffect(() => {
let interval = null;
if (isActive && timeLeft > 0) {
interval = setInterval(() => {
setTimeLeft(prev => prev - 1);
if (mode === 'focus' && timeLeft % 60 === 0 && timeLeft !== 25*60) {
setStats(prev => ({ ...prev, minutesFocused: (prev.minutesFocused || 0) + 1 }));
}
}, 1000);
} else if (timeLeft === 0) {
setIsActive(false);
if (mode === 'focus') { setMode('break'); setTimeLeft(5 * 60); }
else { setMode('focus'); setTimeLeft(25 * 60); }
}
return () => clearInterval(interval);
}, [isActive, timeLeft, mode, setStats]);
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const totalTime = mode === 'focus' ? 25 * 60 : 5 * 60;
const progress = ((totalTime - timeLeft) / totalTime) * 100;
return (
{mode === 'focus' ? 'Deep Work' : 'Chill Break'}
);
};
const renderContent = () => {
switch(activeTab) {
case 'dashboard': return ;
case 'chat': return ;
case 'tasks': return ;
case 'flashcards': return ;
case 'focus': return ;
case 'settings': return ;
default: return ;
}
};
return (
{/* Abstract Background Shapes */}
{renderContent()}
{!apiKey && activeTab !== 'settings' && (
setActiveTab('settings')} className="bg-cyan-500 text-black px-4 py-3 rounded-xl font-bold shadow-lg shadow-cyan-500/20 cursor-pointer flex items-center gap-2 hover:bg-cyan-400">
Setup API Key
)}
);
};
export default App;