From 173ce4ca04e9624754e3e21a0d5999ae6e6f4832 Mon Sep 17 00:00:00 2001 From: ectasy663 Date: Fri, 25 Sep 2026 16:01:38 +0530 Subject: [PATCH] UI fixes: Admin UI padding, AI Tutor Chat History, DPP filtering and Premium Gates --- backend/main.py | 3 + src/app/admin/tests/page.tsx | 2 +- src/app/dashboard/cbt/page.tsx | 3 +- src/app/dashboard/dpp/page.tsx | 53 ++++++++---- src/components/AI/TutorChat.tsx | 145 ++++++++++++++++++++++++++------ 5 files changed, 161 insertions(+), 45 deletions(-) diff --git a/backend/main.py b/backend/main.py index fbaea7d..810e71e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -445,6 +445,8 @@ def list_active_tests(exam_pattern: Optional[str] = None, type: Optional[str] = if exam_pattern: if exam_pattern == 'JEE_NEET': pass + elif exam_pattern == 'JEE': + query = query.where(models.Test.exam_pattern.in_(['JEE_Main', 'JEE_Advanced'])) else: query = query.where(models.Test.exam_pattern == exam_pattern) if type: @@ -1839,3 +1841,4 @@ def verify_payment(payload: VerifyPaymentPayload, session: Session = Depends(get return {"success": True, "tier": user.tier.value} + diff --git a/src/app/admin/tests/page.tsx b/src/app/admin/tests/page.tsx index e5bfdf1..7ef496e 100644 --- a/src/app/admin/tests/page.tsx +++ b/src/app/admin/tests/page.tsx @@ -255,7 +255,7 @@ export default function AdminTestsPage() { } return ( -
+

Tests & DPPs

Manage CBT examinations and daily practice sheets

diff --git a/src/app/dashboard/cbt/page.tsx b/src/app/dashboard/cbt/page.tsx index 63b5e48..d28324c 100644 --- a/src/app/dashboard/cbt/page.tsx +++ b/src/app/dashboard/cbt/page.tsx @@ -63,7 +63,7 @@ export default function CBTPage() { setLoading(true); try { let url = `/api/tests?user_id=${userId}&type=Mock`; - if (selectedPattern !== "JEE" && selectedPattern !== "JEE_NEET") { + if (selectedPattern) { url += `&exam_pattern=${selectedPattern}`; } const testRes = await fetchWithAuth(url); @@ -363,3 +363,4 @@ export default function CBTPage() { ); } + diff --git a/src/app/dashboard/dpp/page.tsx b/src/app/dashboard/dpp/page.tsx index 1201bca..19776bd 100644 --- a/src/app/dashboard/dpp/page.tsx +++ b/src/app/dashboard/dpp/page.tsx @@ -8,6 +8,7 @@ import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye import { useEffect, useState } from "react" import Link from "next/link" import { useActiveUser } from "@/hooks/useActiveUser" +import { PremiumGate } from "@/components/PremiumGate" export default function AIDailyPracticePage() { const { user, userId } = useActiveUser() @@ -27,9 +28,22 @@ export default function AIDailyPracticePage() { try { const pattern = user.target_exam || "JEE_Main"; // Also fetch from the correct Next.js proxied route instead of localhost:8000 - const res = await fetchWithAuth(`/api/tests?type=DPS&exam_pattern=${pattern}`); + const res = await fetchWithAuth(`/api/tests?type=DPS`); if (res.ok) { - const data = await res.json() + let data = await res.json() + + // Filter by subject based on target exam + const target = user.target_exam || "JEE_Main"; + data = data.filter((dpp: any) => { + const title = dpp.title.toLowerCase(); + const isMath = title.includes("math"); + const isBio = title.includes("bio") || title.includes("botany") || title.includes("zoology"); + + if (target === "NEET" && isMath) return false; + if (target.startsWith("JEE") && target !== "JEE_NEET" && isBio) return false; + return true; + }); + setDpps(data) } @@ -155,7 +169,7 @@ export default function AIDailyPracticePage() {
- {isAttempted ? :
} + {isAttempted ? :
}
@@ -200,28 +214,34 @@ export default function AIDailyPracticePage() { ) : isExpired && !isAttempted && !dpp.pdf_url ? ( ) : dpp.pdf_url ? ( - + View PDF}> + + ) : isAttempted ? ( <> {!isExpired && ( - - - + Retake}> + + + + )} ) : ( - - - + Launch Practice Test}> + + + + )}
@@ -335,3 +355,4 @@ export default function AIDailyPracticePage() {
) } + diff --git a/src/components/AI/TutorChat.tsx b/src/components/AI/TutorChat.tsx index 30e2ecf..e7a3d66 100644 --- a/src/components/AI/TutorChat.tsx +++ b/src/components/AI/TutorChat.tsx @@ -3,7 +3,7 @@ import { fetchWithAuth } from "@/lib/apiClient"; import { useState, useRef, useEffect } from "react" import { Card, CardContent } from "@/components/ui/card" -import { Send, Loader2, BrainCircuit, Sparkles } from "lucide-react" +import { Send, Loader2, BrainCircuit, Sparkles, PlusCircle, MessageSquare } from "lucide-react" import ReactMarkdown from "react-markdown" import remarkMath from "remark-math" import rehypeKatex from "rehype-katex" @@ -16,6 +16,13 @@ interface Message { content: string } +interface ChatSession { + id: string; + title: string; + timestamp: number; + messages: Message[]; +} + const QUICK_PROMPTS = [ "Explain Newton's laws with an example", "What is the difference between speed and velocity?", @@ -23,10 +30,11 @@ const QUICK_PROMPTS = [ "Solve: Integration of x² from 0 to 3", ] +const INITIAL_MSG: Message = { role: "assistant", content: "Hello! I'm your PrepPilot AI. Ask me anything — from concept explanations to step-by-step problem solving for JEE & NEET topics." } + export function TutorChat() { - const [messages, setMessages] = useState([ - { role: "assistant", content: "Hello! I'm your PrepPilot AI. Ask me anything — from concept explanations to step-by-step problem solving for JEE & NEET topics." } - ]) + const [sessions, setSessions] = useState([]) + const [activeSessionId, setActiveSessionId] = useState("") const [input, setInput] = useState("") const [streaming, setStreaming] = useState(false) const messagesEndRef = useRef(null) @@ -36,21 +44,50 @@ export function TutorChat() { // Load from localStorage on mount useEffect(() => { if (userId) { + const savedV2 = localStorage.getItem("ai_tutor_chats_v2_" + userId) + if (savedV2) { + try { + const parsed = JSON.parse(savedV2) + if (parsed && parsed.length > 0) { + setSessions(parsed) + setActiveSessionId(parsed[0].id) + return + } + } catch (e) {} + } + + // Fallback to legacy const saved = localStorage.getItem("ai_tutor_chat_" + userId) if (saved) { try { - setMessages(JSON.parse(saved)) + const parsedMsgs = JSON.parse(saved) + if (parsedMsgs && parsedMsgs.length > 1) { + const legacySession = { + id: Date.now().toString(), + title: "Previous Chat", + timestamp: Date.now(), + messages: parsedMsgs + } + setSessions([legacySession]) + setActiveSessionId(legacySession.id) + return + } } catch (e) {} } + + // Default empty session + handleNewChat() } }, [userId]) - // Save to localStorage whenever messages change + // Save to localStorage whenever sessions change useEffect(() => { - if (userId && messages.length > 1) { // Only save if there's actual history beyond initial message - localStorage.setItem("ai_tutor_chat_" + userId, JSON.stringify(messages)) + if (userId && sessions.length > 0) { + localStorage.setItem("ai_tutor_chats_v2_" + userId, JSON.stringify(sessions)) } - }, [messages, userId]) + }, [sessions, userId]) + + const activeMessages = sessions.find(s => s.id === activeSessionId)?.messages || [INITIAL_MSG] const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) @@ -58,16 +95,33 @@ export function TutorChat() { useEffect(() => { scrollToBottom() - }, [messages]) + }, [activeMessages]) + + const handleNewChat = () => { + const newId = Date.now().toString() + setSessions(prev => [{ + id: newId, + title: "New Chat", + timestamp: Date.now(), + messages: [INITIAL_MSG] + }, ...prev]) + setActiveSessionId(newId) + } const sendMessage = async (text: string) => { if (!text.trim() || streaming || userLoading || !userId) return setInput("") - setMessages(prev => [...prev, { role: "user", content: text.trim() }]) - setMessages(prev => [...prev, { role: "assistant", content: "" }]) setStreaming(true) + setSessions(prev => prev.map(s => { + if (s.id === activeSessionId) { + const title = s.messages.length === 1 ? text.trim().substring(0, 30) + "..." : s.title; + return { ...s, title, messages: [...s.messages, { role: "user", content: text.trim() }, { role: "assistant", content: "" }] } + } + return s + })) + try { const res = await fetchWithAuth("/api/ai-coach/chat/stream", { method: "POST", @@ -84,23 +138,29 @@ export function TutorChat() { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) - setMessages(prev => { - const msgs = [...prev] - msgs[msgs.length - 1].content += chunk - return msgs - }) + + setSessions(prev => prev.map(s => { + if (s.id === activeSessionId) { + const msgs = [...s.messages] + msgs[msgs.length - 1].content += chunk + return { ...s, messages: msgs } + } + return s + })) } } catch { - // Graceful fallback — don't expose internal errors const fallback = "I'm having trouble connecting right now. Please try again in a moment. In the meantime, you can browse your **Study Plan** or review your **Mistake Queue** for offline practice." const words = fallback.split(" ") for (let i = 0; i < words.length; i++) { await new Promise(r => setTimeout(r, 40)) - setMessages(prev => { - const msgs = [...prev] - msgs[msgs.length - 1].content += (i === 0 ? "" : " ") + words[i] - return msgs - }) + setSessions(prev => prev.map(s => { + if (s.id === activeSessionId) { + const msgs = [...s.messages] + msgs[msgs.length - 1].content += (i === 0 ? "" : " ") + words[i] + return { ...s, messages: msgs } + } + return s + })) } } finally { setStreaming(false) @@ -113,8 +173,32 @@ export function TutorChat() { } return ( -
- +
+ {/* Sidebar for chat history */} +
+ +
+ Past Chats + {sessions.map(s => ( + + ))} +
+
+ + {/* Main Chat Area */} + {/* Chat Header */}
@@ -126,11 +210,18 @@ export function TutorChat() { Online · JEE & NEET specialist
+
{/* Messages */} - {messages.map((msg, idx) => ( + {activeMessages.map((msg, idx) => (
{/* Quick Prompts — shown only when only 1 message (initial) */} - {messages.length === 1 && ( + {activeMessages.length === 1 && (
{QUICK_PROMPTS.map(p => (