diff --git a/PragatiVeer_v2_Production_QA_Report.pdf b/PragatiVeer_v2_Production_QA_Report.pdf new file mode 100644 index 0000000..d7d2ebf Binary files /dev/null and b/PragatiVeer_v2_Production_QA_Report.pdf differ diff --git a/backend/main.py b/backend/main.py index 8de6646..39c70c8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,6 +8,7 @@ from uuid import UUID from datetime import datetime, timedelta, date from pydantic import BaseModel import os +import re from database import init_db, get_session import models @@ -838,14 +839,14 @@ def get_leaderboard( # Calculate real user's stats user_score = 0 user_accuracy = "0%" - user_streak = 1 + user_streak = 0 # BUG-04 fix: real zero for new users (no artificial min floor) if requesting_user: if str(requesting_user.id) in DEMO_USER_UUIDS: user_score = 9250 user_accuracy = "88.5%" user_streak = requesting_user.streak_count or 25 else: - user_streak = max(1, requesting_user.streak_count) + user_streak = requesting_user.streak_count or 0 # BUG-04 fix: 0 is valid user_attempts = session.exec( select(models.Attempt).where( models.Attempt.user_id == requesting_user.id, @@ -871,10 +872,11 @@ def get_leaderboard( "isCurrentUser": False, }) - # Add the requesting real user + # Add the requesting real user — strip role annotations from display name if requesting_user: + clean_name = re.sub(r'\s*\([^)]*\)\s*$', '', requesting_user.name).strip() entries.append({ - "name": requesting_user.name, + "name": clean_name, "score": user_score, "accuracy": user_accuracy, "streak": user_streak, diff --git a/src/app/dashboard/ai-coach/cbt-dpp/page.tsx b/src/app/dashboard/ai-coach/cbt-dpp/page.tsx index 032bdea..6d56dea 100644 --- a/src/app/dashboard/ai-coach/cbt-dpp/page.tsx +++ b/src/app/dashboard/ai-coach/cbt-dpp/page.tsx @@ -185,10 +185,12 @@ export default function AIDailyPracticePage() {

{cleanTitle(dpp.title)} - +

+
+ {dpp.difficulty} - +
{count} Questions • {dpp.total_time} mins {dpp.scheduled_date && ( diff --git a/src/app/dashboard/cbt/page.tsx b/src/app/dashboard/cbt/page.tsx index 1cb396f..46aacd7 100644 --- a/src/app/dashboard/cbt/page.tsx +++ b/src/app/dashboard/cbt/page.tsx @@ -411,7 +411,14 @@ export default function CBTPage() {
) : attempts.filter(a => a.source_type === "Test" && exams.some(e => e.id === a.source_id)).map((attempt, i) => { const exam = exams.find(e => e.id === attempt.source_id); - const scoreCalc = (attempt.score * 4) - ((attempt.total_questions - attempt.score) * 1); // rough calc for display + // BUG-07 fix: use actual final_score from backend if available, else derive correctly + // backend stores final_score = (correct*4) - (incorrect*1) + const correctCount = attempt.answers ? attempt.answers.filter((a: any) => a.is_correct).length : attempt.score; + const incorrectCount = attempt.answers ? (attempt.answers.filter((a: any) => a.selected_answer !== null).length - correctCount) : 0; + const scoreCalc = attempt.final_score ?? ((correctCount * 4) - (incorrectCount * 1)); + const maxScore = attempt.total_questions * 4; + const scorePercent = maxScore > 0 ? (scoreCalc / maxScore) * 100 : 0; + const scoreColor = scorePercent >= 60 ? 'text-green-500' : scorePercent >= 35 ? 'text-amber-500' : 'text-red-500'; return ( @@ -424,8 +431,8 @@ export default function CBTPage() {
Score
- {scoreCalc} - /{attempt.total_questions * 4} + {scoreCalc} + /{maxScore}
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 4ab852b..811021f 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -49,7 +49,11 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { return () => document.removeEventListener("mousedown", handleClickOutside) }, []) - const userInitials = user ? user.name.split(" ").filter(n => /^[a-zA-Z]/.test(n)).slice(0, 2).map(n => n[0]).join("").toUpperCase() : "DS" + // BUG-05 fix: strip any parenthetical role annotations from display names + // e.g. "Rahul (AI Coach)" → "Rahul", "Priya (NEET Aspirant)" → "Priya" + const cleanDisplayName = (name: string) => name.replace(/\s*\([^)]*\)\s*$/, '').trim(); + + const userInitials = user ? cleanDisplayName(user.name).split(" ").filter(n => /^[a-zA-Z]/.test(n)).slice(0, 2).map(n => n[0]).join("").toUpperCase() : "DS" const targetExamLabel = user?.target_exam === "JEE_Main" ? "JEE" : user?.target_exam === "JEE_Advanced" ? "JEE Advanced" : user?.target_exam === "NEET" ? "NEET" : "NEET/JEE" // Trial logic removed as requested @@ -251,7 +255,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { {dropdownOpen && user && (
-

{user.name}

+

{cleanDisplayName(user.name)}

{user.email || user.phone}

{/* Trial info in dropdown */} {trialInfo.isTrialActive && ( @@ -295,7 +299,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { {userInitials}
-

{user ? user.name.split(" ")[0] : "Loading..."}

+

{user ? cleanDisplayName(user.name).split(" ")[0] : "Loading..."}

{trialInfo.isTrialActive ? ( @@ -319,7 +323,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {

- {user ? `Welcome back, ${user.name.split(" ")[0]}!` : "Welcome back!"} + {user ? `Welcome back, ${cleanDisplayName(user.name).split(" ")[0]}!` : "Welcome back!"}

{user && ( diff --git a/src/app/dashboard/leaderboard/page.tsx b/src/app/dashboard/leaderboard/page.tsx index 2a2531b..2e1890e 100644 --- a/src/app/dashboard/leaderboard/page.tsx +++ b/src/app/dashboard/leaderboard/page.tsx @@ -14,6 +14,9 @@ export default function LeaderboardPage() { const [rankings, setRankings] = useState([]) const [loadingRankings, setLoadingRankings] = useState(true) + // Strip parenthetical role annotations from user names (e.g. "Rahul (AI Coach)" → "Rahul") + const cleanDisplayName = (name: string) => name.replace(/\s*\([^)]*\)\s*$/, '').trim(); + useEffect(() => { async function fetchLeaderboard() { setLoadingRankings(true) @@ -39,7 +42,7 @@ export default function LeaderboardPage() { rank: rankings.length + 1, score: 0, streak: user?.streak_count || 1, - name: user?.name || "You" + name: user?.name ? cleanDisplayName(user.name) : "You" } return ( @@ -121,7 +124,7 @@ export default function LeaderboardPage() {

- {student.isCurrentUser ? (isAnonymous ? "Anonymous User" : `${student.name} (You)`) : student.name} + {student.isCurrentUser ? (isAnonymous ? "Anonymous User" : `${cleanDisplayName(student.name)} (You)`) : cleanDisplayName(student.name)}

Accuracy: {student.accuracy} diff --git a/src/app/dashboard/rank-predictor/page.tsx b/src/app/dashboard/rank-predictor/page.tsx index f6ca213..184ed1f 100644 --- a/src/app/dashboard/rank-predictor/page.tsx +++ b/src/app/dashboard/rank-predictor/page.tsx @@ -85,7 +85,7 @@ export default function RankPredictorPage() {

{user && (
- Profile Connected: {user.name} ({user.target_exam?.replace("_", " ")}) + Profile Connected: {user.name.replace(/\s*\([^)]*\)\s*$/, '').trim()} ({user.target_exam?.replace("_", " ")})
)} diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 413d07c..ad823f2 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -32,7 +32,9 @@ export default function SettingsPage() { setAvatarUrl(localStorage.getItem(`user_avatar_${user.id}`) || null); if (user.name) { - const parts = user.name.split(" "); + // BUG-06 fix: strip parenthetical role annotations before splitting into first/last + const cleanName = user.name.replace(/\s*\([^)]*\)\s*$/, '').trim(); + const parts = cleanName.split(" "); setFirstName(parts[0] || ""); setLastName(parts.slice(1).join(" ") || ""); } diff --git a/src/components/AI/AITutorChat.tsx b/src/components/AI/AITutorChat.tsx index 77f6450..a11e7f3 100644 --- a/src/components/AI/AITutorChat.tsx +++ b/src/components/AI/AITutorChat.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useRef, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -39,6 +39,15 @@ export function AITutorChat() { const [input, setInput] = useState(''); const [isOpen, setIsOpen] = useState(false); const [isExpanded, setIsExpanded] = useState(false); + // BUG-03 fix: ref to anchor bottom of chat for auto-scroll + const bottomRef = useRef(null); + + // Auto-scroll to latest message whenever messages change + useEffect(() => { + if (bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [messages]); const handleSend = () => { if (!input.trim()) return; @@ -118,6 +127,8 @@ export function AITutorChat() {
))} + {/* BUG-03 fix: invisible scroll anchor */} +
diff --git a/src/components/CBT/Calculator.tsx b/src/components/CBT/Calculator.tsx index 4e651ae..2261c6d 100644 --- a/src/components/CBT/Calculator.tsx +++ b/src/components/CBT/Calculator.tsx @@ -20,8 +20,17 @@ export function Calculator({ isOpen, onClose }: CalculatorProps) { const handleCalculate = () => { try { + if (!display.trim()) return; + + // BUG-02 fix: strip trailing operator(s) so "5*", "9+", "/2" (leading slash) don't crash + const sanitized = display + .replace(/[+\-*/^]+$/, "") // remove trailing operators + .replace(/^[*/^]+/, ""); // remove leading * / ^ (but allow leading minus for negatives) + + if (!sanitized.trim()) return; + // Map functions to JS Math - let mappedExp = display + let mappedExp = sanitized .replace(/sin\(/g, 'Math.sin(') .replace(/cos\(/g, 'Math.cos(') .replace(/tan\(/g, 'Math.tan(') @@ -30,8 +39,15 @@ export function Calculator({ isOpen, onClose }: CalculatorProps) { .replace(/sqrt\(/g, 'Math.sqrt(') .replace(/\^/g, '**'); + // eslint-disable-next-line no-new-func const result = new Function('return ' + mappedExp)(); + // Guard against NaN / Infinity + if (result === null || result === undefined || !isFinite(result) || isNaN(result)) { + setDisplay("Error"); + return; + } + // Handle floating point precision issues simply if (typeof result === 'number' && !Number.isInteger(result)) { setDisplay(String(parseFloat(result.toFixed(6)))); @@ -43,6 +59,7 @@ export function Calculator({ isOpen, onClose }: CalculatorProps) { } }; + const handleClear = () => setDisplay(""); const handleBackspace = () => setDisplay((prev) => prev.slice(0, -1)); diff --git a/src/components/Dashboard/StreakOnboardingModal.tsx b/src/components/Dashboard/StreakOnboardingModal.tsx index ea0e241..8657685 100644 --- a/src/components/Dashboard/StreakOnboardingModal.tsx +++ b/src/components/Dashboard/StreakOnboardingModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Flame, Sparkles } from "lucide-react"; +import { Flame, Sparkles, X } from "lucide-react"; import { useActiveUser } from "@/hooks/useActiveUser"; import { useTheme } from "next-themes"; @@ -42,7 +42,7 @@ export function StreakOnboardingModal() { wishMessage = `Incredible! A ${streak}-day streak shows true championship discipline. You are outperforming the competition. Let's make today count!`; } - const handleEnter = () => { + const handleDismiss = () => { sessionStorage.setItem(`hasSeenStreakPopup_${user.id}`, "true"); setIsOpen(false); }; @@ -57,10 +57,24 @@ export function StreakOnboardingModal() { } return ( -
- - {/* Centered Modal Card Container covering 80% screen size (w-[80vw] h-[80vh]) */} -
+ // BUG-01 fix: backdrop click dismisses modal +
+ {/* Centered Modal Card — stop propagation so inner clicks don't close */} +
e.stopPropagation()} + > + {/* BUG-01 fix: X close button */} + {/* Subtle background glow */}
@@ -112,7 +126,7 @@ export function StreakOnboardingModal() { {/* Action Button */}