diff --git a/app/page.tsx b/app/page.tsx index ed2f3fc..1f6746e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,23 +1,24 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import Navigation from '@/components/navigation' import Dashboard from '@/components/pages/dashboard' import GoalCreation from '@/components/pages/goal-creation' import StudyPlan from '@/components/pages/study-plan' import LearningScreen from '@/components/pages/learning-screen' +import QuizPage from '@/components/pages/quiz-page' import AIChat from '@/components/pages/ai-chat' import ResourceLibrary from '@/components/pages/resource-library' import Analytics from '@/components/pages/analytics' import Settings from '@/components/pages/settings' +import Auth from '@/components/pages/auth' + +type Goal = { id: number; title: string; progress: number; daysLeft: number } export default function Home() { const [currentPage, setCurrentPage] = useState('dashboard') - const [goals, setGoals] = useState([ - { id: 1, title: "Master Calculus", progress: 65, daysLeft: 12 }, - { id: 2, title: "Biology Fundamentals", progress: 42, daysLeft: 18 }, - { id: 3, title: "Physics Concepts", progress: 78, daysLeft: 8 }, - ]) + const [goals, setGoals] = useState([]) + const [dashboardRefreshKey, setDashboardRefreshKey] = useState(0) const [learningScreenState, setLearningScreenState] = useState({ isCompleted: false, @@ -25,27 +26,115 @@ export default function Home() { messages: [ { id: 1, role: "tutor", text: "Hi! I'm your AI tutor. What would you like to learn about derivatives?" }, ], - inputMessage: "" + inputMessage: "", + chatLoading: false, + selectedGoalTitle: null as string | null, + selectedModule: null as any, }); - const handleDeleteGoal = (id: number) => { - setGoals(goals.filter(goal => goal.id !== id)); + const [auth, setAuth] = useState<{ token: string | null; name?: string; email?: string; role?: string }>({ token: null }) + const [selectedGoal, setSelectedGoal] = useState(null) + + useEffect(() => { + const t = typeof window !== 'undefined' ? localStorage.getItem('token') : null + if (!t) return + const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080' + fetch(`${base}/api/auth/me`, { headers: { Authorization: `Bearer ${t}` } }) + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data) setAuth({ token: t, name: data.name, email: data.email, role: data.role }) + else setAuth({ token: null }) + }) + .catch(() => setAuth({ token: null })) + }, []) + + useEffect(() => { + const t = auth.token + if (!t) return + const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080' + fetch(`${base}/api/goals`, { headers: { Authorization: `Bearer ${t}` } }) + .then(res => res.ok ? res.json() : []) + .then((list) => setGoals(Array.isArray(list) ? list : [])) + .catch(() => setGoals([])) + }, [auth.token]) + + const refreshGoals = async () => { + const t = typeof window !== 'undefined' ? localStorage.getItem('token') : null + if (!t) return + const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080' + try { + const res = await fetch(`${base}/api/goals`, { headers: { Authorization: `Bearer ${t}` } }) + const list = await res.json().catch(() => []) + setGoals(Array.isArray(list) ? list : []) + setDashboardRefreshKey((k) => k + 1) + } catch { + // ignore + } + } + + const handleDeleteGoal = async (id: number) => { + const t = typeof window !== 'undefined' ? localStorage.getItem('token') : null + const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080' + try { + if (!t) { + console.warn('Not authenticated; cannot delete goal') + return + } + const res = await fetch(`${base}/api/goals/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${t}` } }) + const data = await res.json().catch(() => ({})) + if (!res.ok) { + // Fallback path for environments blocking DELETE (403/405): try POST /delete + if (res.status === 403 || res.status === 405) { + const res2 = await fetch(`${base}/api/goals/delete/${id}`, { method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' } }) + const data2 = await res2.json().catch(() => ({})) + if (!res2.ok) { + throw new Error(data2?.error || 'Failed to delete goal') + } + } else { + throw new Error(data?.error || 'Failed to delete goal') + } + } + setGoals(prev => prev.filter(goal => goal.id !== id)) + if (selectedGoal && selectedGoal.id === id) setSelectedGoal(null) + } catch (e) { + console.error(e) + } }; const renderPage = () => { switch (currentPage) { case 'dashboard': - return + return ( + { setSelectedGoal(goal); setCurrentPage('study-plan'); }} + refreshKey={dashboardRefreshKey} + /> + ) case 'goals': return case 'study-plan': - return + return { setSelectedGoal(g); setCurrentPage('study-plan'); }} onStartLearning={(goalTitle, module) => { setLearningScreenState(prev => ({ ...prev, selectedGoalTitle: goalTitle, selectedModule: module })); setCurrentPage(module?.type === 'quiz' ? 'quiz' : 'learning'); }} /> case 'learning': return + case 'quiz': + return learningScreenState.selectedGoalTitle && learningScreenState.selectedModule ? ( + + ) : ( + { setSelectedGoal(g); setCurrentPage('study-plan'); }} onStartLearning={(goalTitle, module) => { setLearningScreenState(prev => ({ ...prev, selectedGoalTitle: goalTitle, selectedModule: module })); setCurrentPage(module?.type === 'quiz' ? 'quiz' : 'learning'); }} /> + ) case 'chat': return case 'resources': @@ -55,14 +144,18 @@ export default function Home() { case 'settings': return default: - return + return } } return ( -
- -
{renderPage()}
-
+ auth.token ? ( +
+ { if (page === 'study-plan') setSelectedGoal(null); setCurrentPage(page); }} /> +
{renderPage()}
+
+ ) : ( + setAuth(u)} /> + ) ) } diff --git a/components/pages/ai-chat.tsx b/components/pages/ai-chat.tsx index e098631..a3cb2ad 100644 --- a/components/pages/ai-chat.tsx +++ b/components/pages/ai-chat.tsx @@ -35,17 +35,23 @@ export default function AIChat() { const endpoint = process.env.NEXT_PUBLIC_BACKEND_URL ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/ai-chat` : "http://localhost:8080/api/ai-chat"; + const payloadMessages = [...messages, userMsg].map((m: any) => ({ role: m.role, text: m.text })) + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null const res = await fetch(endpoint, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ messages: [...messages, userMsg] }), + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ messages: payloadMessages }), }) if (!res.ok) throw new Error("Failed to get AI response") const data = await res.json() const reply = (data?.reply ?? "I couldn't generate a response.").toString() + const clean = reply.replace(/\$/g, "").replace(/\\[()\\[\\]]/g, "") setMessages((prev) => [ ...prev, - { id: prev.length + 1, role: "tutor", text: reply }, + { id: prev.length + 1, role: "tutor", text: clean }, ]) } catch (err) { setMessages((prev) => [ diff --git a/components/pages/analytics.tsx b/components/pages/analytics.tsx index daf4d5b..ff8b30c 100644 --- a/components/pages/analytics.tsx +++ b/components/pages/analytics.tsx @@ -15,31 +15,46 @@ import { Pie, Cell, } from "recharts" +import { useEffect, useState } from "react" export default function Analytics() { - const studyTimeData = [ - { day: "Mon", hours: 2.5 }, - { day: "Tue", hours: 3.2 }, - { day: "Wed", hours: 2.8 }, - { day: "Thu", hours: 3.5 }, - { day: "Fri", hours: 2.1 }, - { day: "Sat", hours: 4.0 }, - { day: "Sun", hours: 1.5 }, - ] + const [loading, setLoading] = useState(false) + const [studyTimeData, setStudyTimeData] = useState>([]) + const [progressData, setProgressData] = useState>([]) + const [contentTypeData, setContentTypeData] = useState>([]) + const [totalMinutesWeek, setTotalMinutesWeek] = useState(0) + const [modulesCompleted, setModulesCompleted] = useState(0) + const [modulesTotal, setModulesTotal] = useState(0) + const [averageQuizScore, setAverageQuizScore] = useState(0) + const [currentStreakDays, setCurrentStreakDays] = useState(0) - const progressData = [ - { week: "Week 1", progress: 25 }, - { week: "Week 2", progress: 42 }, - { week: "Week 3", progress: 58 }, - { week: "Week 4", progress: 72 }, - ] - - const contentTypeData = [ - { name: "Videos", value: 45 }, - { name: "Articles", value: 30 }, - { name: "Quizzes", value: 15 }, - { name: "Notes", value: 10 }, - ] + useEffect(() => { + let cancelled = false + const load = async () => { + setLoading(true) + try { + const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080' + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null + const res = await fetch(`${base}/api/analytics/summary`, { headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) } }) + const data = await res.json().catch(() => ({})) + if (!cancelled && res.ok) { + setTotalMinutesWeek(Number(data?.totalStudyMinutesThisWeek || 0)) + setModulesCompleted(Number(data?.modulesCompleted || 0)) + setModulesTotal(Number(data?.modulesTotal || 0)) + setAverageQuizScore(Number(data?.averageQuizScore || 0)) + setCurrentStreakDays(Number(data?.currentStreakDays || 0)) + setStudyTimeData(Array.isArray(data?.studyTime) ? data.studyTime : []) + setProgressData(Array.isArray(data?.progress) ? data.progress : []) + setContentTypeData(Array.isArray(data?.contentType) ? data.contentType : []) + } + } catch { + } finally { + if (!cancelled) setLoading(false) + } + } + load() + return () => { cancelled = true } + }, []) const COLORS = ["#4f46e5", "#06b6d4", "#f97316", "#8b5cf6"] @@ -54,22 +69,22 @@ export default function Analytics() {

Total Study Time

-

19.6 hrs

+

{Math.round((totalMinutesWeek/60) * 10) / 10} hrs

This week

Modules Completed

-

24

-

Out of 35

+

{modulesCompleted}

+

Out of {modulesTotal}

Average Score

-

87%

+

{averageQuizScore}%

On quizzes

Current Streak

-

7 days

+

{currentStreakDays} days

Keep it up!

diff --git a/components/pages/auth.tsx b/components/pages/auth.tsx new file mode 100644 index 0000000..7f0a791 --- /dev/null +++ b/components/pages/auth.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +interface AuthProps { + onAuthenticated: (auth: { token: string; name: string; email: string; role: string }) => void; +} + +export default function Auth({ onAuthenticated }: AuthProps) { + const [mode, setMode] = useState<"login" | "register">("login"); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + setError(null); + }, [mode]); + + const submit = async () => { + if (!email || !password || (mode === "register" && !name)) return; + setLoading(true); + setError(null); + try { + const endpointBase = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; + const url = mode === "login" ? `${endpointBase}/api/auth/login` : `${endpointBase}/api/auth/register`; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify( + mode === "login" ? { email, password } : { name, email, password } + ), + }); + const data = await res.json(); + if (!res.ok) { + setError(data?.error || "Authentication failed"); + return; + } + const token = data?.token as string; + if (token) { + localStorage.setItem("token", token); + onAuthenticated({ token, name: data?.name, email: data?.email, role: data?.role }); + } else { + setError("No token returned by server"); + } + } catch (e: any) { + setError(e?.message || "Request failed"); + } finally { + setLoading(false); + } + }; + + return ( +
+ +

+ {mode === "login" ? "Login" : "Create an account"} +

+ {mode === "register" && ( +
+ + setName(e.target.value)} + placeholder="Your name" + /> +
+ )} +
+ + setEmail(e.target.value)} + placeholder="you@example.com" + /> +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + /> +
+ {error ? ( +

{error}

+ ) : null} + + +
+
+ ); +} diff --git a/components/pages/dashboard.tsx b/components/pages/dashboard.tsx index fb5a03a..b12ca92 100644 --- a/components/pages/dashboard.tsx +++ b/components/pages/dashboard.tsx @@ -3,7 +3,7 @@ import { Card } from "@/components/ui/card" import { Progress } from "@/components/ui/progress" import { CheckCircle2, Clock, Zap, TrendingUp, X } from "lucide-react" -import { useState, useMemo } from "react" +import { useState, useEffect, useMemo } from "react" interface Goal { id: number; @@ -15,43 +15,80 @@ interface Goal { export default function Dashboard({ onNavigate, goals, - onDeleteGoal + onDeleteGoal, + onSelectGoal, + refreshKey, }: { onNavigate: (page: string) => void; goals: Goal[]; onDeleteGoal: (id: number) => void; + onSelectGoal?: (goal: Goal) => void; + refreshKey?: number; }) { - const [dailyTasks, setDailyTasks] = useState([ - { id: 1, title: "Complete Calculus Chapter 3", duration: "45 min", completed: true }, - { id: 2, title: "Review Biology Notes", duration: "30 min", completed: true }, - { id: 3, title: "Practice Physics Problems", duration: "60 min", completed: false }, - { id: 4, title: "Read History Article", duration: "25 min", completed: false }, - ]) - - const sortedTasks = useMemo(() => { - return [...dailyTasks].sort((a, b) => - a.completed === b.completed ? 0 : a.completed ? 1 : -1 - ) - }, [dailyTasks]) - - const completedToday = dailyTasks.filter((t) => t.completed).length - const totalTime = dailyTasks.reduce((acc, t) => { - const minutes = Number.parseInt(t.duration) - return acc + minutes - }, 0) + const [loadingSummary, setLoadingSummary] = useState(false) + const [tasksCompletedToday, setTasksCompletedToday] = useState(0) + const [studyMinutesToday, setStudyMinutesToday] = useState(0) + const [streakDays, setStreakDays] = useState(0) + const [todaysTasks, setTodaysTasks] = useState>([]) + + useEffect(() => { + let cancelled = false + const run = async () => { + setLoadingSummary(true) + try { + const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080' + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null + const res = await fetch(`${base}/api/dashboard/summary`, { + headers: { + ...(token ? { Authorization: `Bearer ${token}` } : {}), + } + }) + const data = await res.json().catch(() => ({})) + if (!cancelled && res.ok) { + setTasksCompletedToday(Number(data?.tasksCompletedToday || 0)) + setStudyMinutesToday(Number(data?.studyMinutesToday || 0)) + setStreakDays(Number(data?.streakDays || 0)) + setTodaysTasks(Array.isArray(data?.todaysTasks) ? data.todaysTasks : []) + } + } catch { + if (!cancelled) { + setTasksCompletedToday(0) + setStudyMinutesToday(0) + setStreakDays(0) + setTodaysTasks([]) + } + } finally { + if (!cancelled) setLoadingSummary(false) + } + } + run() + return () => { cancelled = true } + }, [refreshKey]) + + const completedToday = tasksCompletedToday + const totalTime = studyMinutesToday const overallProgress = Math.round( goals.reduce((acc, goal) => acc + goal.progress, 0) / goals.length ) - const handleTaskCompletion = (id: number) => { - setDailyTasks( - dailyTasks.map((task) => - task.id === id ? { ...task, completed: !task.completed } : task - ) - ) + const openTaskGoal = (goalTitle: string) => { + const g = goals.find(x => x.title === goalTitle) + if (g) { + onSelectGoal ? onSelectGoal(g) : onNavigate('study-plan') + } } + const visibleTasks = useMemo(() => { + return Array.isArray(todaysTasks) + ? todaysTasks.filter(t => goals.some(g => g.title === t.goalTitle)) + : [] + }, [todaysTasks, goals]) + + const visibleTasksSorted = useMemo(() => { + return [...visibleTasks].sort((a, b) => (a.completed === b.completed ? 0 : a.completed ? 1 : -1)) + }, [visibleTasks]) + return (
{/* Header */} @@ -66,9 +103,7 @@ export default function Dashboard({

Tasks Completed

-

- {completedToday}/{dailyTasks.length} -

+

{completedToday}

@@ -88,7 +123,7 @@ export default function Dashboard({

Streak

-

7 days

+

{streakDays} days

@@ -110,33 +145,36 @@ export default function Dashboard({

Today's Tasks

-
- {sortedTasks.map((task) => ( -
onNavigate("learning")} - > - handleTaskCompletion(task.id)} - onClick={(e) => e.stopPropagation()} - className="w-5 h-5 rounded border-2 border-primary cursor-pointer" - /> -
-

- {task.title} -

+ {loadingSummary ? ( +

Loading today's tasks...

+ ) : visibleTasks.length === 0 ? ( +

No tasks scheduled for today.

+ ) : ( +
+ {visibleTasksSorted.map((t, idx) => ( +
openTaskGoal(t.goalTitle)} + > + e.stopPropagation()} + className="w-5 h-5 rounded border-2 border-primary" + /> +
+

+ {t.moduleTitle} ({t.goalTitle}) +

+

{t.type}

+
+ {t.duration}
- {task.duration} -
- ))} -
+ ))} +
+ )}
@@ -148,7 +186,7 @@ export default function Dashboard({ {goals.map((goal) => (
-

onNavigate("learning")}> +

onSelectGoal ? onSelectGoal(goal) : onNavigate("study-plan")}> {goal.title}

diff --git a/components/pages/goal-creation.tsx b/components/pages/goal-creation.tsx index 44d04c9..e491a83 100644 --- a/components/pages/goal-creation.tsx +++ b/components/pages/goal-creation.tsx @@ -46,15 +46,33 @@ export default function GoalCreation({ setGoals, onNavigate }: GoalCreationProps } } - const handleCreateGoal = () => { - const newGoal: Goal = { - id: Date.now(), - title: goalTitle, - progress: 0, - daysLeft: deadline ? Math.ceil((new Date(deadline).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : 0, - }; - setGoals((prevGoals) => [...prevGoals, newGoal]); - onNavigate("dashboard"); + const handleCreateGoal = async () => { + const base = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + try { + const res = await fetch(`${base}/api/goals`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + title: goalTitle, + description, + targetDate: deadline || null, + topics, + }) + }) + const data = await res.json(); + if (!res.ok) { + alert(data?.error || 'Failed to create goal'); + return; + } + setGoals((prev) => [...prev, data]); + onNavigate("dashboard"); + } catch (e) { + alert('Failed to create goal'); + } } return ( diff --git a/components/pages/learning-screen.tsx b/components/pages/learning-screen.tsx index 7c88cbf..299cc10 100644 --- a/components/pages/learning-screen.tsx +++ b/components/pages/learning-screen.tsx @@ -4,37 +4,221 @@ import { useState, useEffect } from "react" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { CheckCircle2, MessageSquare, FileText, BookMarked, Save } from "lucide-react" +import { CheckCircle2, MessageSquare, FileText, BookMarked, Save, Loader2, ArrowLeft } from "lucide-react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" interface LearningScreenProps { onNavigate: (page: string) => void; learningState: any; setLearningState: (state: any) => void; + onProgressUpdated?: () => void | Promise; } -export default function LearningScreen({ onNavigate, learningState, setLearningState }: LearningScreenProps) { - const { isCompleted, notes, messages, inputMessage } = learningState; +export default function LearningScreen({ onNavigate, learningState, setLearningState, onProgressUpdated }: LearningScreenProps) { + const { isCompleted, notes, messages, inputMessage, chatLoading, selectedGoalTitle, selectedModule } = learningState; const [saveStatus, setSaveStatus] = useState("idle"); // idle, saving, saved + const [articleLoading, setArticleLoading] = useState(false) + const [articleError, setArticleError] = useState(null) + const [articleContent, setArticleContent] = useState("") + const [videoLoading, setVideoLoading] = useState(false) + const [videoError, setVideoError] = useState(null) + const [video, setVideo] = useState<{ videoId: string; url: string; videoTitle?: string; channelTitle?: string } | null>(null) + const [moduleProgress, setModuleProgress] = useState<{ percent: number; done: number; total: number }>({ percent: 0, done: 0, total: 0 }) const setState = (newState: any) => { setLearningState({ ...learningState, ...newState }); }; - const sendMessage = () => { + const topic = selectedModule?.title || "Learning Module"; + + useEffect(() => { + if (!selectedGoalTitle || !selectedModule?.title) return; + let cancelled = false; + const run = async () => { + setArticleLoading(true); + setArticleError(null); + try { + const base = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + const res = await fetch(`${base}/api/articles/content`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + goalTitle: selectedGoalTitle, + moduleTitle: selectedModule.title, + moduleType: selectedModule.type, + moduleId: selectedModule.id, + }) + }) + const data = await res.json(); + if (!res.ok) throw new Error(data?.error || 'Failed to load article'); + if (!cancelled) setArticleContent((data?.content ?? '').toString()); + } catch (e: any) { + if (!cancelled) setArticleError(e?.message || 'Failed to load article'); + } finally { + if (!cancelled) setArticleLoading(false); + } + }; + run(); + return () => { cancelled = true; }; + }, [selectedGoalTitle, selectedModule?.title]) + + useEffect(() => { + if (!selectedGoalTitle || !selectedModule?.title) return; + let cancelled = false; + const run = async () => { + setVideoLoading(true); + setVideoError(null); + const base = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + const language = typeof window !== 'undefined' ? (localStorage.getItem('language') || 'english') : 'english'; + let lastErr: any = null; + for (let attempt = 1; attempt <= 2; attempt++) { + try { + const res = await fetch(`${base}/api/videos/content`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + goalTitle: selectedGoalTitle, + moduleTitle: selectedModule.title, + moduleId: selectedModule.id, + language, + }) + }) + const data = await res.json(); + if (!res.ok) throw new Error(data?.error || 'Failed to load video'); + if (!cancelled) setVideo({ videoId: data.videoId, url: data.url, videoTitle: data.videoTitle, channelTitle: data.channelTitle }); + lastErr = null; + break; + } catch (e: any) { + lastErr = e; + await new Promise(r => setTimeout(r, 200 * attempt)); + } + } + if (!cancelled) { + if (lastErr) setVideoError(lastErr?.message || 'Failed to load video'); + setVideoLoading(false); + } + }; + run(); + return () => { cancelled = true; }; + }, [selectedGoalTitle, selectedModule?.title, selectedModule?.id]) + + // Fetch module/goal progress for sidebar + useEffect(() => { + const fetchProgress = async () => { + if (!selectedGoalTitle) return; + try { + const base = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + const res = await fetch(`${base}/api/study-plan/progress?goalTitle=${encodeURIComponent(selectedGoalTitle)}`, { + headers: { + ...(token ? { Authorization: `Bearer ${token}` } : {}), + } + }) + const data = await res.json(); + if (res.ok) { + const done = Number(data?.completedModules ?? 0) + const total = Number(data?.totalModules ?? 0) + const percent = Number(data?.goalProgress ?? (total > 0 ? Math.round(done * 100 / total) : 0)) + setModuleProgress({ percent, done, total }) + } + } catch {} + } + fetchProgress() + }, [selectedGoalTitle]) + + const toggleCompletion = async () => { + if (!selectedGoalTitle || !selectedModule?.id) { + setState({ isCompleted: !isCompleted }) + return + } + const newVal = !isCompleted + setState({ isCompleted: newVal }) + try { + const base = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + const res = await fetch(`${base}/api/study-plan/complete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + goalTitle: selectedGoalTitle, + moduleId: selectedModule.id, + moduleTitle: selectedModule.title, + completed: newVal, + }) + }) + const data = await res.json() + if (res.ok) { + const done = Number(data?.completedModules ?? 0) + const total = Number(data?.totalModules ?? 0) + const percent = Number(data?.goalProgress ?? (total > 0 ? Math.round(done * 100 / total) : 0)) + setModuleProgress({ percent, done, total }) + if (onProgressUpdated) await onProgressUpdated() + } + } catch {} + } + + const sendMessage = async () => { if (inputMessage.trim()) { const newMessages = [...messages, { id: messages.length + 1, role: "user", text: inputMessage }]; - setState({ messages: newMessages, inputMessage: "" }); - - setTimeout(() => { - setState({ messages: [ - ...newMessages, - { - id: newMessages.length + 1, - role: "tutor", - text: "Great question! The derivative measures how a function changes at a specific point...", + setState({ messages: newMessages, inputMessage: "", chatLoading: true }); + + try { + const endpoint = process.env.NEXT_PUBLIC_BACKEND_URL + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/ai-chat` + : "http://localhost:8080/api/ai-chat"; + const payloadMessages = newMessages.map((m: any) => ({ role: m.role, text: m.text })); + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + const res = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), }, - ]}) - }, 500) + body: JSON.stringify({ + messages: payloadMessages, + systemPrompt: `You are a friendly, student-focused AI tutor. Write answers in clean Markdown (headings, lists, bold) with step-by-step clarity. Do NOT use LaTeX or $...$ or \\(...\\) or \\[...\\]. Use plain-text math: exponents with ^ (x^2), fractions as a/b, and inline code for short expressions (like x^2 + 1). Stay strictly on the topic: ${topic}.`, + }), + }); + if (!res.ok) throw new Error("Failed to get AI response"); + const data = await res.json(); + const reply = (data?.reply ?? "I couldn't generate a response.").toString(); + const clean = reply.replace(/\$/g, "").replace(/\\[()\\[\\]]/g, ""); + setState({ + messages: [ + ...newMessages, + { + id: newMessages.length + 1, + role: "tutor", + text: clean, + }, + ], + chatLoading: false, + }); + } catch (e) { + setState({ + messages: [ + ...newMessages, + { + id: newMessages.length + 1, + role: "tutor", + text: "Sorry, I had trouble answering that. Please try again.", + }, + ], + chatLoading: false, + }); + } } } @@ -58,12 +242,17 @@ export default function LearningScreen({ onNavigate, learningState, setLearningS return (
-
-

Power Rule & Chain Rule

-

Calculus Fundamentals

+
+ +
+

{topic}

+

{selectedGoalTitle || 'Personalized Learning'}

+