Fix AI Coach typo, fix bulk register UI, and update exam tiers

This commit is contained in:
2026-09-24 21:59:04 +05:30
parent 6ba69a05ea
commit ec4849ccce
13 changed files with 87 additions and 34 deletions

View File

@@ -1,7 +1,7 @@
from fastapi import FastAPI, Depends, HTTPException, status, Request, UploadFile, File, Form, BackgroundTasks
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import Session, select
from sqlmodel import Session, select, delete
from contextlib import asynccontextmanager
from typing import List, Dict, Any, Optional
from uuid import UUID
@@ -286,6 +286,21 @@ async def bulk_register_students(file: UploadFile = File(...), session: Session
session.commit()
return results
class DeleteUsersRequest(BaseModel):
user_ids: List[UUID]
@app.delete("/api/admin/students/bulk", tags=["Admin"])
def bulk_delete_students(payload: DeleteUsersRequest, session: Session = Depends(get_session)):
for uid in payload.user_ids:
user = session.get(models.User, uid)
if user:
# Delete associated records manually since foreign key cascade might not be configured in sqlmodel by default
session.exec(delete(models.Attempt).where(models.Attempt.user_id == uid))
session.exec(delete(models.Mistake).where(models.Mistake.user_id == uid))
session.delete(user)
session.commit()
return {"success": True}
@app.post("/api/auth/register", tags=["Authentication"])
def register(payload: RegisterPayload, session: Session = Depends(get_session)):
import bcrypt

View File

@@ -24,7 +24,7 @@ class MediumEnum(str, Enum):
class TierEnum(str, Enum):
FREE = "Free"
PRO = "Pro"
AI_COACH = "Al_Coach"
AI_COACH = "AI Coach"
class QuestionTypeEnum(str, Enum):
MCQ_SINGLE = "MCQ_Single"

31
demo_30_users.csv Normal file
View File

@@ -0,0 +1,31 @@
name,email,phone,target_exam,tier
Aarav Sharma,aarav.sharma@example.com,9876543201,JEE + NEET,Pro
Vivaan Gupta,vivaan.gupta@example.com,9876543202,NEET,Free
Aditya Patel,aditya.patel@example.com,9876543203,JEE,Pro
Vihaan Kumar,vihaan.kumar@example.com,9876543204,JEE + NEET,Pro
Arjun Singh,arjun.singh@example.com,9876543205,JEE,Free
Sai Reddy,sai.reddy@example.com,9876543206,JEE + NEET,AI Coach
Reyansh Das,reyansh.das@example.com,9876543207,NEET,Pro
Ayaan Joshi,ayaan.joshi@example.com,9876543208,JEE + NEET,Free
Krishna Iyer,krishna.iyer@example.com,9876543209,NEET,Pro
Ishaan Verma,ishaan.verma@example.com,9876543210,JEE,Free
Shaurya Rajput,shaurya.rajput@example.com,9876543211,JEE,AI Coach
Atharv Nair,atharv.nair@example.com,9876543212,NEET,Free
Ananya Menon,ananya.menon@example.com,9876543213,JEE + NEET,Pro
Diya Agarwal,diya.agarwal@example.com,9876543214,JEE + NEET,AI Coach
Pari Kapoor,pari.kapoor@example.com,9876543215,JEE + NEET,Free
Navya Jain,navya.jain@example.com,9876543216,JEE,Free
Kavya Desai,kavya.desai@example.com,9876543217,JEE + NEET,Free
Myra Choudhury,myra.choudhury@example.com,9876543218,JEE,Pro
Ira Sengupta,ira.sengupta@example.com,9876543219,JEE,Free
Saanvi Bhatt,saanvi.bhatt@example.com,9876543220,JEE + NEET,Pro
Rudra Tiwari,rudra.tiwari@example.com,9876543221,NEET,Pro
Kabir Mehra,kabir.mehra@example.com,9876543222,NEET,Free
Madhav Rao,madhav.rao@example.com,9876543223,JEE + NEET,AI Coach
Dhruv Menon,dhruv.menon@example.com,9876543224,JEE,Free
Aryan Dutta,aryan.dutta@example.com,9876543225,JEE + NEET,Free
Prisha Soni,prisha.soni@example.com,9876543226,JEE,Pro
Avni Khatri,avni.khatri@example.com,9876543227,JEE + NEET,Pro
Zara Sheikh,zara.sheikh@example.com,9876543228,JEE,Free
Riya Mishra,riya.mishra@example.com,9876543229,JEE,Free
Sneha Chawla,sneha.chawla@example.com,9876543230,JEE,Free
1 name email phone target_exam tier
2 Aarav Sharma aarav.sharma@example.com 9876543201 JEE + NEET Pro
3 Vivaan Gupta vivaan.gupta@example.com 9876543202 NEET Free
4 Aditya Patel aditya.patel@example.com 9876543203 JEE Pro
5 Vihaan Kumar vihaan.kumar@example.com 9876543204 JEE + NEET Pro
6 Arjun Singh arjun.singh@example.com 9876543205 JEE Free
7 Sai Reddy sai.reddy@example.com 9876543206 JEE + NEET AI Coach
8 Reyansh Das reyansh.das@example.com 9876543207 NEET Pro
9 Ayaan Joshi ayaan.joshi@example.com 9876543208 JEE + NEET Free
10 Krishna Iyer krishna.iyer@example.com 9876543209 NEET Pro
11 Ishaan Verma ishaan.verma@example.com 9876543210 JEE Free
12 Shaurya Rajput shaurya.rajput@example.com 9876543211 JEE AI Coach
13 Atharv Nair atharv.nair@example.com 9876543212 NEET Free
14 Ananya Menon ananya.menon@example.com 9876543213 JEE + NEET Pro
15 Diya Agarwal diya.agarwal@example.com 9876543214 JEE + NEET AI Coach
16 Pari Kapoor pari.kapoor@example.com 9876543215 JEE + NEET Free
17 Navya Jain navya.jain@example.com 9876543216 JEE Free
18 Kavya Desai kavya.desai@example.com 9876543217 JEE + NEET Free
19 Myra Choudhury myra.choudhury@example.com 9876543218 JEE Pro
20 Ira Sengupta ira.sengupta@example.com 9876543219 JEE Free
21 Saanvi Bhatt saanvi.bhatt@example.com 9876543220 JEE + NEET Pro
22 Rudra Tiwari rudra.tiwari@example.com 9876543221 NEET Pro
23 Kabir Mehra kabir.mehra@example.com 9876543222 NEET Free
24 Madhav Rao madhav.rao@example.com 9876543223 JEE + NEET AI Coach
25 Dhruv Menon dhruv.menon@example.com 9876543224 JEE Free
26 Aryan Dutta aryan.dutta@example.com 9876543225 JEE + NEET Free
27 Prisha Soni prisha.soni@example.com 9876543226 JEE Pro
28 Avni Khatri avni.khatri@example.com 9876543227 JEE + NEET Pro
29 Zara Sheikh zara.sheikh@example.com 9876543228 JEE Free
30 Riya Mishra riya.mishra@example.com 9876543229 JEE Free
31 Sneha Chawla sneha.chawla@example.com 9876543230 JEE Free

View File

@@ -11,7 +11,7 @@ export default function SettingsPage() {
allow_new_registrations: true,
enable_free_trial: true,
trial_duration_days: 5,
trial_tier: "Al_Coach"
trial_tier: "AI Coach"
});
const [loading, setLoading] = useState(true);
@@ -117,7 +117,7 @@ export default function SettingsPage() {
onChange={(e) => setSettings(prev => ({ ...prev, trial_tier: e.target.value }))}
className="w-full bg-background border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--cyan-ai)]">
<option value="Pro">Pro Tier</option>
<option value="Al_Coach">AI Coach (All Features)</option>
<option value="AI Coach">AI Coach (All Features)</option>
</select>
</div>
</div>

View File

@@ -13,11 +13,11 @@ import {
} from "lucide-react"
const TIER_STYLE: Record<string, string> = {
"Al_Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)]",
"AI Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)]",
"Pro": "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
"Free": "bg-secondary text-foreground border-border",
}
const TIER_LABEL: Record<string, string> = { "Al_Coach": "AI Coach", "Pro": "Pro", "Free": "Free" }
const TIER_LABEL: Record<string, string> = { "AI Coach": "AI Coach", "Pro": "Pro", "Free": "Free" }
function formatDate(iso?: string | null) {
if (!iso) return "—"
@@ -164,7 +164,7 @@ export default function StudentKundliPage() {
>
<option value="Free">Free</option>
<option value="Pro">Pro</option>
<option value="Al_Coach">AI Coach</option>
<option value="AI Coach">AI Coach</option>
</select>
) : (
<Badge className={TIER_STYLE[data.tier] || TIER_STYLE["Free"]} variant="outline">
@@ -178,13 +178,14 @@ export default function StudentKundliPage() {
onChange={e => setEditExam(e.target.value)}
className="text-sm bg-secondary border border-border rounded-lg px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
>
<option value="JEE_Main">JEE</option>
<option value="NEET">NEET</option>
<option value="JEE_Main">JEE Main</option>
<option value="JEE_Advanced">JEE Advanced</option>
<option value="JEE_NEET">JEE + NEET</option>
</select>
) : (
<Badge variant="outline" className="border-border">{data.target_exam.replace("_", " ")}</Badge>
<Badge variant="outline" className="border-border">
{data.target_exam === "JEE_Main" ? "JEE" : data.target_exam.replace("_", " ")}
</Badge>
)}
</div>

View File

@@ -9,13 +9,13 @@ import { useRouter } from "next/navigation"
import { useRef } from "react"
const TIER_STYLE: Record<string, string> = {
"Al_Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)] font-bold",
"AI Coach": "bg-[var(--cyan-ai-10)] text-[var(--cyan-ai)] border-[var(--cyan-ai-20)] font-bold",
"Pro": "bg-yellow-500/10 text-yellow-500 border-yellow-500/20 font-bold",
"Free": "bg-secondary text-foreground border-border",
}
const TIER_LABEL: Record<string, string> = {
"Al_Coach": "AI Coach",
"AI Coach": "AI Coach",
"Pro": "Pro",
"Free": "Free",
}
@@ -27,12 +27,14 @@ export default function StudentsPage() {
const [search, setSearch] = useState("")
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [deleting, setDeleting] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const handleBulkUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setIsUploading(true)
setLoading(true)
try {
const formData = new FormData()
@@ -68,6 +70,7 @@ export default function StudentsPage() {
} catch (err: any) {
alert("Error: " + err.message)
} finally {
setIsUploading(false)
setLoading(false)
if (fileInputRef.current) fileInputRef.current.value = ""
}
@@ -141,9 +144,10 @@ export default function StudentsPage() {
<Trash2 size={16} className="mr-2" /> Delete ({selectedIds.length})
</Button>
)}
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} />
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={() => fileInputRef.current?.click()}>
<Upload size={16} className="mr-2" /> Bulk Register CSV
<input type="file" accept=".csv" className="hidden" ref={fileInputRef} onChange={handleBulkUpload} disabled={isUploading} />
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" disabled={isUploading} onClick={() => fileInputRef.current?.click()}>
{isUploading ? <RefreshCw size={16} className="mr-2 animate-spin" /> : <Upload size={16} className="mr-2" />}
{isUploading ? "Uploading..." : "Bulk Register CSV"}
</Button>
<Button variant="outline" className="border-border hover:bg-secondary hover:text-white" onClick={downloadTemplate}>
<Download size={16} className="mr-2" /> Download Template
@@ -236,7 +240,9 @@ export default function StudentsPage() {
{TIER_LABEL[s.tier] || s.tier}
</Badge>
</td>
<td className="px-6 py-4 font-medium">{s.target_exam.replace("_", " ")}</td>
<td className="px-6 py-4 font-medium">
{s.target_exam === "JEE_Main" ? "JEE" : s.target_exam.replace("_", " ")}
</td>
<td className="px-6 py-4 font-mono">{s.total_tests}</td>
<td className="px-6 py-4">
<span className={`font-bold ${s.avg_accuracy >= 80 ? "text-green-500" : s.avg_accuracy >= 60 ? "text-yellow-500" : "text-muted-foreground"}`}>

View File

@@ -64,7 +64,7 @@ function CheckoutContent() {
}
}, [userId, userLoading, router, tier])
const amount = tier === "Al_Coach" ? 11999 : 3499
const amount = tier === "AI Coach" ? 11999 : 3499
const handlePayment = async () => {
if (!address.trim() || !phone.trim() || phone.length < 10) {

View File

@@ -350,7 +350,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<div className="flex-1 min-w-0">
<p className="text-sm font-bold leading-none truncate">{user ? cleanDisplayName(user.name).split(" ")[0] : "Loading..."}</p>
<div className="mt-1">
<TierBadge tier={user ? (user.tier === "Al_Coach" ? "AI Coach" : user.tier === "Pro" ? "Pro" : "Free") : "Free"} />
<TierBadge tier={user ? (user.tier === "AI Coach" ? "AI Coach" : user.tier === "Pro" ? "Pro" : "Free") : "Free"} />
</div>
</div>
</div>

View File

@@ -48,7 +48,7 @@ export default function DashboardPage() {
const rank = isNew ? "#--" : `#${userStats?.city_rank || 1} / ${userStats?.city_total || 50}`
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
const plan = user?.tier === "Al_Coach" ? "AI Coach" : user?.tier || "Free"
const plan = user?.tier === "AI Coach" ? "AI Coach" : user?.tier || "Free"
const name = user?.name || "Student"
const date = new Intl.DateTimeFormat('en-IN', {
dateStyle: 'medium',

View File

@@ -33,9 +33,9 @@ const AI_COACH_FEATURES = [
export default function OnboardingPage() {
const router = useRouter();
const [selecting, setSelecting] = useState<"Pro" | "Al_Coach" | null>(null);
const [selecting, setSelecting] = useState<"Pro" | "AI Coach" | null>(null);
const handleSelect = async (plan: "Pro" | "Al_Coach") => {
const handleSelect = async (plan: "Pro" | "AI Coach") => {
setSelecting(plan);
// Save chosen plan preference to localStorage so dashboard can read it
if (typeof window !== "undefined") {
@@ -165,7 +165,7 @@ export default function OnboardingPage() {
background:
"linear-gradient(135deg, rgba(12,96,96,0.5) 0%, rgba(12,96,96,0.08) 100%)",
}}
onClick={() => !selecting && handleSelect("Al_Coach")}
onClick={() => !selecting && handleSelect("AI Coach")}
>
{/* Most Popular Badge */}
<div className="absolute -top-4 left-1/2 -translate-x-1/2 z-10 inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-[var(--cyan-ai)] text-black text-xs font-black uppercase tracking-wider shadow-[0_0_20px_-3px_var(--cyan-ai)]">
@@ -201,12 +201,12 @@ export default function OnboardingPage() {
<button
onClick={(e) => {
e.stopPropagation();
if (!selecting) handleSelect("Al_Coach");
if (!selecting) handleSelect("AI Coach");
}}
disabled={!!selecting}
className="w-full h-12 rounded-xl bg-[var(--cyan-ai)] text-black font-bold text-base hover:bg-[var(--cyan-ai-80)] transition-all duration-300 shadow-[0_0_20px_-5px_rgba(12,96,96,0.5)] hover:shadow-[0_0_30px_-3px_rgba(12,96,96,0.8)] hover:-translate-y-0.5 disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{selecting === "Al_Coach" ? (
{selecting === "AI Coach" ? (
<>
<span className="size-4 border-2 border-black/30 border-t-black rounded-full animate-spin" />
Starting Trial…

View File

@@ -87,7 +87,7 @@ export default function UpgradePage() {
</div>
<Button
className="w-full font-bold text-lg rounded-2xl h-12 bg-[var(--cyan-ai)] text-black hover:bg-[var(--cyan-ai-80)] transition-all"
onClick={() => handleUpgrade("Al_Coach")}
onClick={() => handleUpgrade("AI Coach")}
disabled={userLoading}
>
{userLoading ? <Loader2 className="animate-spin" /> : "Get AI Coach"}

View File

@@ -15,7 +15,7 @@ export function ProgressShare() {
const accuracy = "85"
const name = user?.name || "Student"
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
const plan = user?.tier === "Al_Coach" ? "AI Coach" : user?.tier || "Free"
const plan = user?.tier === "AI Coach" ? "AI Coach" : user?.tier || "Free"
const imageUrl = `/api/og/progress?streak=${streak}&accuracy=${accuracy}&name=${encodeURIComponent(name)}&target=${target}&plan=${encodeURIComponent(plan)}`
const handleDownload = async () => {
@@ -97,7 +97,7 @@ export function ProgressShare() {
<div className="mt-auto flex justify-between items-end w-full pt-4">
<div className="text-left">
<div className="text-sm font-bold text-white">{name}</div>
<div className="text-[10px] text-white/70 mt-0.5">{user?.tier === "Al_Coach" ? "AI Coach Plan" : `${user?.tier || "Free"} Plan`}</div>
<div className="text-[10px] text-white/70 mt-0.5">{user?.tier === "AI Coach" ? "AI Coach Plan" : `${user?.tier || "Free"} Plan`}</div>
<div className="text-[10px] text-[var(--cyan-ai)]">{user?.target_exam === "NEET" ? "NEET Aspirant" : "JEE Aspirant"}</div>
</div>
<div className="text-right">

View File

@@ -14,7 +14,7 @@ export interface UserProfile {
school: string | null;
city: string;
state: string;
tier: "Free" | "Pro" | "Al_Coach";
tier: "Free" | "Pro" | "AI Coach";
streak_count: number;
longest_streak: number;
active_session_token?: string;
@@ -48,14 +48,14 @@ export function computeTrialInfo(user: UserProfile | null, globalSettings: any):
trialDayNumber: number; // 1-based current day in trial
trialDaysRemaining: number; // days left (0 if expired)
trialTotalDays: number;
effectiveTier: "Free" | "Pro" | "Al_Coach";
effectiveTier: "Free" | "Pro" | "AI Coach";
} {
const defaultResult = {
isTrialActive: false,
trialDayNumber: 0,
trialDaysRemaining: 0,
trialTotalDays: 0,
effectiveTier: (user?.tier ?? "Free") as "Free" | "Pro" | "Al_Coach",
effectiveTier: (user?.tier ?? "Free") as "Free" | "Pro" | "AI Coach",
};
if (!user || !globalSettings) return defaultResult;
@@ -106,7 +106,7 @@ export function computeTrialInfo(user: UserProfile | null, globalSettings: any):
trialDaysRemaining,
trialTotalDays: trialDays,
effectiveTier: isTrialActive
? ((globalSettings.trial_tier as "Pro" | "Al_Coach") ?? "Al_Coach")
? ((globalSettings.trial_tier as "Pro" | "AI Coach") ?? "AI Coach")
: "Free",
};
}
@@ -311,14 +311,14 @@ export function useActiveUser() {
case "weekly-test-series":
case "full-length-mock-tests":
case "previous-year-papers":
case "detailed-performance-analytics":
case "chapter-wise-progress-tracking":
case "doubt-resolution-community":
case "parent-dashboard":
case "offline-mode":
case "pdf-export":
return tier === "Pro" || tier === "Al_Coach";
return tier === "Pro" || tier === "AI Coach";
case "detailed-performance-analytics":
case "ai-coach-custom-schedules":
case "personalized-study-plan":
case "ai-tutor-doubt-resolution":
@@ -327,7 +327,7 @@ export function useActiveUser() {
case "ai-tutor-chat":
case "college-predictor":
case "exam-day-strategy":
return tier === "Al_Coach";
return tier === "AI Coach";
default:
// Free tier features: DPS (1/day), basic dashboard, basic streak tracking, leaderboard