/* ======================================== App Component — padhle (Backend-Connected) ======================================== */ import { useState, useRef, useCallback, useEffect } from "react"; import Sidebar from "./components/sidebar/Sidebar"; import TopNav from "./components/top-nav/TopNav"; import ChatHistory from "./components/chat-history/ChatHistory"; import ChatInput from "./components/chat-input/ChatInput"; import SelectorFlow from "./components/selector-flow/SelectorFlow"; import { useAuth } from "./lib/auth/SupabaseAuth"; import SignInModal from "./lib/auth/SignIn"; import { resetAppState } from "./appState"; const API_BASE = ""; // Uses Vite proxy → localhost:3001 in dev // Default selectors const DEFAULTS = { grade: "Choose standard", subject: "choose-subject", chapter: "choose-chapter", }; /* ─── Selector data ─── */ const gradeItems = [ { id: "grade-6", label: "Grade 6", icon: "backpack" }, { id: "grade-7", label: "Grade 7", icon: "auto_stories" }, { id: "grade-8", label: "Grade 8", icon: "menu_book" }, { id: "grade-9", label: "Grade 9", icon: "science" }, { id: "grade-10", label: "Grade 10", icon: "calculate" }, { id: "grade-11", label: "Grade 11", icon: "psychology" }, { id: "grade-12", label: "Grade 12", icon: "school" }, ]; const subjectItems = [ { id: "math", label: "Mathematics" }, { id: "science", label: "Science" }, { id: "history", label: "History" }, { id: "language", label: "Language Arts" }, { id: "english", label: "English" }, { id: "geography", label: "Geography" }, ]; const chapterData = { math: [ { id: "ch-1", label: "Chapter 1: Rational Numbers" }, { id: "ch-2", label: "Chapter 2: Linear Equations" }, { id: "ch-3", label: "Chapter 3: Coordinate Geometry" }, { id: "ch-4", label: "Chapter 4: Algebra" }, { id: "ch-5", label: "Chapter 5: Geometry" }, { id: "ch-6", label: "Chapter 6: Trigonometry" }, ], science: [ { id: "ch-1", label: "Chapter 1: Nutrition in Plants" }, { id: "ch-2", label: "Chapter 2: Photosynthesis" }, { id: "ch-3", label: "Chapter 3: Human Physiology" }, { id: "ch-4", label: "Chapter 4: Chemistry Basics" }, { id: "ch-5", label: "Chapter 5: Motion & Force" }, { id: "ch-6", label: "Chapter 6: Electricity" }, ], history: [ { id: "ch-1", label: "Chapter 1: Early Civilizations" }, { id: "ch-2", label: "Chapter 2: Ancient India" }, { id: "ch-3", label: "Chapter 3: Medieval Period" }, { id: "ch-4", label: "Chapter 4: Modern India" }, { id: "ch-5", label: "Chapter 5: World Wars" }, { id: "ch-6", label: "Chapter 6: Independence Movement" }, ], language: [ { id: "ch-1", label: "Chapter 1: Grammar Basics" }, { id: "ch-2", label: "Chapter 2: Comprehension" }, { id: "ch-3", label: "Chapter 3: Creative Writing" }, { id: "ch-4", label: "Chapter 4: Literature" }, { id: "ch-5", label: "Chapter 5: Vocabulary" }, { id: "ch-6", label: "Chapter 6: Composition" }, ], english: [ { id: "ch-1", label: "Chapter 1: Reading Skills" }, { id: "ch-2", label: "Chapter 2: Writing Skills" }, { id: "ch-3", label: "Chapter 3: Grammar" }, { id: "ch-4", label: "Chapter 4: Poetry" }, { id: "ch-5", label: "Chapter 5: Prose" }, { id: "ch-6", label: "Chapter 6: Drama" }, ], geography: [ { id: "ch-1", label: "Chapter 1: Earth & Universe" }, { id: "ch-2", label: "Chapter 2: Landforms" }, { id: "ch-3", label: "Chapter 3: Climate & Weather" }, { id: "ch-4", label: "Chapter 4: Natural Resources" }, { id: "ch-5", label: "Chapter 5: Maps & Atlas" }, { id: "ch-6", label: "Chapter 6: Human Geography" }, ], }; function App() { const { isAuthenticated, user, signIn, signUp, signOut, getToken } = useAuth(); const [messages, setMessages] = useState([]); const [activeSubject, setActiveSubject] = useState(DEFAULTS.subject); /* ─── Subject label resolution (ID → display name) ─── */ const getSubjectLabel = (id) => { if (!id || id === "choose-subject") return "Choose Subject"; return subjectItems.find((s) => s.id === id)?.label || id; }; /* ─── Chapter list resolution (subject ID → array) ─── */ const getChaptersForSubject = (subjectId) => { return chapterData[subjectId] || []; }; /* ─── Chapter label resolution (ID → display name) ─── */ const getChapterLabel = (chapterId) => { if (!chapterId || chapterId === "choose-chapter") return "Choose Chapter"; return chapterId; }; const [activeChat, setActiveChat] = useState(null); const [activeGrade, setActiveGrade] = useState(DEFAULTS.grade); const [activeChapter, setActiveChapter] = useState(DEFAULTS.chapter); const [isTyping, setIsTyping] = useState(false); const [sessions, setSessions] = useState([]); const [isLoadingSessions, setIsLoadingSessions] = useState(false); const [selectorStep, setSelectorStep] = useState("grade"); // grade → subject → chapter → chat const abortRef = useRef(null); const hasMountedRef = useRef(false); // track first mount to avoid resetting on initial load // Reset all user-scoped state whenever authentication changes. useEffect(() => { if (!hasMountedRef.current) { hasMountedRef.current = true; if (isAuthenticated) loadSessions(); return; } if (abortRef.current) { abortRef.current.abort(); abortRef.current = null; } const next = resetAppState(); setMessages(next.messages); setActiveChat(next.activeChat); setActiveGrade(next.activeGrade); setActiveSubject(next.activeSubject); setActiveChapter(next.activeChapter); setIsTyping(next.isTyping); setSelectorStep(next.selectorStep); setSessions(next.sessions); if (isAuthenticated) loadSessions(); }, [isAuthenticated]); // ─── Auth handlers ─── const handleAuthError = (msg) => { setMessages((prev) => [ ...prev, { id: Date.now(), role: "assistant", text: `⚠️ ${msg}` }, ]); }; const loadSessions = async () => { setIsLoadingSessions(true); try { const token = await getToken(); const headers = {}; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(`${API_BASE}/api/sessions`, { headers, credentials: "include" }); if (res.ok) { const data = await res.json(); setSessions(data); } } catch (err) { console.error("Failed to load sessions:", err); } finally { setIsLoadingSessions(false); } }; const handleNewChat = () => { // Abort any in-flight request if (abortRef.current) { abortRef.current.abort(); abortRef.current = null; } // Clear everything setMessages([]); setActiveChat(null); setActiveGrade(DEFAULTS.grade); setActiveSubject(DEFAULTS.subject); setActiveChapter(DEFAULTS.chapter); setIsTyping(false); // Reset selector to grade step setSelectorStep("grade"); // Refresh sessions list from backend loadSessions(); }; const handleChatSelect = async (sessionId) => { // Abort any in-flight request if (abortRef.current) { abortRef.current.abort(); abortRef.current = null; } setIsTyping(true); setMessages([]); try { const token = await getToken(); const headers = {}; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(`${API_BASE}/api/chat/${sessionId}`, { headers, credentials: "include" }); if (!res.ok) { throw new Error("Session not found"); } const data = await res.json(); // Set the session's context (grade, subject, chapter) if (data.session) { setActiveGrade(data.session.grade || DEFAULTS.grade); setActiveSubject(data.session.subject || DEFAULTS.subject); setActiveChapter(data.session.chapter || DEFAULTS.chapter); } // Move to chat view setSelectorStep("chat"); // Load the messages if (data.messages) { setMessages(data.messages.map((m) => ({ ...m, streaming: false }))); } setActiveChat(sessionId); } catch (err) { console.error("Failed to load session:", err); setMessages([ { id: Date.now(), role: "assistant", text: `⚠️ Could not load that chat. It may have been deleted.`, }, ]); } finally { setIsTyping(false); } }; const handleDeleteChat = async (sessionId) => { try { const token = await getToken(); const headers = {}; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(`${API_BASE}/api/sessions/${sessionId}`, { method: "DELETE", headers, credentials: "include", }); if (res.ok) { // If we were on the deleted chat, clear it if (activeChat === sessionId) { handleNewChat(); } else { loadSessions(); } } } catch (err) { console.error("Failed to delete session:", err); } }; const handleSend = useCallback( async (text) => { if (!text.trim() || isTyping) return; // Abort any previous in-flight request if (abortRef.current) { abortRef.current.abort(); } // Add user message immediately const userMsg = { id: Date.now(), role: "user", text }; setMessages((prev) => [...prev, userMsg]); setIsTyping(true); const fetchChatId = activeChat; // Build request body const body = { text: text.trim(), chatId: fetchChatId, grade: activeGrade !== DEFAULTS.grade ? activeGrade : undefined, subject: activeSubject !== DEFAULTS.subject ? activeSubject : undefined, chapter: activeChapter !== DEFAULTS.chapter ? activeChapter : undefined, }; // Open SSE connection const controller = new AbortController(); abortRef.current = controller; const token = await getToken(); const headers = { "Content-Type": "application/json" }; if (token) headers["Authorization"] = `Bearer ${token}`; fetch(`${API_BASE}/api/chat`, { method: "POST", headers, credentials: "include", body: JSON.stringify(body), signal: controller.signal, }) .then(async (response) => { if (!response.ok) { throw new Error(`Server error: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let assistantText = ""; // Create placeholder assistant message const assistantId = Date.now() + 1; setMessages((prev) => [ ...prev, { id: assistantId, role: "assistant", text: "", streaming: true }, ]); while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const jsonStr = line.slice(6); if (jsonStr === "[DONE]") continue; try { const data = JSON.parse(jsonStr); if (data.type === "chunk") { assistantText += data.content; setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, text: assistantText } : m ) ); } else if (data.type === "done") { // Mark as non-streaming setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, streaming: false } : m ) ); } else if (data.type === "error") { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, text: `⚠️ Error: ${data.message}`, streaming: false, } : m ) ); } } catch { // Skip malformed SSE frames } } } setIsTyping(false); abortRef.current = null; // Refresh sessions to update timestamps loadSessions(); }) .catch((err) => { if (err.name === "AbortError") return; // intentional abort setIsTyping(false); setMessages((prev) => [ ...prev, { id: Date.now() + 1, role: "assistant", text: `⚠️ Connection error: ${err.message}. Make sure the backend is running on port 3001.`, }, ]); abortRef.current = null; }); }, [isTyping, activeChat, activeGrade, activeSubject, activeChapter] ); const handlePromptClick = useCallback( (promptText) => { handleSend(promptText); }, [handleSend] ); /* ─── Selector step handlers ─── */ const handleGradeSelect = (item) => { setActiveGrade(item.label); setSelectorStep("subject"); }; const handleSubjectSelect = (item) => { setActiveSubject(item.id); setActiveChapter(DEFAULTS.chapter); setSelectorStep("chapter"); }; const handleChapterSelect = (item) => { setActiveChapter(item.label); setSelectorStep("chat"); }; /* ─── Selector step data ─── */ const selectorData = { grade: { title: "Select your standard", subtitle: "Choose the grade you are studying in", items: gradeItems, selected: activeGrade, onSelect: handleGradeSelect, }, subject: { title: "Select your subject", subtitle: `Chose by grade – ${activeGrade}`, items: subjectItems, selectedValue: activeSubject, onSelect: handleSubjectSelect, compareField: "id", }, chapter: { title: "Select a chapter", subtitle: `Subject – ${getSubjectLabel(activeSubject)}`, items: getChaptersForSubject(activeSubject), selectedValue: activeChapter, onSelect: handleChapterSelect, compareField: "label", }, }; // ─── Not authenticated → show sign-in modal ─── if (!isAuthenticated) { return ( { try { await signIn(email, password); } catch (err) { handleAuthError(err.message || "Sign in failed"); } }} onSignUp={async (email, password) => { try { await signUp(email, password); } catch (err) { handleAuthError(err.message || "Sign up failed"); } }} onError={handleAuthError} /> ); } return (
{ setActiveSubject(id); setActiveChapter(DEFAULTS.chapter); if (selectorStep !== "chat") { setSelectorStep("subject"); } }} activeChat={activeChat} onChatSelect={handleChatSelect} onNewChat={handleNewChat} sessions={sessions} onDeleteChat={handleDeleteChat} isLoadingSessions={isLoadingSessions} showUpgrade={false} user={user} onSignOut={signOut} />
setActiveGrade(g)} subjectItems={subjectItems} chapterData={chapterData} activeSubject={activeSubject} activeChapter={activeChapter} onChapterChange={(ch) => { setActiveChapter(ch); if (selectorStep !== "chat") { setSelectorStep("chapter"); } }} onSubjectChange={(id) => { setActiveSubject(id); setActiveChapter(DEFAULTS.chapter); if (selectorStep !== "chat") { setSelectorStep("subject"); } }} user={user} onSignOut={signOut} />
); } export default App;