SocraticAI / frontend /src /App.jsx
Deployer
Initial deployment commit with Git LFS tracking
a10a6c0
Raw
History Blame Contribute Delete
10.5 kB
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import { Send, Bot, User, ChevronLeft, LayoutGrid, GraduationCap as CapIcon, BookOpen, Upload, Settings, RefreshCw } from 'lucide-react';
const API_BASE = window.location.port === "5173" ? "http://localhost:8000/api" : "/api";
function App() {
const [page, setPage] = useState('welcome');
const [curriculum, setCurriculum] = useState({});
const [selection, setSelection] = useState({ grade: '', subject: '', topic: '' });
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const [loading, setLoading] = useState(false);
const [hintLevel, setHintLevel] = useState(1);
const [status, setStatus] = useState('ACTIVE');
const [ingestStatus, setIngestStatus] = useState('');
const [uploadData, setUploadData] = useState({ grade: '', subject: '', file: null });
const messagesEndRef = useRef(null);
useEffect(() => {
fetchCurriculum();
}, []);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const fetchCurriculum = async () => {
try {
const res = await fetch(`${API_BASE}/curriculum`);
const data = await res.json();
setCurriculum(data);
} catch (err) {
console.error("Failed to fetch curriculum:", err);
}
};
const startSession = (grade, subject, topic) => {
setSelection({ grade, subject, topic });
setMessages([{
role: 'assistant',
content: `Hello! I'm your AI Learner for **${grade} ${subject}**. What would you like to explore today?`
}]);
setPage('chat');
};
const sendMessage = async (e) => {
e.preventDefault();
if (!inputText.trim() || loading) return;
const userMsg = { role: 'user', content: inputText };
const newMessages = [...messages, userMsg];
setMessages(newMessages);
setInputText('');
setLoading(true);
try {
const res = await fetch(`${API_BASE}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: newMessages,
grade: selection.grade,
subject: selection.subject,
topic: selection.topic,
hint_level: hintLevel,
status: status
})
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.detail || `Server error: ${res.status}`);
}
const data = await res.json();
setMessages([...newMessages, data.message]);
setHintLevel(data.hint_level);
setStatus(data.status);
} catch (err) {
console.error("Chat error:", err);
setMessages([...newMessages, { role: 'assistant', content: `⚠️ **Error:** ${err.message}. Please check if the backend server is running and try again.` }]);
} finally {
setLoading(false);
}
};
const handleUpload = async (e) => {
e.preventDefault();
if (!uploadData.file || !uploadData.grade || !uploadData.subject) return;
setIngestStatus('Uploading...');
const formData = new FormData();
formData.append('file', uploadData.file);
try {
await fetch(`${API_BASE}/upload?grade=${uploadData.grade}&subject=${uploadData.subject}`, {
method: 'POST',
body: formData
});
setIngestStatus('Processing...');
const res = await fetch(`${API_BASE}/ingest`, { method: 'POST' });
const result = await res.json();
setIngestStatus(result.message);
fetchCurriculum();
} catch (err) {
setIngestStatus('Error: ' + err.message);
}
};
// --- RENDERERS ---
if (page === 'ingest') {
return (
<div className="selection-screen">
<button onClick={() => setPage('welcome')} style={{position: 'absolute', top: '2rem', left: '2rem', background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.5rem'}}>
<ChevronLeft size={20} /> Back
</button>
<div className="logo"><Bot size={32} /> AI Learner Admin</div>
<h2>Ingest New Knowledge</h2>
<form onSubmit={handleUpload} style={{width: '100%', maxWidth: '500px', display: 'flex', flexDirection: 'column', gap: '1.5rem', marginTop: '2rem'}}>
<div className="input-group" style={{display: 'flex', flexDirection: 'column', gap: '0.5rem'}}>
<label style={{fontSize: '0.8rem', color: '#94a3b8'}}>Grade</label>
<input type="text" placeholder="e.g. Grade 10" onChange={e => setUploadData({...uploadData, grade: e.target.value})} />
</div>
<div className="input-group" style={{display: 'flex', flexDirection: 'column', gap: '0.5rem'}}>
<label style={{fontSize: '0.8rem', color: '#94a3b8'}}>Subject</label>
<input type="text" placeholder="e.g. Science" onChange={e => setUploadData({...uploadData, subject: e.target.value})} />
</div>
<div className="input-group" style={{display: 'flex', flexDirection: 'column', gap: '0.5rem'}}>
<label style={{fontSize: '0.8rem', color: '#94a3b8'}}>PDF File</label>
<input type="file" accept=".pdf" onChange={e => setUploadData({...uploadData, file: e.target.files[0]})} />
</div>
<button type="submit" className="card" style={{width: '100%', background: 'var(--accent-primary)', color: 'white', border: 'none'}}>
<Upload size={20} style={{marginBottom: '0.5rem'}} /> Upload & Process
</button>
{ingestStatus && <div style={{textAlign: 'center', padding: '1rem', background: 'rgba(255,255,255,0.05)', borderRadius: '0.5rem'}}>{ingestStatus}</div>}
</form>
</div>
);
}
if (page === 'welcome') {
return (
<div className="selection-screen">
<button onClick={() => setPage('ingest')} style={{position: 'absolute', top: '2rem', right: '2rem', background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.5rem'}}>
<Settings size={20} /> Admin
</button>
<div className="logo"><Bot size={32} /> AI Learner</div>
<h2>Select your grade to begin</h2>
<div className="grid">
{Object.keys(curriculum).map(grade => (
<div key={grade} className="card" onClick={() => {
setSelection({ ...selection, grade });
setPage('subject');
}}>
<CapIcon size={40} color="#3b82f6" />
<h3 style={{marginTop: '1rem'}}>{grade}</h3>
</div>
))}
</div>
</div>
);
}
if (page === 'subject') {
return (
<div className="selection-screen">
<button className="back-btn" onClick={() => setPage('welcome')} style={{position: 'absolute', top: '2rem', left: '2rem', background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.5rem'}}>
<ChevronLeft size={20} /> Back
</button>
<div className="logo"><Bot size={32} /> AI Learner</div>
<h2>Select Subject ({selection.grade})</h2>
<div className="grid">
{Object.keys(curriculum[selection.grade] || {}).map(subject => (
<div key={subject} className="card" onClick={() => {
startSession(selection.grade, subject, 'General');
}}>
<BookOpen size={40} color="#8b5cf6" />
<h3 style={{marginTop: '1rem'}}>{subject}</h3>
</div>
))}
</div>
</div>
);
}
return (
<div className="app-container">
<div className="sidebar">
<div className="logo"><Bot size={24} /> AI Learner</div>
<div style={{marginTop: 'auto', display: 'flex', flexDirection: 'column', gap: '1rem'}}>
<div style={{padding: '1rem', background: 'rgba(255,255,255,0.05)', borderRadius: '0.5rem', fontSize: '0.9rem'}}>
<div style={{color: '#94a3b8', fontSize: '0.75rem', textTransform: 'uppercase', marginBottom: '0.5rem'}}>Active Session</div>
<div style={{fontWeight: 'bold'}}>{selection.topic}</div>
<div style={{color: '#64748b'}}>{selection.grade} • {selection.subject}</div>
</div>
<button
onClick={() => setPage('welcome')}
style={{padding: '0.75rem', background: 'transparent', border: '1px solid var(--border-color)', color: 'white', borderRadius: '0.5rem', cursor: 'pointer', transition: 'background 0.2s'}}
onMouseOver={(e) => e.target.style.background = 'rgba(255,255,255,0.05)'}
onMouseOut={(e) => e.target.style.background = 'transparent'}
>
Switch Topic
</button>
</div>
</div>
<div className="chat-main">
<div className="chat-header">
<div>
<h2 style={{fontSize: '1.25rem'}}>{selection.topic}</h2>
<div style={{fontSize: '0.75rem', color: '#64748b'}}>Powered by Gemini 3.1 Flash-Lite</div>
</div>
<div style={{display: 'flex', gap: '0.5rem'}}>
<span className="status-badge hint-level">Hint Level: {hintLevel}</span>
</div>
</div>
<div className="messages-container">
{messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
<div className="prose">
<ReactMarkdown>{msg.content}</ReactMarkdown>
</div>
</div>
))}
{loading && (
<div className="message assistant" style={{fontStyle: 'italic', color: '#64748b'}}>
AI Learner is thinking...
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="input-area">
<form className="input-wrapper" onSubmit={sendMessage}>
<input
type="text"
placeholder={`Ask about ${selection.topic}...`}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
disabled={loading}
/>
<button type="submit" className="send-btn" disabled={loading || !inputText.trim()}>
<Send size={20} />
</button>
</form>
</div>
</div>
</div>
);
}
export default App;