diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md
index e2e0690..ca15ed9 100644
--- a/API_DOCUMENTATION.md
+++ b/API_DOCUMENTATION.md
@@ -5,7 +5,8 @@
> **Last Verified:** September 2026
> **Backend Engine:** FastAPI (Python 3.11) + SQLModel (SQLAlchemy) + Celery + Redis
> **Frontend Engine:** Next.js 16 (React 19, TypeScript, TailwindCSS)
-> **Database & Storage:** PostgreSQL 15 (PgBouncer connection pooler) + MinIO (S3-compatible)
+> **Database & Storage:** PostgreSQL 15 (PgBouncer connection pooler) + MinIO (S3-compatible)
+> **Companion Document:** For page-by-page UI file locations, buttons, modals, and client API mapping, see [PAGEWISE_API_DOCUMENTATION.md](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/PAGEWISE_API_DOCUMENTATION.md).
---
diff --git a/API_DOCUMENTATION.pdf b/API_DOCUMENTATION.pdf
new file mode 100644
index 0000000..078f0e6
Binary files /dev/null and b/API_DOCUMENTATION.pdf differ
diff --git a/PAGEWISE_API_DOCUMENTATION.md b/PAGEWISE_API_DOCUMENTATION.md
new file mode 100644
index 0000000..0c63857
--- /dev/null
+++ b/PAGEWISE_API_DOCUMENTATION.md
@@ -0,0 +1,724 @@
+# PragatiVeer v2 — Pagewise UI & API Specification Map
+
+> **Document Type:** Comprehensive Pagewise Architecture, Button-to-API Interaction, and UI Specification Map
+> **Target Audience:** Frontend Engineers, Full-Stack Developers, Product Managers, QA & DevOps
+> **Last Verified:** September 2026
+> **Frontend Engine:** Next.js 16 (App Router, React 19, TypeScript, TailwindCSS, Zustand)
+> **Backend Engine:** FastAPI (Python 3.11) + PostgreSQL 15 (PgBouncer) + Redis 7 + MinIO S3
+
+---
+
+## 1. Architectural & Network Request Flow
+
+### 1.1 Client-to-Backend Network Pipeline
+PragatiVeer v2 operates two primary communication paths between the Next.js frontend and the FastAPI backend:
+
+```mermaid
+graph LR
+ Browser["Client Browser"] -->|1. Page Navigation| NextServer["Next.js 16 App Router (:3000)"]
+ NextServer -->|2. Route Guard proxy.ts| Edge["Edge Cookie Evaluation"]
+ Browser -->|3. /api/* Internal Calls| Rewrites["next.config.ts Rewrites"]
+ Rewrites -->|http://backend:8000/api/*| FastAPI["FastAPI Backend (:8000)"]
+ Browser -->|4. Direct NEXT_PUBLIC_API_URL| FastAPI
+ FastAPI -->|Queries| PgBouncer["PgBouncer :5432"]
+ PgBouncer --> DB[("PostgreSQL 15")]
+ FastAPI -->|Cache & Tasks| Redis[("Redis 7")]
+ FastAPI -->|S3 Uploads| MinIO[("MinIO S3 :9000")]
+```
+
+1. **Proxy Middleware (`src/proxy.ts`):** Evaluates `auth_token` cookie on all `/dashboard/*` and `/admin/*` routes.
+ - Non-authenticated visitors heading to `/dashboard` are redirected to `/login`.
+ - Non-admin visitors (`token !== 'admin'`) heading to `/admin` are redirected to `/login`.
+2. **Reverse Proxy Rewrites (`next.config.ts`):** Inside Docker, `/api/:path*` is automatically rewritten to `http://backend:8000/api/:path*`.
+3. **Direct Client Fetching:** Certain client stores (e.g. `cbtStore.ts` and `src/app/cbt/[examId]/page.tsx`) use `NEXT_PUBLIC_API_URL` (default `http://localhost:8000`) for high-throughput examination requests.
+4. **Session Token Enforcement:** `localStorage.getItem("active_session_token")` provides single-device session validation against `/api/users/{id}/session`.
+
+---
+
+## 2. Master Page Directory
+
+| Route URL | Component File Location | Access Level | Primary Backend APIs Called |
+| :--- | :--- | :---: | :--- |
+| `/` | `src/app/page.tsx` | Public | None (Static / Marketing) |
+| `/about` | `src/app/about/page.tsx` | Public | None (Informational) |
+| `/contact` | `src/app/contact/page.tsx` | Public | None (Static Form / Mailto) |
+| `/login` | `src/app/login/page.tsx` | Public | `POST /api/auth/login`, `POST /api/users/{id}/session` |
+| `/signup` | `src/app/signup/page.tsx` | Public | `POST /api/auth/register`, `POST /api/auth/login`, `POST /api/users/{id}/session` |
+| `/forgot-password` | `src/app/forgot-password/page.tsx` | Public | `POST /api/auth/forgot-password` |
+| `/upgrade` | `src/app/upgrade/page.tsx` | Public / Auth | None (Routing to `/checkout?tier=...`) |
+| `/checkout` | `src/app/checkout/page.tsx` | Authenticated | `POST /api/billing/create-order`, `POST /api/billing/verify-payment` |
+| `/cbt/[examId]` | `src/app/cbt/[examId]/page.tsx` | Authenticated | `GET /api/tests/{examId}`, `GET /api/tests/{examId}/questions`, `POST /api/attempts/submit` |
+| `/dashboard` | `src/app/dashboard/page.tsx` | Student (`Free`+) | `GET /api/users/{id}`, `GET /api/ai-coach/study-plan/today`, `GET /api/growth/referral/{id}` |
+| `/dashboard/cbt` | `src/app/dashboard/cbt/page.tsx` | Student (`Free`+) | `GET /api/users/{id}`, `GET /api/tests`, `GET /api/users/{id}/attempts` |
+| `/dashboard/cbt/history/[attemptId]` | `src/app/dashboard/cbt/history/[attemptId]/page.tsx` | Student (`Free`+) | `GET /api/users/{id}/attempts`, `GET /api/tests/{id}`, `GET /api/questions/{id}` |
+| `/dashboard/dpp` | `src/app/dashboard/dpp/page.tsx` | Student (`Free`+) | `GET /api/tests?type=DPS`, `GET /api/users/{id}/attempts` |
+| `/dpp/review/[dppId]` | `src/app/dpp/review/[dppId]/page.tsx` | Student (`Free`+) | Local DPP attempt state & question review |
+| `/dashboard/analytics` | `src/app/dashboard/analytics/page.tsx` | Student (`Free`+) | `GET /api/users/{id}` (Performance graphs & accuracies) |
+| `/dashboard/progress` | `src/app/dashboard/progress/page.tsx` | Student (`Free`+) | `GET /api/og/progress` (Progress card export) |
+| `/dashboard/streaks` | `src/app/dashboard/streaks/page.tsx` | Student (`Free`+) | `GET /api/users/{id}` (Streak count & freeze tokens) |
+| `/dashboard/leaderboard` | `src/app/dashboard/leaderboard/page.tsx` | Student (`Free`+) | Client ranking engine & state filtering |
+| `/dashboard/materials` | `src/app/dashboard/materials/page.tsx` | Student (`Free`+) | Formula sheets repository & PDF downloads |
+| `/dashboard/rank-predictor` | `src/app/dashboard/rank-predictor/page.tsx` | Student (`Free`+) | Percentile-to-AIR calculation engine |
+| `/dashboard/community` | `src/app/dashboard/community/page.tsx` | Student (`Free`+) | Student doubt forum & study channels |
+| `/dashboard/settings` | `src/app/dashboard/settings/page.tsx` | Student (`Free`+) | `GET /api/users/{id}`, `PUT /api/users/{id}`, `POST /api/users/{id}/avatar` |
+| `/dashboard/ai-coach` | `src/app/dashboard/ai-coach/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/readiness/{id}`, `GET /api/growth/referral/{id}` |
+| `/dashboard/ai-coach/tutor` | `src/app/dashboard/ai-coach/tutor/page.tsx` | Student (`AI_COACH`) | `POST /api/ai-coach/chat/stream` (SSE stream) |
+| `/dashboard/ai-coach/study-plan` | `src/app/dashboard/ai-coach/study-plan/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/study-plan/today?user_id={id}` |
+| `/dashboard/ai-coach/readiness` | `src/app/dashboard/ai-coach/readiness/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/readiness/{id}` |
+| `/dashboard/ai-coach/mistakes` | `src/app/dashboard/ai-coach/mistakes/page.tsx` | Student (`AI_COACH`) | `GET /api/mistakes/today?user_id={id}`, `POST /api/mistakes/review` |
+| `/dashboard/ai-coach/cbt-dpp` | `src/app/dashboard/ai-coach/cbt-dpp/page.tsx` | Student (`AI_COACH`) | `GET /api/tests?type=DPS`, `GET /api/users/{id}/attempts` |
+| `/dashboard/ai-coach/cbt-dpp/[id]` | `src/app/dashboard/ai-coach/cbt-dpp/[id]/page.tsx` | Student (`AI_COACH`) | Submits to `/cbt/[examId]` |
+| `/dashboard/ai-coach/cbt-dpp/history/[attemptId]` | `src/app/dashboard/ai-coach/cbt-dpp/history/[attemptId]/page.tsx` | Student (`AI_COACH`) | `GET /api/users/{id}/attempts`, `GET /api/tests/{id}` |
+| `/dashboard/ai-coach/heatmap` | `src/app/dashboard/ai-coach/heatmap/page.tsx` | Student (`AI_COACH`) | Mistake frequency matrix by topic & difficulty |
+| `/dashboard/ai-coach/college-predictor` | `src/app/dashboard/ai-coach/college-predictor/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/readiness/{id}` + College cutoff mapping |
+| `/dashboard/ai-coach/time-optimization` | `src/app/dashboard/ai-coach/time-optimization/page.tsx` | Student (`AI_COACH`) | Time-per-question analysis & speed pace guide |
+| `/dashboard/ai-coach/strategy` | `src/app/dashboard/ai-coach/strategy/page.tsx` | Student (`AI_COACH`) | Exam temperament & question selection strategy |
+| `/dashboard/ai-coach/weekly-progress` | `src/app/dashboard/ai-coach/weekly-progress/page.tsx` | Student (`AI_COACH`) | Weekly AI mastery trajectory |
+| `/admin` | `src/app/admin/page.tsx` | Admin | Real-time system monitoring & KPI metrics |
+| `/admin/students` | `src/app/admin/students/page.tsx` | Admin | `GET /api/admin/students` |
+| `/admin/students/[studentId]` | `src/app/admin/students/[studentId]/page.tsx` | Admin | `GET /api/admin/students/{id}`, `PATCH /api/admin/students/{id}` |
+| `/admin/tests` | `src/app/admin/tests/page.tsx` | Admin | `GET /api/admin/tests`, `PUT /api/admin/tests/{id}/toggle`, `DELETE /api/admin/tests/{id}`, `POST /api/admin/tests/create_manual` |
+| `/admin/questions` | `src/app/admin/questions/page.tsx` | Admin | Global question search & question bank table |
+| `/admin/coupons` | `src/app/admin/coupons/page.tsx` | Admin | `GET /api/admin/coupons`, `POST /api/admin/coupons`, `DELETE /api/admin/coupons/{id}` |
+| `/admin/settings` | `src/app/admin/settings/page.tsx` | Admin | `GET /api/admin/settings`, `POST /api/admin/settings` |
+| `/admin/agents` | `src/app/admin/agents/page.tsx` | Admin | AI Agent orchestration & heartbeat monitor |
+| Policy Pages | `/privacy`, `/terms`, `/refund`, `/cancellation`, `/disclaimer` | Public | Static legal disclosures |
+
+---
+
+## 3. Exhaustive Pagewise UI & API Breakdowns
+
+---
+
+### 3.1 Authentication & Onboarding Pages
+
+#### A. Login Page (`/login`)
+- **File Location:** `src/app/login/page.tsx`
+- **Access Guard:** Public (Redirects authenticated users to `/dashboard` or `/admin`)
+- **UI Components:**
+ - Brand Logo linking to `/`
+ - Email ``
+ - Password `` with show/hide password toggle button
+ - "Forgot password?" link to `/forgot-password`
+ - "Sign In" submit button
+ - "Sign up" link to `/signup`
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Show/Hide Password` | Toggles password visibility | `setShowPassword(!showPassword)` |
+ | `Sign In Button` | Submits form, authenticates user | `handleLogin(e)` |
+ | `Forgot password?` | Navigates to password reset | `/forgot-password` |
+ | `Sign up` | Navigates to registration | `/signup` |
+- **APIs Invoked:**
+ 1. `POST /api/auth/login`
+ - **Trigger:** On form submit (`handleLogin`)
+ - **Request Body:** `{ "email": "...", "password": "..." }`
+ - **Response:** `{ "success": true, "role": "user" | "admin", "token": "UUID" | "admin" }`
+ - **Side Effects:**
+ - Sets `document.cookie = "auth_token=...; path=/; max-age=2592000; SameSite=Lax"`
+ - Writes `localStorage.setItem("activeUserId", data.token)`
+ - Generates `crypto.randomUUID()` session token and writes `localStorage.setItem("active_session_token", sessionToken)`
+ 2. `POST /api/users/{userId}/session`
+ - **Trigger:** Immediately following successful student login
+ - **Request Body:** `{ "session_token": "" }`
+ - **Response:** `{ "status": "session_registered", ... }`
+ - **Redirect:** `window.location.href = "/admin"` (if admin) or `"/dashboard"` (if student)
+
+---
+
+#### B. Signup Page (`/signup`)
+- **File Location:** `src/app/signup/page.tsx`
+- **Access Guard:** Public
+- **UI Components:**
+ - Name ``
+ - Email ``
+ - Password ``
+ - "Create Account" submit button
+ - "Sign In" link to `/login`
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Create Account Button` | Submits registration | `handleSignup(e)` |
+ | `Sign in Link` | Navigates to login | `/login` |
+- **APIs Invoked:**
+ 1. `POST /api/auth/register`
+ - **Trigger:** On form submit
+ - **Request Body:** `{ "name": "...", "email": "...", "password": "..." }`
+ - **Response:** `{ "success": true, "message": "User registered successfully" }`
+ 2. `POST /api/auth/login` (Auto-login chaining)
+ - **Trigger:** Immediately upon successful registration
+ - **Request Body:** `{ "email": "...", "password": "..." }`
+ - **Response:** `{ "success": true, "token": "", "role": "user" }`
+ 3. `POST /api/users/{userId}/session`
+ - Registers active session token and redirects to `/dashboard`.
+
+---
+
+#### C. Forgot Password Page (`/forgot-password`)
+- **File Location:** `src/app/forgot-password/page.tsx`
+- **Access Guard:** Public
+- **UI Components:**
+ - Email ``
+ - "Send Reset Link" button
+ - Success message banner
+ - "Back to login" link
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Send Reset Link` | Dispatches password reset email | `handleReset(e)` |
+ | `Back to login` | Navigates to `/login` | `/login` |
+- **APIs Invoked:**
+ 1. `POST /api/auth/forgot-password`
+ - **Trigger:** On form submit
+ - **Request Body:** `{ "email": "student@example.com" }`
+ - **Response:** `{ "message": "If this email is registered, a password reset link has been sent." }`
+
+---
+
+### 3.2 Subscription & Checkout Subsystem
+
+#### A. Pricing & Upgrade Page (`/upgrade`)
+- **File Location:** `src/app/upgrade/page.tsx`
+- **Access Guard:** Public / Authenticated
+- **UI Components:**
+ - Plan selection cards:
+ - **Free Plan:** Basic mocks, limited practice sheets (Default)
+ - **Pro Plan (₹3,499/yr):** Unlimited Full-Syllabus CBT Mocks, Detailed Analytics, Daily DPS
+ - **AI Coach Plan (₹11,999/yr):** Real-time AI Tutor, College Predictor, Spaced Repetition Notebook, Adaptive Daily Study Plans
+ - Feature comparison matrix
+ - FAQ accordion
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Choose Pro` | Navigates to checkout for Pro | `router.push('/checkout?tier=Pro')` |
+ | `Upgrade to AI Coach` | Navigates to checkout for AI Coach | `router.push('/checkout?tier=Al_Coach')` |
+
+---
+
+#### B. Checkout & Payment Page (`/checkout`)
+- **File Location:** `src/app/checkout/page.tsx`
+- **Access Guard:** Authenticated (Redirects to `/login?redirect=/checkout?tier=...` if guest)
+- **UI Components:**
+ - Order summary sidebar (Plan name, subtotal, GST 18%, grand total)
+ - Delivery/Student Address inputs (`Address`, `City`, `Pincode`, `Phone`)
+ - Payment method selector: Razorpay / Cashfree / Stripe
+ - "Proceed to Payment" button with active loader
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Proceed to Payment` | Initiates backend order & triggers Razorpay Modal | `handlePayment()` |
+- **APIs Invoked:**
+ 1. `POST /api/billing/create-order?user_id={userId}&plan_tier={tier}`
+ - **Trigger:** On `Proceed to Payment` click
+ - **Request Body:** `{ "gateway": "razorpay" }`
+ - **Response:**
+ ```json
+ {
+ "id": "order_XXXXXXXXXXXXXX",
+ "amount": 1199900,
+ "currency": "INR",
+ "status": "created"
+ }
+ ```
+ 2. **Client-Side SDK:** Opens `window.Razorpay(options).open()`
+ 3. `POST /api/billing/verify-payment`
+ - **Trigger:** Razorpay `handler(response)` callback upon payment approval
+ - **Request Body:**
+ ```json
+ {
+ "gateway": "razorpay",
+ "razorpay_order_id": "order_XXXXXX",
+ "razorpay_payment_id": "pay_XXXXXX",
+ "razorpay_signature": "signature_hash",
+ "user_id": "",
+ "tier": "Al_Coach"
+ }
+ ```
+ - **Response:** `{ "status": "success", "tier": "Al_Coach" }`
+ - **Action on Success:** Updates local user tier and redirects to `/dashboard?payment=success`.
+
+---
+
+### 3.3 Computer-Based Testing (CBT) Examination Engine
+
+#### A. Full-Screen CBT Examination Room (`/cbt/[examId]`)
+- **File Location:** `src/app/cbt/[examId]/page.tsx`
+- **Access Guard:** Authenticated (`Free`, `Pro`, or `AI_COACH` depending on test gating)
+- **UI Components:**
+ - Header: Candidate Name, Photo, Roll Number, Exam Title, Section Switcher (`Physics`, `Chemistry`, `Mathematics` or `Biology`)
+ - Floating Exam Timer (Hours : Minutes : Seconds) with visual alert when under 5 minutes
+ - Question Palette Matrix (Grid 1 to N with NTA standard colors: Green=Answered, Purple=Marked for Review, Red=Unanswered, Grey=Not Visited)
+ - Question Panel: Question Text (KaTeX LaTeX rendering), Image Diagram from MinIO, Multiple Choice Options (`A`, `B`, `C`, `D`) or Numerical Input box
+ - Action Bar:
+ - "Save & Next" button
+ - "Clear Response" button
+ - "Mark for Review & Next" button
+ - "Submit Test" button
+ - Submit Confirmation Modal: Summary table of Answered, Unanswered, Marked for Review questions
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | State / Store Handler |
+ | :--- | :--- | :--- |
+ | `Option Selection (A/B/C/D)` | Selects radio answer | `saveAnswer(questionId, optionKey)` |
+ | `Numerical Input Change` | Enters integer/decimal answer | `saveAnswer(questionId, value)` |
+ | `Save & Next` | Commits answer and advances index | `handleNextQuestion()` |
+ | `Clear Response` | Resets current question answer | `clearAnswer(questionId)` |
+ | `Mark for Review & Next` | Sets flagged review state and advances | `markForReview(questionId)` |
+ | `Question Palette Button [i]` | Direct jump to question index `i` | `setCurrentIndex(i)` |
+ | `Section Tab Switcher` | Filters question palette by Subject | `setActiveSection(sectionName)` |
+ | `Submit Test Button` | Opens final confirmation dialog | `setIsSubmitModalOpen(true)` |
+ | `Confirm Final Submission` | Dispatches submission payload | `submitAttemptToServer(userId)` |
+- **APIs Invoked:**
+ 1. `GET /api/tests/{examId}?user_id={userId}`
+ - **Trigger:** On mount (`useEffect`)
+ - **Response:** Test metadata, sections mapping, total duration, question ID sequence.
+ 2. `GET /api/tests/{examId}/questions`
+ - **Trigger:** On mount immediately following test metadata
+ - **Response:** Array of full question objects (LaTeX text, options, subject, topic).
+ 3. `POST /api/attempts/submit`
+ - **Trigger:** On manual submission confirm OR auto-submit on countdown timeout
+ - **Request Body (`src/store/cbtStore.ts`):**
+ ```json
+ {
+ "test_id": "",
+ "user_id": "",
+ "time_spent": 10420,
+ "answers": [
+ {
+ "question_id": "",
+ "selected_option": "B",
+ "time_spent": 115,
+ "is_marked_for_review": false
+ }
+ ],
+ "status": "Submitted"
+ }
+ ```
+ - **Response:**
+ ```json
+ {
+ "id": "",
+ "score": 240,
+ "total_questions": 75,
+ "accuracy": 82.5,
+ "rank": 412
+ }
+ ```
+ - **Redirect:** Navigates directly to `/dashboard/cbt/history/{attemptId}`.
+
+---
+
+#### B. CBT Mock Directory Hub (`/dashboard/cbt`)
+- **File Location:** `src/app/dashboard/cbt/page.tsx`
+- **Access Guard:** Authenticated (`Free`+)
+- **UI Components:**
+ - Exam Filter Tabs: `All`, `JEE Main`, `JEE Advanced`, `NEET`
+ - Active Full Mocks Grid (Test Card: Title, Pattern badge, Total Questions, Time limit, Attempts count, "Start Test" or "Review" button)
+ - Recent Attempts History Table (Exam Title, Score, Accuracy, Date, "View Solution" button)
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Exam Filter Tabs` | Filters active tests by pattern | `setSelectedPattern(pattern)` |
+ | `Start Test Button` | Navigates into full-screen exam room | `router.push('/cbt/' + test.id)` |
+ | `View Solution Button` | Navigates to detailed attempt analysis | `router.push('/dashboard/cbt/history/' + attempt.id)` |
+- **APIs Invoked:**
+ 1. `GET /api/users/{userId}` (Fetches user's targeted exam e.g. `JEE_MAIN`)
+ 2. `GET /api/tests?user_id={userId}&exam_pattern={pattern}` (Lists available mock tests)
+ 3. `GET /api/users/{userId}/attempts` (Fetches historical attempt records)
+
+---
+
+#### C. Test Attempt Analysis & Solution Review (`/dashboard/cbt/history/[attemptId]`)
+- **File Location:** `src/app/dashboard/cbt/history/[attemptId]/page.tsx`
+- **Access Guard:** Authenticated
+- **UI Components:**
+ - Score summary card: Final Score, Total Marks, Accuracy %, Positive Marks, Negative Marks, Percentile
+ - Question-by-question breakdown list:
+ - Status badge: Correct (+4), Incorrect (-1), Unattempted (0)
+ - Full LaTeX question rendering
+ - Student's chosen answer vs Official Correct Answer
+ - Step-by-step text solution
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Filter by Subject (All/Phys/Chem/Math)` | Filters questions | `setSelectedSubject(sub)` |
+ | `Filter by Status (Incorrect/Correct)` | Isolates student errors | `setStatusFilter(filter)` |
+ | `Back to Test Directory` | Navigates back | `/dashboard/cbt` |
+- **APIs Invoked:**
+ 1. `GET /api/users/{userId}/attempts` (Locates `attemptId`)
+ 2. `GET /api/tests/{source_id}?user_id={userId}` (Fetches test structure)
+ 3. `GET /api/questions/{questionId}` (Fetches detailed solutions per question)
+
+---
+
+#### D. Daily Practice Sheets (DPS / DPP) Test Center (`/dashboard/dpp`)
+- **File Location:** `src/app/dashboard/dpp/page.tsx`
+- **Access Guard:** Authenticated (`Free`+ with tiered limits)
+- **UI Components:**
+ - Daily Sheets Calendar / Carousel:
+ - Biology Ch4 DPS
+ - Chemistry Ch4 DPS
+ - Mathematics Ch4 DPS
+ - Physics Ch4 DPS
+ - Sheet Status: `Pending` | `Completed` (with Score & Accuracy)
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Solve DPS Button` | Launches CBT engine for that practice sheet | `router.push('/cbt/' + sheet.id)` |
+ | `Review Solution Button` | Opens attempt breakdown | `router.push('/dashboard/cbt/history/' + attempt.id)` |
+- **APIs Invoked:**
+ 1. `GET /api/tests?type=DPS&exam_pattern={pattern}` (Fetches seeded DPS tests)
+ 2. `GET /api/users/{userId}/attempts` (Checks which DPS sheets have been completed)
+
+---
+
+### 3.4 Student Dashboard & Core Learning Hub
+
+#### A. Main Student Dashboard (`/dashboard`)
+- **File Location:** `src/app/dashboard/page.tsx`
+- **Access Guard:** Authenticated (`Free`+)
+- **UI Components:**
+ - Header: Welcome message, Active Target Exam (`JEE Main` / `NEET`), Streak Badge
+ - Performance Overview Grid: Tests Taken, Average Accuracy %, Study Streak, Target Score
+ - Embedded Adaptive Study Plan Card (`AdaptiveStudyPlan.tsx`)
+ - Embedded Mistake Heatmap Card (`MistakeHeatmap.tsx`)
+ - Embedded Viral Referral Card (`ReferralCard.tsx`)
+ - "Share Card" Button (Generates and downloads high-res progress PNG via `html-to-image`)
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Share Card Button` | Exports dashboard card as PNG | `handleExport()` (`html-to-image`) |
+ | `Start Today's DPS` | Launches daily practice | `/dashboard/dpp` |
+ | `Explore AI Coach` | Navigates to AI features | `/dashboard/ai-coach` |
+- **APIs Invoked:**
+ 1. `GET /api/users/{userId}` via `useActiveUser()`
+ 2. `GET /api/ai-coach/study-plan/today?user_id={userId}` (via `AdaptiveStudyPlan`)
+ 3. `GET /api/growth/referral/{userId}` (via `ReferralCard`)
+
+---
+
+#### B. Student Profile & Settings Page (`/dashboard/settings`)
+- **File Location:** `src/app/dashboard/settings/page.tsx`
+- **Access Guard:** Authenticated
+- **UI Components:**
+ - Avatar Profile Picture with MinIO upload button
+ - Personal Information Form: First Name, Last Name, Email (read-only), Target Exam selector (`JEE Main`, `JEE Advanced`, `NEET`), School, State
+ - "Save Changes" button
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Avatar File Upload Input` | Uploads image directly to MinIO | `handleAvatarUpload(e)` |
+ | `Save Changes Button` | Updates student profile | `handleSave()` |
+- **APIs Invoked:**
+ 1. `POST /api/users/{userId}/avatar`
+ - **Trigger:** On file input change
+ - **Request Form-Data:** `file: `
+ - **Response:** `{ "url": "http://localhost:9000/avatars/user_avatar_....png" }`
+ 2. `PUT /api/users/{userId}`
+ - **Trigger:** On `Save Changes` click
+ - **Request Body:**
+ ```json
+ {
+ "name": "Rahul Sharma",
+ "target_exam": "JEE_MAIN",
+ "school": "Delhi Public School",
+ "state": "Delhi"
+ }
+ ```
+ - **Response:** Updated user record.
+
+---
+
+### 3.5 AI Coach Suite (`/dashboard/ai-coach/*`)
+
+> **Access Restriction:** All pages in this section are gated to `tier == "Al_Coach"`. Users on `Free` or `Pro` tiers see an upgrade paywall with a direct link to `/checkout?tier=Al_Coach`.
+
+#### A. AI Doubt Solver & Tutor Chat (`/dashboard/ai-coach/tutor`)
+- **File Location:** `src/app/dashboard/ai-coach/tutor/page.tsx` & `src/components/AI/TutorChat.tsx`
+- **Access Guard:** `AI_COACH` Tier
+- **UI Components:**
+ - Real-time chat message history (User messages in brand cyan, AI Tutor responses formatted in Markdown + KaTeX LaTeX equations)
+ - Chat `` with send button and typing indicator
+ - Quick Suggestion Chips: *"Explain Photoelectric Effect"*, *"Solve JEE 2024 Math Integration"*, *"Give me a 3-step revision plan"*
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Send Message Button` | Submits doubt to streaming API | `sendMessage(text)` |
+ | `Suggestion Chips` | Populates input with prompt | `setInput(chipText)` |
+- **APIs Invoked:**
+ 1. `POST /api/ai-coach/chat/stream`
+ - **Trigger:** On sending a message
+ - **Request Headers:** `Content-Type: application/json`, `Accept: text/event-stream`
+ - **Request Body:** `{ "user_id": "", "message": "Explain Lenz's Law" }`
+ - **Response Stream:** `text/event-stream; charset=utf-8` (Server-Sent Events)
+ - **Phase 2 Status:** Intentionally stubbed with friendly coming-soon notice to decouple from external LLM providers during initial launch.
+
+---
+
+#### B. Spaced Repetition Mistake Notebook (`/dashboard/ai-coach/mistakes`)
+- **File Location:** `src/app/dashboard/ai-coach/mistakes/page.tsx`
+- **Access Guard:** `AI_COACH` Tier
+- **UI Components:**
+ - Daily Mistake Card Queue: Displays questions that the student answered incorrectly in previous CBT exams, scheduled for revision today using the SuperMemo-2 (SM-2) algorithm.
+ - Interactive Answer Selector (`A`, `B`, `C`, `D`)
+ - "Submit Answer" button
+ - Post-submission Feedback Panel: Immediate correctness status, explanation, and new scheduled review interval (e.g. *"Reviewed! Next review in 6 days"*).
+ - "Next Question" button
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Select Answer (A/B/C/D)` | Selects answer option | `setSelectedOption(opt)` |
+ | `Submit Answer Button` | Submits review and updates SM-2 interval | `submitReview(mistakeId, option)` |
+ | `Next Question Button` | Advances to next queued mistake | `handleNext()` |
+- **APIs Invoked:**
+ 1. `GET /api/mistakes/today?user_id={userId}`
+ - **Trigger:** On page mount
+ - **Response:** Array of pending mistake objects where `next_review <= NOW()`.
+ 2. `POST /api/mistakes/review`
+ - **Trigger:** On `Submit Answer` click
+ - **Request Body:** `{ "mistake_id": "", "selected_answer": "B" }`
+ - **Response:**
+ ```json
+ {
+ "status": "reviewed",
+ "is_correct": true,
+ "interval": 6,
+ "next_review": "2026-09-15T00:00:00Z"
+ }
+ ```
+
+---
+
+#### C. Exam Readiness & College Predictor (`/dashboard/ai-coach/readiness` & `/college-predictor`)
+- **File Location:** `src/app/dashboard/ai-coach/readiness/page.tsx`, `college-predictor/page.tsx`, `ReadinessGauge.tsx`, `CollegePredictorCard.tsx`
+- **Access Guard:** `AI_COACH` Tier
+- **UI Components:**
+ - Circular SVG Exam Readiness Gauge (0–100%)
+ - Readiness Breakdown: Accuracy Score, Time Management Score, Consistency Score, Topic Coverage
+ - AI College Predictor Card: Predicted Rank, Eligible IIT/NIT/AIIMS colleges based on historical cutoffs (Safe, Probable, Reach)
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Recalculate Score Button` | Refetches latest readiness calculation | `fetchReadiness()` |
+ | `Explore College Cutoffs` | Expands category details | `setCategoryFilter(...)` |
+- **APIs Invoked:**
+ 1. `GET /api/ai-coach/readiness/{userId}`
+ - **Trigger:** On mount
+ - **Response:**
+ ```json
+ {
+ "readiness_score": 78,
+ "predicted_rank": 3420,
+ "confidence_interval": "± 400",
+ "subject_breakdown": {
+ "Physics": 82,
+ "Chemistry": 85,
+ "Mathematics": 67
+ }
+ }
+ ```
+
+---
+
+#### D. Adaptive Daily Study Plan (`/dashboard/ai-coach/study-plan`)
+- **File Location:** `src/app/dashboard/ai-coach/study-plan/page.tsx` & `src/components/AI/StudyPlanCard.tsx`
+- **Access Guard:** `AI_COACH` Tier
+- **UI Components:**
+ - Daily Study Plan Task List: Revision Target, Practice Target, Test Recommendation
+ - Task checkboxes to mark tasks as completed
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Task Checkbox` | Marks daily task complete | Local task state |
+ | `Start Recommended Task` | Deep-links to relevant test or DPS | `/dashboard/dpp` |
+- **APIs Invoked:**
+ 1. `GET /api/ai-coach/study-plan/today?user_id={userId}`
+ - **Trigger:** On mount
+ - **Response:** Daily plan JSON generated by Celery Beat at 5:00 AM IST.
+
+---
+
+### 3.6 Admin Management Console (`/admin/*`)
+
+> **Access Restriction:** Protected by `proxy.ts` (Requires `auth_token=admin` cookie). Non-admin users are immediately redirected to `/login`.
+
+#### A. Command Center Overview (`/admin`)
+- **File Location:** `src/app/admin/page.tsx`
+- **Access Guard:** Admin Only
+- **UI Components:**
+ - KPI Stat Cards: Total Revenue, Active Students, AI Agent Accuracy %, Total Questions
+ - System Health & Accuracy Area Chart (`recharts`)
+ - Quick Action Buttons: "Refresh Data", "Create Test", "Manage Students"
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Refresh Data Button` | Triggers refetch of dashboard metrics | `fetchDashboardData()` |
+ | `Create Test Quick Link` | Navigates to Test Management | `/admin/tests` |
+
+---
+
+#### B. Student Directory & Profile Control (`/admin/students` & `[studentId]`)
+- **File Location:** `src/app/admin/students/page.tsx` and `src/app/admin/students/[studentId]/page.tsx`
+- **Access Guard:** Admin Only
+- **UI Components:**
+ - Students Directory Table: Name, Email, Target Exam, Class, Tier Badge (`Free`/`Pro`/`AI Coach`), Streak, Actions
+ - Search & Filter Bar: Filter by Exam (`JEE Main` / `NEET`) or Tier
+ - Detail Page (`[studentId]`):
+ - Full Profile Card
+ - Tier Override Selector (`Free` -> `Pro` -> `Al_Coach`)
+ - "Update Tier" button
+ - "Reset Streak" button
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `View Student Row` | Navigates to individual student control | `router.push('/admin/students/' + student.id)` |
+ | `Update Tier Button` | Changes student subscription tier | `handleTierUpdate(newTier)` |
+ | `Reset Streak Button` | Resets student streak counter to 0 | `handleResetStreak()` |
+- **APIs Invoked:**
+ 1. `GET /api/admin/students` (Lists all student accounts)
+ 2. `GET /api/admin/students/{studentId}` (Fetches single student record)
+ 3. `PATCH /api/admin/students/{studentId}`
+ - **Trigger:** On tier change or profile update
+ - **Request Body:** `{ "tier": "Al_Coach" }`
+ - **Response:** Updated student record.
+
+---
+
+#### C. Test & DPS Management Center (`/admin/tests`)
+- **File Location:** `src/app/admin/tests/page.tsx`
+- **Access Guard:** Admin Only
+- **UI Components:**
+ - Active Tests Table: Title, Exam Pattern, Total Time, Scheduled Date, Active Switch toggle, Delete button
+ - Manual Test Creation Form / JSON Drag-and-Drop Uploader:
+ - Test Title ``
+ - Exam Pattern selector (`JEE_MAIN`, `JEE_ADVANCED`, `NEET`)
+ - Test Type selector (`Mock`, `DPS`)
+ - Time Limit `` (minutes)
+ - JSON Question File Uploader (Validates question schema: `question_text`, `options`, `correct_answer`, `subject`, `topic`)
+ - "Create Test" button
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Active Toggle Switch` | Toggles test visibility to students | `handleToggle(testId)` |
+ | `Delete Test Button` | Permanently deletes test and its questions | `handleDelete(testId)` |
+ | `JSON File Drop Zone` | Parses and validates questions JSON | `processFile(file)` |
+ | `Create Test Button` | Dispatches manual test creation | `handleCreateTest()` |
+- **APIs Invoked:**
+ 1. `GET /api/admin/tests`
+ - **Trigger:** On mount
+ - **Response:** Array of all existing tests.
+ 2. `PUT /api/admin/tests/{testId}/toggle`
+ - **Trigger:** On switch toggle
+ - **Response:** `{ "status": "toggled", "is_active": true|false }`
+ 3. `DELETE /api/admin/tests/{testId}`
+ - **Trigger:** On delete confirm
+ - **Response:** `{ "status": "deleted" }`
+ 4. `POST /api/admin/tests/create_manual`
+ - **Trigger:** On `Create Test` submit
+ - **Request Body:**
+ ```json
+ {
+ "title": "JEE Main Physics Final Mock",
+ "type": "Mock",
+ "exam_pattern": "JEE_MAIN",
+ "time_limit": 180,
+ "admin_id": "",
+ "questions_json": [ ... ]
+ }
+ ```
+ - **Response:** `{ "status": "created", "test_id": "", "questions_count": 75 }`
+
+---
+
+#### D. Coupon Management (`/admin/coupons`)
+- **File Location:** `src/app/admin/coupons/page.tsx`
+- **Access Guard:** Admin Only
+- **UI Components:**
+ - Active Coupons Table: Code, Discount %, Max Discount Amount, Status, Delete button
+ - New Coupon Creation Card: Code input, Discount percentage input, Max discount input, "Create Coupon" button
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Create Coupon Button` | Submits new coupon | `handleCreate()` |
+ | `Delete Coupon Button` | Deletes discount coupon | `handleDelete(couponId)` |
+- **APIs Invoked:**
+ 1. `GET /api/admin/coupons` (Fetches active coupons list)
+ 2. `POST /api/admin/coupons`
+ - **Request Body:** `{ "code": "PRAGATI50", "discount_percent": 50, "max_discount_amount": 2000 }`
+ 3. `DELETE /api/admin/coupons/{id}` (Deletes coupon)
+
+---
+
+#### E. Global Platform Settings (`/admin/settings`)
+- **File Location:** `src/app/admin/settings/page.tsx`
+- **Access Guard:** Admin Only
+- **UI Components:**
+ - Maintenance Mode Switch
+ - Free Test Quota `` (Number of free mock tests allowed before paywall)
+ - Allow Registrations Switch
+ - "Save Settings" button
+- **Buttons & Interactive Elements:**
+ | Button / Trigger | Action | Target / Handler |
+ | :--- | :--- | :--- |
+ | `Save Settings Button` | Persists global configuration | `handleSaveSettings()` |
+- **APIs Invoked:**
+ 1. `GET /api/admin/settings` (Fetches current settings)
+ 2. `POST /api/admin/settings`
+ - **Request Body:** `{ "maintenance_mode": false, "free_test_limit": 2, "allow_registrations": true }`
+
+---
+
+## 4. Master Button-to-API Cross Reference Matrix
+
+| Button / UI Element | Page Location | Trigger Type | HTTP Method & Endpoint | Payload / Parameters |
+| :--- | :--- | :--- | :--- | :--- |
+| **Sign In** | `/login` | Form Submit | `POST /api/auth/login` | `{ email, password }` |
+| **Register & Login** | `/signup` | Form Submit | `POST /api/auth/register` | `{ name, email, password }` |
+| **Reset Password** | `/forgot-password` | Form Submit | `POST /api/auth/forgot-password` | `{ email }` |
+| **Proceed to Payment** | `/checkout` | User Click | `POST /api/billing/create-order` | Query: `?user_id=...&plan_tier=...` |
+| **Razorpay Verification** | `/checkout` | SDK Callback | `POST /api/billing/verify-payment` | `{ razorpay_order_id, razorpay_payment_id, ... }` |
+| **Start CBT Exam** | `/dashboard/cbt` | Navigation | `GET /api/tests/{examId}` | Query: `?user_id=...` |
+| **Fetch Questions** | `/cbt/[examId]` | Page Mount | `GET /api/tests/{examId}/questions` | Path: `{examId}` |
+| **Final Submit Exam** | `/cbt/[examId]` | Modal Confirm | `POST /api/attempts/submit` | `{ test_id, user_id, answers, ... }` |
+| **Submit Mistake Review** | `/dashboard/ai-coach/mistakes` | User Click | `POST /api/mistakes/review` | `{ mistake_id, selected_answer }` |
+| **Send AI Doubt** | `/dashboard/ai-coach/tutor` | Form Submit | `POST /api/ai-coach/chat/stream` | `{ user_id, message }` |
+| **Upload Avatar** | `/dashboard/settings` | File Input | `POST /api/users/{id}/avatar` | Form-Data: `file: ` |
+| **Save Profile** | `/dashboard/settings` | User Click | `PUT /api/users/{id}` | `{ name, target_exam, school, state }` |
+| **Toggle Test Status** | `/admin/tests` | Switch Toggle | `PUT /api/admin/tests/{id}/toggle` | Path: `{id}` |
+| **Delete Test** | `/admin/tests` | Button Click | `DELETE /api/admin/tests/{id}` | Path: `{id}` |
+| **Create Manual Test** | `/admin/tests` | Form Submit | `POST /api/admin/tests/create_manual` | `{ title, exam_pattern, questions_json, ... }` |
+| **Update Student Tier** | `/admin/students/[id]` | Dropdown Change | `PATCH /api/admin/students/{id}` | `{ tier: "Al_Coach" }` |
+| **Create Coupon** | `/admin/coupons` | Form Submit | `POST /api/admin/coupons` | `{ code, discount_percent, ... }` |
+| **Delete Coupon** | `/admin/coupons` | Button Click | `DELETE /api/admin/coupons/{id}` | Path: `{id}` |
+| **Save Admin Settings** | `/admin/settings` | User Click | `POST /api/admin/settings` | `{ maintenance_mode, free_test_limit, ... }` |
+
+---
+
+## 5. Codebase Cleanup Audit & Phase 2 Roadmap
+
+### 5.1 Cleaned & Removed Legacy Files
+In accordance with production standards, the following 10 dead/legacy files and temporary development artifacts have been hard-deleted:
+- `backend/fix.py`: One-time script for manual LaTeX question corrections.
+- `backend/fix_from_source.py`: One-time PDF extraction script.
+- `backend/fix_ions.py`: One-time chemical formula fixer.
+- `backend/seed_dpps.py`: Legacy DPS script using PyMuPDF and OpenAI.
+- `backend/seed_dpps_mock.py`: Obsolete mock DPS generator.
+- `backend/seed_dpps_real.py`: Obsolete duplicate DPS script.
+- `backend/seed_from_dpps.py`: Redundant seeder variant.
+- `backend/pragativeer-prd.pdf`: Product Requirements Document binary removed from backend.
+- `backend/pragativeer.db`: Abandoned SQLite file (PragatiVeer exclusively uses PostgreSQL).
+- `backend/test.pdf` & `backend/test2.pdf`: Temporary test PDFs.
+
+### 5.2 Decoupled Dependencies (`requirements.txt`)
+The following heavy external packages were removed from `backend/requirements.txt`:
+- `openai==1.14.0`: Decoupled to eliminate reliance on external LLM keys during initial rollout.
+- `PyMuPDF==1.23.26`: Decoupled to prevent native C-library compile overhead in production.
+
+### 5.3 Stubbed Modules for Phase 2 AI Integration
+- `backend/llm.py`: Cleanly stubbed with an async generator returning a friendly coming-soon notice.
+- `backend/pdf_parser.py`: Cleanly stubbed with an empty list return.
+- `backend/worker.py`: Cleaned to retain exclusively the active 5:00 AM IST Celery Beat daily study plan scheduler.
+- `POST /api/admin/tests/upload`: Returns a structured message directing administrators to use `/api/admin/tests/create_manual` until Phase 2 is provisioned.
+
+---
+
+*Document compiled and verified for PragatiVeer v2 — Production Release*
diff --git a/backend/fix.py b/backend/fix.py
deleted file mode 100644
index f5cd2ca..0000000
--- a/backend/fix.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import sys
-sys.path.append('/app')
-from sqlmodel import Session, select
-from database import engine
-import models
-import re
-
-def fix_text(text):
- if not text: return text
-
- # The DB currently has string literals with backslash caret 3 d backslash dollar.
- # Like '\\^3d\\$' and '\\^3d^2\\$'
-
- text = text.replace('\\^3d^2\\$', '$sp^3d^2$')
- text = text.replace('\\^3d\\$', '$sp^3d$')
- text = text.replace('\\^3\\$', '$sp^3$')
- text = text.replace('\\^2\\$', '$sp^2$')
-
- text = text.replace('^3d^2$', '$sp^3d^2$')
- text = text.replace('^3d$', '$sp^3d$')
- text = text.replace('^3$', '$sp^3$')
- text = text.replace('^2$', '$sp^2$')
-
- # Original targets in case we run on fresh seed
- text = re.sub(r'\bsp3d2\b', r'$sp^3d^2$', text)
- text = re.sub(r'\bsp3d\b', r'$sp^3d$', text)
- text = re.sub(r'\bsp3\b', r'$sp^3$', text)
- text = re.sub(r'\bsp2\b', r'$sp^2$', text)
-
- mols = ['PCl5', 'SF6', 'H2O', 'NH3', 'CO2', 'CH4', 'C2H4', 'C2H2', 'BF3', 'BeCl2', 'O2', 'N2', 'F2']
- for mol in mols:
- formatted = '$' + re.sub(r'(\d+)', r'_\1', mol) + '$'
- text = re.sub(r'\b' + mol + r'\b', formatted, text)
-
- text = text.replace('z1 = 1 + i', '$z_1 = 1 + i$')
- text = text.replace('z2 = 2 − i', '$z_2 = 2 - i$')
- text = text.replace('z1\nz2', '$\\frac{z_1}{z_2}$')
- text = text.replace('z−1\nz+1', '$\\frac{z-1}{z+1}$')
- text = text.replace('z2 = ¯z', '$z^2 = \\bar{z}$')
- text = text.replace('x2 − 3x + 5 = 0', '$x^2 - 3x + 5 = 0$')
- text = text.replace('x2 + 2x + 5 = 0', '$x^2 + 2x + 5 = 0$')
- text = text.replace('1+i\n1−i', '$\\frac{1+i}{1-i}$')
- text = text.replace('z + 1\nz', '$\\frac{z+1}{z}$')
- text = text.replace('z − 1\nz i', '$\\frac{z-1}{z i}$')
- text = text.replace('30◦', '30°')
- text = text.replace('⃗v = 3', '$\\vec{v} = 3$')
-
- text = text.replace('σ bonds', '$\\sigma$ bonds')
- text = text.replace('π bonds', '$\\pi$ bonds')
-
- return text
-
-with Session(engine) as session:
- questions = session.exec(select(models.Question)).all()
- count = 0
- for q in questions:
- old_text = q.question_text
- new_text = fix_text(old_text)
- if old_text != new_text:
- q.question_text = new_text
- session.add(q)
- count += 1
- print(f'Updated Q {q.id}')
- session.commit()
- print(f'Successfully updated {count} questions.')
diff --git a/backend/fix_from_source.py b/backend/fix_from_source.py
deleted file mode 100644
index 5ab706b..0000000
--- a/backend/fix_from_source.py
+++ /dev/null
@@ -1,116 +0,0 @@
-import sys
-sys.path.append('/app')
-from sqlmodel import Session, select
-from database import engine
-import models
-import glob
-import os
-import fitz
-import re
-
-def parse_pdf(filepath):
- doc = fitz.open(filepath)
- text = ""
- for page in doc:
- text += page.get_text() + "\n"
-
- q_block = text.split("Questions\n")[-1].split("Answer Key\n")[0]
- q_pattern = re.compile(r'^(\d+)\.\s+(.*?)(?=^\d+\.\s+|\Z)', re.DOTALL | re.MULTILINE)
-
- questions_dict = {}
- for match in q_pattern.finditer(q_block):
- q_num = int(match.group(1))
- if 1 <= q_num <= 15:
- questions_dict[q_num] = match.group(2).strip()
-
- answers_dict = {}
- if "Answer Key\n" in text:
- a_block = text.split("Answer Key\n")[-1]
- a_pattern = re.compile(r'^(\d+)\s+(.*?)(?=^\d+\s+|\Z)', re.DOTALL | re.MULTILINE)
- for match in a_pattern.finditer(a_block):
- q_num = int(match.group(1))
- if 1 <= q_num <= 15:
- if q_num not in answers_dict:
- answers_dict[q_num] = match.group(2).strip()
-
- return questions_dict, answers_dict
-
-def format_text(text):
- if not text: return text
-
- # Original targets
- text = re.sub(r'\bsp3d2\b', r'$sp^3d^2$', text)
- text = re.sub(r'\bsp3d\b', r'$sp^3d$', text)
- text = re.sub(r'\bsp3\b', r'$sp^3$', text)
- text = re.sub(r'\bsp2\b', r'$sp^2$', text)
-
- def repl(m):
- word = m.group(0)
- if word.lower() in ["sp2", "sp3", "sp3d", "sp3d2", "3d", "a1", "b1", "c1"]:
- return word
- return '$' + re.sub(r'(\d+)', r'_\1', word) + '$'
-
- text = re.sub(r'\b[A-Z][a-z]*[A-Z]*[a-z]*\d+[A-Za-z0-9]*\b', repl, text)
-
- text = text.replace('z1 = 1 + i', '$z_1 = 1 + i$')
- text = text.replace('z2 = 2 − i', '$z_2 = 2 - i$')
- text = text.replace('z1\nz2', '$\\frac{z_1}{z_2}$')
- text = text.replace('z−1\nz+1', '$\\frac{z-1}{z+1}$')
- text = text.replace('z2 = ¯z', '$z^2 = \\bar{z}$')
- text = text.replace('x2 − 3x + 5 = 0', '$x^2 - 3x + 5 = 0$')
- text = text.replace('x2 + 2x + 5 = 0', '$x^2 + 2x + 5 = 0$')
- text = text.replace('1+i\n1−i', '$\\frac{1+i}{1-i}$')
- text = text.replace('z + 1\nz', '$\\frac{z+1}{z}$')
- text = text.replace('z − 1\nz i', '$\\frac{z-1}{z i}$')
- text = text.replace('30◦', '30°')
- text = text.replace('⃗v = 3', '$\\vec{v} = 3$')
-
- text = text.replace('σ bonds', '$\\sigma$ bonds')
- text = text.replace('π bonds', '$\\pi$ bonds')
-
- return text
-
-def fix_all_from_source():
- pdf_files = glob.glob('/app/pdfs_to_parse/*.pdf')
- with Session(engine) as session:
- for pdf_path in pdf_files:
- filename = os.path.basename(pdf_path)
- subject = "Physics"
- topic = "General"
- if "phy" in filename.lower():
- subject = "Physics"
- topic = "Laws of Motion"
- elif "che" in filename.lower():
- subject = "Chemistry"
- topic = "Chemical Bonding"
- elif "mth" in filename.lower():
- subject = "Mathematics"
- topic = "Calculus"
- elif "bio" in filename.lower():
- subject = "Biology"
- topic = "Genetics"
-
- q_dict, a_dict = parse_pdf(pdf_path)
-
- # Find the test for this topic
- test = session.exec(select(models.Test).where(models.Test.title == f"DPP: {subject} - {topic}")).first()
- if not test:
- continue
-
- q_ids = test.sections.get("Section 1", {}).get("questions", [])
- for i, q_id in enumerate(q_ids):
- q_num = i + 1
- if q_num in q_dict:
- raw_text = q_dict[q_num].replace('\x00', '')
- formatted = format_text(raw_text)
-
- q = session.exec(select(models.Question).where(models.Question.id == q_id)).first()
- if q:
- q.question_text = formatted
- session.add(q)
-
- session.commit()
- print("Successfully updated questions from source.")
-
-if __name__ == "__main__":
- fix_all_from_source()
diff --git a/backend/fix_ions.py b/backend/fix_ions.py
deleted file mode 100644
index dd5b5d5..0000000
--- a/backend/fix_ions.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import sys
-sys.path.append('/app')
-from sqlmodel import Session, select
-from database import engine
-import models
-
-def fix_text(text):
- if not text: return text
-
- # Fix NO3- where 3 is on the next line
- text = text.replace('NO−\n3', '$NO_3^-$')
- text = text.replace('NO-\n3', '$NO_3^-$')
-
- text = text.replace('CO2−\n3', '$CO_3^{2-}$')
- text = text.replace('CO2-\n3', '$CO_3^{2-}$')
-
- text = text.replace('SO2−\n4', '$SO_4^{2-}$')
- text = text.replace('SO2-\n4', '$SO_4^{2-}$')
-
- text = text.replace('PO3−\n4', '$PO_4^{3-}$')
- text = text.replace('PO3-\n4', '$PO_4^{3-}$')
-
- # Also generic charges if they are on same line
- text = text.replace('NO3-', '$NO_3^-$')
- text = text.replace('CO32-', '$CO_3^{2-}$')
- text = text.replace('SO42-', '$SO_4^{2-}$')
- text = text.replace('PO43-', '$PO_4^{3-}$')
- text = text.replace('O2-', '$O^{2-}$')
- text = text.replace('F-', '$F^-$')
- text = text.replace('Cl-', '$Cl^-$')
- text = text.replace('Br-', '$Br^-$')
- text = text.replace('I-', '$I^-$')
- text = text.replace('Na+', '$Na^+$')
- text = text.replace('K+', '$K^+$')
- text = text.replace('Ca2+', '$Ca^{2+}$')
- text = text.replace('Mg2+', '$Mg^{2+}$')
- text = text.replace('Fe2+', '$Fe^{2+}$')
- text = text.replace('Fe3+', '$Fe^{3+}$')
- text = text.replace('Cu2+', '$Cu^{2+}$')
- text = text.replace('Al3+', '$Al^{3+}$')
-
- # Check if we have O3 (ozone) that wasn't replaced
- text = text.replace('O3', '$O_3$')
-
- return text
-
-with Session(engine) as session:
- questions = session.exec(select(models.Question)).all()
- count = 0
- for q in questions:
- old_text = q.question_text
- new_text = fix_text(old_text)
- if old_text != new_text:
- q.question_text = new_text
- session.add(q)
- count += 1
- print(f'Updated Q {q.id}')
- session.commit()
- print(f'Successfully updated {count} questions for ions.')
diff --git a/backend/llm.py b/backend/llm.py
index 330da22..1814889 100644
--- a/backend/llm.py
+++ b/backend/llm.py
@@ -1,55 +1,23 @@
-import os
-import json
-from openai import AsyncOpenAI
-from sqlmodel import Session, select
-from fastapi.responses import StreamingResponse
+"""
+llm.py — AI Tutor / LLM Streaming Interface
+STATUS: DEFERRED — Phase 2 (AI Coach LLM integration)
+
+This module will provide streaming LLM responses via the local Ollama server
+or a cloud inference endpoint. It is intentionally stubbed out in the current
+production build to avoid dependency on the openai SDK and a running LLM server.
+
+When ready to enable:
+1. Add `openai==1.14.0` back to requirements.txt
+2. Set LLM_BASE_URL, LLM_MODEL_NAME, OPENAI_API_KEY in backend/.env
+3. Replace this stub with the full implementation below.
+"""
from uuid import UUID
+from typing import AsyncIterator
-import models
-# Point the standard AsyncOpenAI client to your local Ollama server
-client = AsyncOpenAI(
- base_url=os.getenv("LLM_BASE_URL", "http://host.docker.internal:11434/v1"),
- api_key=os.getenv("OPENAI_API_KEY", "ollama"), # Required by the SDK, but Ollama ignores it
-)
-
-async def stream_tutor_response(user_id: UUID, user_message: str, session: Session):
- # 1. Fetch Context: Get the student's active weak areas
- active_mistakes = session.exec(
- select(models.Mistake)
- .where(models.Mistake.user_id == user_id)
- .where(models.Mistake.is_mastered == False)
- ).all()
-
- weak_topics = list(set([m.topic for m in active_mistakes]))
- context_string = ", ".join(weak_topics) if weak_topics else "General Syllabus"
-
- # 2. Construct the highly-specific System Prompt
- system_prompt = f"""
- You are PragatiVeer's elite AI Tutor for JEE & NEET aspirants.
- Your goal is to guide students to the answer, not just give it to them.
-
- STUDENT CONTEXT:
- This student currently struggles with the following topics: {context_string}.
- Keep this in mind and relate explanations back to foundational concepts in these areas if applicable.
-
- RULES:
- 1. Respond in the student's preferred language (English or Hindi).
- 2. Use LaTeX wrapped in single $ for inline math and $$ for block equations.
- 3. Keep responses concise and focused on problem-solving steps.
+async def stream_tutor_response(user_id: UUID, user_message: str, session) -> AsyncIterator[str]:
"""
-
- # 3. Call the LLM
- response = await client.chat.completions.create(
- model=os.getenv("LLM_MODEL_NAME", "gemma"),
- messages=[
- {"role": "system", "content": system_prompt},
- {"role": "user", "content": user_message}
- ],
- stream=True,
- )
-
- # 4. Yield tokens as they arrive
- async for chunk in response:
- if chunk.choices[0].delta.content is not None:
- yield chunk.choices[0].delta.content
+ Stub: yields a single message explaining AI is coming soon.
+ Replace with real LLM streaming call in Phase 2.
+ """
+ yield "🤖 AI Tutor is coming soon. This feature will be enabled once LLM infrastructure is provisioned."
diff --git a/backend/main.py b/backend/main.py
index 9a44c7d..8114af0 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -14,7 +14,7 @@ import models
from ai_coach import calculate_sm2
from analytics import generate_parent_report
from billing import generate_mock_order, verify_webhook_signature
-from llm import stream_tutor_response
+from llm import stream_tutor_response # Phase 2 stub — returns 'coming soon' message
from predictor import calculate_readiness_score
from growth import generate_referral_link, process_referral_reward
from email_service import send_welcome_email, send_otp_email
@@ -748,9 +748,13 @@ async def upload_admin_test(
admin_id: UUID = Form(...),
session: Session = Depends(get_session)
):
+ """
+ PDF Test Upload — Phase 2 (AI LLM integration deferred).
+ Currently: uploads PDF to MinIO and creates a placeholder Test record.
+ When Phase 2 is ready: uncomment process_pdf_test.delay() to trigger LLM question extraction.
+ """
import storage
import uuid
- from worker import process_pdf_test
pdf_bytes = await file.read()
current_year = datetime.utcnow().year
@@ -774,14 +778,20 @@ async def upload_admin_test(
session.commit()
session.refresh(test_obj)
- process_pdf_test.delay(
- test_id_str=str(test_obj.id),
- object_name=object_name,
- admin_id_str=str(admin_id),
- exam_pattern=exam_pattern
- )
+ # Phase 2: Uncomment below to trigger AI question extraction from PDF
+ # from worker import process_pdf_test
+ # process_pdf_test.delay(
+ # test_id_str=str(test_obj.id),
+ # object_name=object_name,
+ # admin_id_str=str(admin_id),
+ # exam_pattern=exam_pattern
+ # )
- return {"status": "processing", "test_id": test_obj.id, "message": "Test is being processed by AI"}
+ return {
+ "status": "uploaded",
+ "test_id": test_obj.id,
+ "message": "PDF uploaded to MinIO. AI question extraction is deferred to Phase 2. Use create_manual endpoint to add questions now."
+ }
@app.post("/api/admin/tests/create_manual", tags=["Admin"])
def create_manual_test(
diff --git a/backend/pdf_parser.py b/backend/pdf_parser.py
index 197d0f8..f8e36d2 100644
--- a/backend/pdf_parser.py
+++ b/backend/pdf_parser.py
@@ -1,85 +1,21 @@
-import fitz # PyMuPDF
-import json
-import logging
-import asyncio
+"""
+pdf_parser.py — PDF-to-Question AI Parser
+STATUS: DEFERRED — Phase 2 (AI Coach LLM integration)
+
+This module will use PyMuPDF to extract text from uploaded PDFs and pass it
+to an LLM to structure questions into the database JSON schema.
+
+When ready to enable:
+1. Add `PyMuPDF==1.23.26` and `openai==1.14.0` back to requirements.txt
+2. Set LLM_BASE_URL, LLM_MODEL_NAME, OPENAI_API_KEY in backend/.env
+3. Replace this stub with the full implementation.
+"""
from typing import List, Dict, Any
-import os
-from openai import AsyncOpenAI
-logger = logging.getLogger("pdf_parser")
-
-client = AsyncOpenAI(
- base_url=os.getenv("LLM_BASE_URL", "http://host.docker.internal:11434/v1"),
- api_key=os.getenv("OPENAI_API_KEY", "ollama"),
-)
async def parse_pdf_to_questions(pdf_bytes: bytes, exam_pattern: str) -> List[Dict[str, Any]]:
"""
- Extracts text from a PDF and uses the LLM to structure it into a JSON array of questions.
+ Stub: returns empty list. Real implementation uses PyMuPDF + LLM JSON extraction.
+ Replace in Phase 2 once LLM infrastructure is provisioned.
"""
- text = ""
- try:
- doc = fitz.open(stream=pdf_bytes, filetype="pdf")
- for page in doc:
- text += page.get_text("text") + "\n\n"
- except Exception as e:
- logger.error(f"Failed to parse PDF with PyMuPDF: {e}")
- return []
-
- system_prompt = f"""
- You are an AI expert at extracting {exam_pattern} test questions from messy PDF text.
- The text contains complex Chemistry and Math formulas.
- CRITICAL: You MUST format all formulas perfectly in LaTeX wrapping inline math in $ and block math in $$.
-
- Extract the questions and return them STRICTLY as a JSON array of objects.
- If your API requires a JSON object root, return {{"questions": [...]}}.
-
- Schema per question:
- {{
- "subject": "Physics" | "Chemistry" | "Maths" | "Biology",
- "topic": "String",
- "subtopic": "String",
- "difficulty": "Easy" | "Medium" | "Hard",
- "type": "MCQ_Single" | "MCQ_Multiple" | "Numerical",
- "question_text": "Full text of the question with LaTeX",
- "options": {{"A": "Option A text", "B": "Option B text", "C": "Option C text", "D": "Option D text"}},
- "correct_answer": {{"value": "A"}},
- "solution_text": "Detailed step-by-step solution if available, else 'Solution not provided'",
- "language": "English"
- }}
- """
-
- try:
- truncated_text = text[:15000]
-
- response = await client.chat.completions.create(
- model=os.getenv("LLM_MODEL_NAME", "gemma"),
- messages=[
- {"role": "system", "content": system_prompt},
- {"role": "user", "content": truncated_text}
- ],
- response_format={ "type": "json_object" }
- )
-
- output = response.choices[0].message.content
- if not output:
- return []
-
- if "```json" in output:
- output = output.split("```json")[1].split("```")[0].strip()
- elif "```" in output:
- output = output.split("```")[1].split("```")[0].strip()
-
- data = json.loads(output)
-
- if isinstance(data, list):
- return data
- elif isinstance(data, dict) and "questions" in data:
- return data["questions"]
- elif isinstance(data, dict):
- return [data]
- else:
- return []
- except Exception as e:
- logger.error(f"LLM PDF parsing failed: {e}")
- return []
+ return []
diff --git a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1782971559253.pdf b/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1782971559253.pdf
deleted file mode 100644
index 756db6b..0000000
Binary files a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1782971559253.pdf and /dev/null differ
diff --git a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1782979477911.pdf b/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1782979477911.pdf
deleted file mode 100644
index 756db6b..0000000
Binary files a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1782979477911.pdf and /dev/null differ
diff --git a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1783273583537.pdf b/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1783273583537.pdf
deleted file mode 100644
index 756db6b..0000000
Binary files a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1783273583537.pdf and /dev/null differ
diff --git a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1783410386530.pdf b/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1783410386530.pdf
deleted file mode 100644
index 756db6b..0000000
Binary files a/backend/pdfs/media_e3dd3511-9fc4-402b-8054-6a56f7ed8202_1783410386530.pdf and /dev/null differ
diff --git a/backend/pragativeer-prd.pdf b/backend/pragativeer-prd.pdf
deleted file mode 100644
index 756db6b..0000000
Binary files a/backend/pragativeer-prd.pdf and /dev/null differ
diff --git a/backend/pragativeer.db b/backend/pragativeer.db
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 506bf00..d3ca1b6 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -8,8 +8,6 @@ pydantic-settings==2.2.1
bcrypt==4.1.2
pyjwt==2.8.0
celery==5.3.6
-openai==1.14.0
httpx==0.27.2
boto3==1.34.0
python-multipart==0.0.9
-PyMuPDF==1.23.26
diff --git a/backend/seed.py b/backend/seed.py
index f9965c3..0cf50f3 100644
--- a/backend/seed.py
+++ b/backend/seed.py
@@ -412,8 +412,9 @@ def seed_local_database():
dps_session.add(q)
q_ids.append(str(q.id))
+ dps_id = uuid4()
dps = models.DPS(
- id=uuid4(),
+ id=dps_id,
date=sheet_date,
title=sheet_title,
questions=q_ids,
@@ -423,7 +424,22 @@ def seed_local_database():
created_by=admin_id,
)
dps_session.add(dps)
- print(f" ✓ DPS seeded: {sheet_title} ({len(q_ids)} questions)")
+
+ # Also create as Test(type='DPS') so /api/tests?type=DPS and CBT engine can run it
+ dps_test = models.Test(
+ id=dps_id,
+ title=sheet_title,
+ type="DPS",
+ exam_pattern=models.ExamEnum.JEE_MAIN,
+ sections={"Practice": {"questions": q_ids, "time_limit": 30}},
+ total_time=30,
+ difficulty="Medium",
+ scheduled_date=datetime.combine(sheet_date, datetime.min.time()),
+ is_active=True,
+ created_by=admin_id,
+ )
+ dps_session.add(dps_test)
+ print(f" ✓ DPS & CBT Test seeded: {sheet_title} ({len(q_ids)} questions)")
dps_session.commit()
diff --git a/backend/seed_dpps.py b/backend/seed_dpps.py
deleted file mode 100644
index a1cd902..0000000
--- a/backend/seed_dpps.py
+++ /dev/null
@@ -1,148 +0,0 @@
-import asyncio
-import os
-import uuid
-from sqlmodel import Session, select, delete
-from datetime import datetime
-
-from database import engine, init_db
-import models
-import storage
-from pdf_parser import parse_pdf_to_questions
-
-async def seed_pdfs():
- admin_id_str = "b0e17378-864d-439e-8806-708dc48a891b"
-
- with Session(engine) as session:
- admin = session.exec(select(models.User).limit(1)).first()
- admin_id = admin.id if admin else uuid.UUID(admin_id_str)
-
- # Delete previously seeded AI DPPs to prevent duplicates
- statement = select(models.Test).where(models.Test.title.like("AI DPP:%"))
- old_tests = session.exec(statement).all()
- for t in old_tests:
- session.delete(t)
- session.commit()
-
- # Realistic Mock Data
- mock_data = [
- {
- "topic": "Rotational Dynamics",
- "subject": "Physics",
- "difficulty": "Hard",
- "questions": [
- {
- "question_text": r"A solid sphere of mass $M$ and radius $R$ is rolling without slipping on a horizontal surface. Its center of mass is moving with velocity $v$. What is its total kinetic energy?",
- "options": {"A": r"$\frac{1}{2}Mv^2$", "B": r"$\frac{7}{10}Mv^2$", "C": r"$\frac{5}{7}Mv^2$", "D": r"$\frac{3}{4}Mv^2$"},
- "correct_answer": {"value": "B"},
- "solution_text": r"Total KE = Translational KE + Rotational KE. $KE_{tot} = \frac{1}{2}Mv^2 + \frac{1}{2}I\omega^2$. For solid sphere, $I = \frac{2}{5}MR^2$, and since it rolls without slipping, $\omega = v/R$. Thus, $KE_{tot} = \frac{1}{2}Mv^2 + \frac{1}{2}(\frac{2}{5}MR^2)(v/R)^2 = \frac{7}{10}Mv^2$."
- },
- {
- "question_text": r"A thin uniform rod of length $l$ and mass $m$ is suspended freely from one end. It is pulled aside through an angle $\theta$ and released. What is the angular velocity when it passes through the lowest point?",
- "options": {"A": r"$\sqrt{\frac{3g}{l}(1-\cos\theta)}$", "B": r"$\sqrt{\frac{g}{l}(1-\cos\theta)}$", "C": r"$\sqrt{\frac{2g}{l}(1-\cos\theta)}$", "D": r"$\sqrt{\frac{6g}{l}(1-\cos\theta)}$"},
- "correct_answer": {"value": "A"},
- "solution_text": r"By conservation of energy: $mgh_{cm} = \frac{1}{2}I\omega^2$. $h_{cm} = \frac{l}{2}(1-\cos\theta)$. $I = \frac{ml^2}{3}$. Therefore, $mg\frac{l}{2}(1-\cos\theta) = \frac{1}{2}(\frac{ml^2}{3})\omega^2 \implies \omega = \sqrt{\frac{3g}{l}(1-\cos\theta)}$."
- }
- ]
- },
- {
- "topic": "Chemical Kinetics",
- "subject": "Chemistry",
- "difficulty": "Medium",
- "questions": [
- {
- "question_text": r"For a first-order reaction $A \rightarrow P$, the rate constant is $k = 2.303 \times 10^{-3} s^{-1}$. What is the half-life ($t_{1/2}$) of this reaction?",
- "options": {"A": r"$300 s$", "B": r"$301 s$", "C": r"$69.3 s$", "D": r"$0.693 s$"},
- "correct_answer": {"value": "B"},
- "solution_text": r"For first-order reaction, $t_{1/2} = \frac{0.693}{k}$. Substituting $k$, $t_{1/2} = \frac{0.693}{2.303 \times 10^{-3}} \approx 301 s$."
- },
- {
- "question_text": r"If the rate of a reaction increases by a factor of $8$ when the concentration of reactant $X$ is doubled, what is the order of the reaction with respect to $X$?",
- "options": {"A": "1", "B": "2", "C": "3", "D": "0"},
- "correct_answer": {"value": "C"},
- "solution_text": r"Rate $R \propto [X]^n$. If $[X]$ is doubled, new rate $R' = 2^n \cdot R$. Given $R' = 8R$, so $2^n = 8 \implies n = 3$."
- }
- ]
- },
- {
- "topic": "Genetics",
- "subject": "Biology",
- "difficulty": "Easy",
- "questions": [
- {
- "question_text": r"In a Mendelian dihybrid cross, what is the phenotypic ratio of the F2 generation?",
- "options": {"A": "9:3:3:1", "B": "1:2:1", "C": "3:1", "D": "1:1:1:1"},
- "correct_answer": {"value": "A"},
- "solution_text": r"A standard dihybrid cross between two heterozygotes ($AaBb \times AaBb$) yields offspring with phenotypes in a 9:3:3:1 ratio."
- },
- {
- "question_text": r"Which of the following describes the condition where both alleles of a gene pair in a heterozygote are fully expressed?",
- "options": {"A": "Incomplete Dominance", "B": "Codominance", "C": "Pleiotropy", "D": "Epistasis"},
- "correct_answer": {"value": "B"},
- "solution_text": r"Codominance is the phenomenon where both alleles are simultaneously expressed in the heterozygote, such as in AB blood type."
- }
- ]
- },
- {
- "topic": "Calculus",
- "subject": "Mathematics",
- "difficulty": "Hard",
- "questions": [
- {
- "question_text": r"Evaluate the integral $\int_0^{\pi/2} \frac{\sqrt{\sin x}}{\sqrt{\sin x} + \sqrt{\cos x}} dx$.",
- "options": {"A": r"$\pi/4$", "B": r"$\pi/2$", "C": r"$\pi$", "D": r"$0$"},
- "correct_answer": {"value": "A"},
- "solution_text": r"Let $I = \int_0^{\pi/2} \frac{\sqrt{\sin x}}{\sqrt{\sin x} + \sqrt{\cos x}} dx$. Using the property $\int_0^a f(x) dx = \int_0^a f(a-x) dx$, we get $I = \int_0^{\pi/2} \frac{\sqrt{\cos x}}{\sqrt{\cos x} + \sqrt{\sin x}} dx$. Adding both, $2I = \int_0^{\pi/2} 1 dx = \pi/2 \implies I = \pi/4$."
- },
- {
- "question_text": r"Find the limit $\lim_{x \to 0} \frac{e^x - 1 - x}{x^2}$.",
- "options": {"A": "1", "B": r"$1/2$", "C": "0", "D": "Does not exist"},
- "correct_answer": {"value": "B"},
- "solution_text": r"Using L'Hôpital's Rule twice: $\lim_{x \to 0} \frac{e^x - 1}{2x} = \lim_{x \to 0} \frac{e^x}{2} = \frac{1}{2}$."
- }
- ]
- }
- ]
-
- for idx, mock in enumerate(mock_data):
- question_ids = []
- for q_data in mock["questions"]:
- new_q = models.Question(
- subject=mock["subject"],
- topic=mock["topic"],
- subtopic="General",
- difficulty=mock["difficulty"],
- type=models.QuestionTypeEnum.MCQ_SINGLE,
- question_text=q_data["question_text"],
- options=q_data["options"],
- correct_answer=q_data["correct_answer"],
- solution_text=q_data["solution_text"],
- language=models.MediumEnum.ENGLISH,
- exam=models.ExamEnum.JEE_MAIN,
- created_by=admin_id
- )
- session.add(new_q)
- session.flush()
- question_ids.append(str(new_q.id))
-
- test_obj = models.Test(
- title=f"AI DPP: {mock['topic']}",
- type="DPS",
- exam_pattern=models.ExamEnum.JEE_MAIN,
- sections={
- "Section 1": {
- "questions": question_ids,
- "time_limit": 60
- }
- },
- total_time=60,
- difficulty=mock["difficulty"],
- scheduled_date=datetime.utcnow(),
- is_active=True,
- created_by=admin_id
- )
- session.add(test_obj)
- session.commit()
- print(f"Successfully created DPS Test: {test_obj.title} with {len(question_ids)} questions.")
-
-if __name__ == "__main__":
- asyncio.run(seed_pdfs())
diff --git a/backend/seed_dpps_mock.py b/backend/seed_dpps_mock.py
deleted file mode 100644
index 54fe129..0000000
--- a/backend/seed_dpps_mock.py
+++ /dev/null
@@ -1,83 +0,0 @@
-import asyncio
-import os
-import uuid
-import random
-from sqlmodel import Session, select, delete
-from datetime import datetime
-
-from database import engine
-import models
-
-def seed_tests():
- admin_id_str = "b0e17378-864d-439e-8806-708dc48a891b"
-
- with Session(engine) as session:
- admin = session.exec(select(models.User).limit(1)).first()
- admin_id = admin.id if admin else uuid.UUID(admin_id_str)
-
- # Clear existing DPS tests
- statement = select(models.Test).where(models.Test.type == "DPS")
- old_tests = session.exec(statement).all()
- for t in old_tests:
- session.delete(t)
- session.commit()
-
- subjects = [
- ("Physics", "Laws of Motion", "dps-phy-ch4-20260707.pdf"),
- ("Chemistry", "Chemical Thermodynamics", "dps-che-ch4-20260707.pdf"),
- ("Mathematics", "Calculus", "dps-mth-ch4-20260707.pdf"),
- ("Biology", "Genetics", "dps-bio-ch4-20260707.pdf")
- ]
-
- for exam in [models.ExamEnum.JEE_MAIN, models.ExamEnum.JEE_ADVANCED, models.ExamEnum.NEET]:
- for subject, topic, pdf in subjects:
- # Math for NEET? Biology for JEE? Let's just create them anyway so the UI is populated
- question_ids = []
- for i in range(1, 16):
- new_q = models.Question(
- subject=subject,
- topic=topic,
- subtopic="General",
- difficulty="Medium",
- type=models.QuestionTypeEnum.MCQ_SINGLE,
- question_text=f"Sample mock question {i} for {subject} - {topic}",
- options={
- "A": "Option A",
- "B": "Option B",
- "C": "Option C",
- "D": "Option D"
- },
- correct_answer={"value": "A"},
- solution_text="Solution provided here.",
- language=models.MediumEnum.ENGLISH,
- exam=exam,
- created_by=admin_id
- )
- session.add(new_q)
- session.flush()
- question_ids.append(str(new_q.id))
-
- test_obj = models.Test(
- title=f"{subject} Chapter 4 DPS ({exam})",
- type="DPS",
- exam_pattern=exam,
- sections={
- "Section 1": {
- "questions": question_ids,
- "time_limit": 30
- }
- },
- total_time=30,
- difficulty="Medium",
- scheduled_date=datetime.utcnow(),
- is_active=True,
- created_by=admin_id
- )
- session.add(test_obj)
- print(f"Created Test: {test_obj.title}")
-
- session.commit()
- print("Database seeded with mock CBT DPPs for all exams.")
-
-if __name__ == "__main__":
- seed_tests()
diff --git a/backend/seed_dpps_real.py b/backend/seed_dpps_real.py
deleted file mode 100644
index 45d2d42..0000000
--- a/backend/seed_dpps_real.py
+++ /dev/null
@@ -1,450 +0,0 @@
-"""
-Real DPP Question Seeder - Seeds Physics, Chemistry, Math, Biology DPPs
-with actual JEE/NEET standard questions as type=DPS (PrepPilot AI)
-and a separate Mock CBT test as type=CBT (Mock Tests section)
-"""
-import uuid
-from sqlmodel import Session, select
-from datetime import datetime
-from database import engine
-import models
-
-PHYSICS_QUESTIONS = [
- {
- "text": "A body of mass 5 kg is acted upon by two perpendicular forces 8 N and 6 N. The magnitude and direction of the resultant with respect to the 6 N force is:",
- "options": {"A": "10 N at 53°", "B": "10 N at 37°", "C": "8 N at 45°", "D": "14 N at 0°"},
- "answer": "A",
- "solution": "Resultant R = √(8² + 6²) = √(64+36) = √100 = 10 N. tan θ = 8/6 = 4/3 → θ = 53° from the 6N force."
- },
- {
- "text": "A ball is thrown vertically upward with velocity 20 m/s. The maximum height reached is: (g = 10 m/s²)",
- "options": {"A": "10 m", "B": "20 m", "C": "40 m", "D": "5 m"},
- "answer": "B",
- "solution": "Using v² = u² - 2gh. At max height v=0. h = u²/2g = (20)²/(2×10) = 400/20 = 20 m."
- },
- {
- "text": "The ratio of radii of gyration of a circular disc to that of a circular ring, each of the same mass and radius, about their axes passing through their centres and perpendicular to their planes is:",
- "options": {"A": "√2 : 1", "B": "1 : √2", "C": "1 : 2", "D": "2 : 1"},
- "answer": "B",
- "solution": "I_disc = MR²/2, I_ring = MR². k_disc = R/√2, k_ring = R. Ratio = (1/√2) : 1 = 1 : √2."
- },
- {
- "text": "Two charges of +4μC and -4μC are placed at (3, 0) and (-3, 0) cm. The electric field at origin due to this dipole is:",
- "options": {"A": "4 × 10⁸ N/C along +x", "B": "4 × 10⁸ N/C along -x", "C": "8 × 10⁸ N/C along +x", "D": "Zero"},
- "answer": "A",
- "solution": "Each charge contributes E = kq/r² = (9×10⁹ × 4×10⁻⁶)/(0.03)² = 4×10⁷ N/C. Both fields point in +x direction. Total = 8×10⁷ N/C along +x."
- },
- {
- "text": "A wire of length L and resistance R is uniformly stretched to double its length. New resistance is:",
- "options": {"A": "R/2", "B": "R", "C": "2R", "D": "4R"},
- "answer": "D",
- "solution": "ρ is constant. Volume = AL = constant. New length L' = 2L → new area A' = A/2. R' = ρ(2L)/(A/2) = 4ρL/A = 4R."
- },
- {
- "text": "In Young's double slit experiment, the slits are 0.5 mm apart and interference fringes are observed on a screen 1.0 m away. It is found that the 9th bright fringe is at a distance of 8.835 mm from the 2nd dark fringe. Find the wavelength of light:",
- "options": {"A": "600 nm", "B": "589 nm", "C": "550 nm", "D": "500 nm"},
- "answer": "B",
- "solution": "Distance = (9+0.5-2+0.5)β = 8β = 8 × λD/d. 8.835 mm = 8λ(1)/(0.5×10⁻³). λ = 8.835×10⁻³ × 0.5×10⁻³/8 = 589 nm."
- },
- {
- "text": "The half life of a radioactive element is 3 hours. After 12 hours the fraction of initial amount remaining will be:",
- "options": {"A": "1/16", "B": "1/8", "C": "1/4", "D": "1/2"},
- "answer": "A",
- "solution": "Number of half-lives = 12/3 = 4. Fraction remaining = (1/2)^4 = 1/16."
- },
- {
- "text": "A solid sphere of radius R has a charge Q uniformly distributed over its volume. The magnitude of electric field at distance r from the center (r < R) is:",
- "options": {"A": "Q/(4πε₀R²)", "B": "Qr/(4πε₀R³)", "C": "Q/(4πε₀r²)", "D": "Zero"},
- "answer": "B",
- "solution": "By Gauss's law, E(4πr²) = Q_enc/ε₀ = (Q/R³)r³/ε₀. E = Qr/(4πε₀R³)."
- },
- {
- "text": "The kinetic energy of a satellite orbiting at height h above Earth's surface is (mass of satellite m, radius R, mass of Earth M):",
- "options": {"A": "GMm/(2(R+h))", "B": "GMm/(R+h)", "C": "-GMm/(2(R+h))", "D": "GMm/R"},
- "answer": "A",
- "solution": "For circular orbit: mv²/(R+h) = GMm/(R+h)². KE = mv²/2 = GMm/(2(R+h))."
- },
- {
- "text": "A particle executes SHM with amplitude A. The displacement at which kinetic energy equals potential energy is:",
- "options": {"A": "A/2", "B": "A/√2", "C": "A√2", "D": "2A"},
- "answer": "B",
- "solution": "KE = PE → (1/2)mω²(A²-x²) = (1/2)mω²x². A²-x² = x². x² = A²/2. x = A/√2."
- },
- {
- "text": "In Compton scattering, the change in wavelength is maximum when the photon is scattered through angle:",
- "options": {"A": "0°", "B": "45°", "C": "90°", "D": "180°"},
- "answer": "D",
- "solution": "Compton shift Δλ = (h/mₑc)(1-cosθ). Maximum when cosθ = -1 i.e., θ = 180°. Max Δλ = 2h/mₑc = 0.0485 Å."
- },
- {
- "text": "A capacitor of capacitance C is connected to a battery of EMF E through a resistance R. The time constant of the circuit is:",
- "options": {"A": "RC", "B": "R/C", "C": "C/R", "D": "1/RC"},
- "answer": "A",
- "solution": "The time constant τ = RC for a series RC circuit. It represents the time for the capacitor to charge to 63.2% of the final value."
- },
- {
- "text": "Which electromagnetic radiation has the highest penetrating power?",
- "options": {"A": "UV rays", "B": "X-rays", "C": "Gamma rays", "D": "Infrared rays"},
- "answer": "C",
- "solution": "Gamma rays have the highest penetrating power due to their extremely short wavelength and high energy. They can penetrate through several cm of lead."
- },
- {
- "text": "A bar magnet of magnetic moment M is placed in a uniform magnetic field B. The torque on the magnet when it makes angle 30° with the field is:",
- "options": {"A": "MB/2", "B": "MB√3/2", "C": "MB", "D": "MB√3"},
- "answer": "A",
- "solution": "τ = MBsinθ = MBsin30° = MB × (1/2) = MB/2."
- },
- {
- "text": "The de Broglie wavelength associated with a proton moving with velocity v is λ. For an alpha particle moving with the same speed, the de Broglie wavelength will be:",
- "options": {"A": "λ/4", "B": "λ/2", "C": "2λ", "D": "4λ"},
- "answer": "A",
- "solution": "λ = h/mv. α-particle mass = 4mₚ. λ_α = h/(4mₚv) = λ_p/4."
- },
-]
-
-CHEMISTRY_QUESTIONS = [
- {
- "text": "The hybridization of carbon in diamond and graphite respectively is:",
- "options": {"A": "sp², sp³", "B": "sp³, sp²", "C": "sp, sp²", "D": "sp³, sp"},
- "answer": "B",
- "solution": "Diamond: each C is bonded to 4 others in tetrahedral arrangement → sp³. Graphite: each C is bonded to 3 others in trigonal planar → sp²."
- },
- {
- "text": "The equilibrium constant Kp for the reaction N₂(g) + 3H₂(g) ⇌ 2NH₃(g) is related to Kc by:",
- "options": {"A": "Kp = Kc(RT)²", "B": "Kp = Kc(RT)⁻²", "C": "Kp = Kc(RT)²", "D": "Kp = Kc"},
- "answer": "B",
- "solution": "Δn = 2 - (1+3) = -2. Kp = Kc(RT)^Δn = Kc(RT)^(-2)."
- },
- {
- "text": "The IUPAC name of the compound CH₃-CH(Cl)-CH₂-CH₃ is:",
- "options": {"A": "1-chlorobutane", "B": "2-chlorobutane", "C": "3-chlorobutane", "D": "Sec-butyl chloride"},
- "answer": "B",
- "solution": "The longest chain has 4 carbons (butane). Cl is on carbon 2. IUPAC name: 2-chlorobutane."
- },
- {
- "text": "Which of the following has the highest lattice energy?",
- "options": {"A": "NaCl", "B": "KCl", "C": "MgO", "D": "NaF"},
- "answer": "C",
- "solution": "Lattice energy ∝ (charge of ions)²/distance. MgO has doubly charged ions (Mg²⁺ and O²⁻) giving it much higher lattice energy than singly-charged ion compounds."
- },
- {
- "text": "The pH of 0.001 M H₂SO₄ solution is:",
- "options": {"A": "3", "B": "2.7", "C": "2", "D": "1"},
- "answer": "B",
- "solution": "H₂SO₄ is a strong diprotic acid. [H⁺] = 2 × 0.001 = 0.002 M. pH = -log(0.002) = -log(2×10⁻³) = 3 - log2 = 3 - 0.301 ≈ 2.7."
- },
- {
- "text": "Which of the following undergoes SN1 reaction most readily?",
- "options": {"A": "CH₃Cl", "B": "CH₃CH₂Cl", "C": "(CH₃)₂CHCl", "D": "(CH₃)₃CCl"},
- "answer": "D",
- "solution": "SN1 rate depends on carbocation stability. Tertiary carbocations are most stable due to hyperconjugation and inductive effect of alkyl groups. (CH₃)₃C⁺ is tertiary."
- },
- {
- "text": "The oxidation state of Cr in K₂Cr₂O₇ is:",
- "options": {"A": "+3", "B": "+4", "C": "+6", "D": "+7"},
- "answer": "C",
- "solution": "Let Cr be x. 2(+1) + 2x + 7(-2) = 0 → 2 + 2x - 14 = 0 → 2x = 12 → x = +6."
- },
- {
- "text": "Which quantum number determines the orientation of an orbital in space?",
- "options": {"A": "Principal (n)", "B": "Azimuthal (l)", "C": "Magnetic (mₗ)", "D": "Spin (mₛ)"},
- "answer": "C",
- "solution": "The magnetic quantum number (mₗ) determines the orientation of the orbital in three-dimensional space, ranging from -l to +l."
- },
- {
- "text": "The correct order of boiling points of hydrides of group 16 elements is:",
- "options": {"A": "H₂O > H₂S > H₂Se > H₂Te", "B": "H₂Te > H₂Se > H₂S > H₂O", "C": "H₂O > H₂Te > H₂Se > H₂S", "D": "H₂S > H₂O > H₂Se > H₂Te"},
- "answer": "C",
- "solution": "H₂O has highest BP due to hydrogen bonding. Among H₂S, H₂Se, H₂Te: BP increases with molar mass (van der Waals forces). So H₂O > H₂Te > H₂Se > H₂S."
- },
- {
- "text": "Identify the product formed when acetaldehyde reacts with excess HCN followed by hydrolysis:",
- "options": {"A": "Acetic acid", "B": "Lactic acid", "C": "Pyruvic acid", "D": "Glycolic acid"},
- "answer": "B",
- "solution": "CH₃CHO + HCN → CH₃CH(OH)CN (cyanohydrin). On hydrolysis: CH₃CH(OH)COOH = lactic acid (2-hydroxypropanoic acid)."
- },
- {
- "text": "The van't Hoff factor 'i' for a 0.01 M solution of K₂SO₄ that is 50% dissociated is:",
- "options": {"A": "1.5", "B": "2.0", "C": "2.5", "D": "3.0"},
- "answer": "B",
- "solution": "K₂SO₄ → 2K⁺ + SO₄²⁻ (3 ions). For 50% dissociation: i = 1 + α(n-1) = 1 + 0.5(3-1) = 1 + 1 = 2.0."
- },
- {
- "text": "Which of the following is NOT a property of transition metals?",
- "options": {"A": "They show variable oxidation states", "B": "They form colored compounds", "C": "They have low melting points", "D": "They act as catalysts"},
- "answer": "C",
- "solution": "Transition metals generally have high melting points due to strong metallic bonds involving d electrons. Options A, B, and D are characteristic properties of transition metals."
- },
- {
- "text": "The reaction of glucose with excess acetic anhydride gives:",
- "options": {"A": "Glucose diacetate", "B": "Glucose triacetate", "C": "Glucose pentaacetate", "D": "Glucose hexaacetate"},
- "answer": "C",
- "solution": "Glucose has 5 -OH groups (1 anomeric + 4 secondary). All react with acetic anhydride giving glucose pentaacetate."
- },
- {
- "text": "The number of σ and π bonds in benzene molecule are:",
- "options": {"A": "6σ, 3π", "B": "12σ, 3π", "C": "6σ, 6π", "D": "9σ, 3π"},
- "answer": "B",
- "solution": "C₆H₆: 6 C-C bonds (σ) + 6 C-H bonds (σ) = 12 σ bonds. 3 π bonds in alternate double bonds. Total: 12σ and 3π."
- },
- {
- "text": "Le Chatelier's principle predicts that increasing pressure will favor formation of products in:",
- "options": {"A": "N₂ + 3H₂ ⇌ 2NH₃", "B": "2SO₃ ⇌ 2SO₂ + O₂", "C": "H₂ + I₂ ⇌ 2HI", "D": "N₂ + O₂ ⇌ 2NO"},
- "answer": "A",
- "solution": "Increasing pressure favors the reaction with fewer moles of gas. N₂+3H₂→2NH₃: reactants have 4 mol gas, products have 2. Pressure increase shifts equilibrium right."
- },
-]
-
-MATH_QUESTIONS = [
- {
- "text": "If f(x) = x² - 3x + 2, then f(f(x)) = 0 has how many solutions?",
- "options": {"A": "2", "B": "3", "C": "4", "D": "6"},
- "answer": "C",
- "solution": "f(x) = (x-1)(x-2). f(f(x)) = 0 → f(x) = 1 or f(x) = 2. f(x)=1: x²-3x+1=0, D=5>0 → 2 roots. f(x)=2: x²-3x=0, x(x-3)=0 → 2 roots. Total 4 roots."
- },
- {
- "text": "The sum of the series 1 + 2/3 + 4/9 + 8/27 + ... to infinity is:",
- "options": {"A": "1/3", "B": "2/3", "C": "3", "D": "3/2"},
- "answer": "C",
- "solution": "GP with a=1, r=2/3. Sum = a/(1-r) = 1/(1-2/3) = 1/(1/3) = 3."
- },
- {
- "text": "If A = [[1,2],[3,4]], then det(2A) = ?",
- "options": {"A": "-4", "B": "-8", "C": "-16", "D": "-2"},
- "answer": "C",
- "solution": "det(2A) = 2ⁿ × det(A) where n=2. det(A) = 4-6 = -2. det(2A) = 4 × (-2) = -8. Wait: det(2A) = 2² × det(A) = 4 × (-2) = -8. Correction: answer is B."
- },
- {
- "text": "The derivative of sin(x²) with respect to x is:",
- "options": {"A": "cos(x²)", "B": "2x·cos(x²)", "C": "2cos(x)", "D": "x·cos(x²)"},
- "answer": "B",
- "solution": "Using chain rule: d/dx[sin(x²)] = cos(x²) · d/dx(x²) = cos(x²) · 2x = 2x·cos(x²)."
- },
- {
- "text": "The area enclosed between y = x² and y = x is:",
- "options": {"A": "1/6", "B": "1/3", "C": "1/2", "D": "1"},
- "answer": "A",
- "solution": "Intersection at x=0 and x=1. Area = ∫₀¹(x - x²)dx = [x²/2 - x³/3]₀¹ = 1/2 - 1/3 = 1/6."
- },
- {
- "text": "The probability of getting exactly 2 heads in 5 tosses of a fair coin is:",
- "options": {"A": "5/16", "B": "1/4", "C": "5/32", "D": "10/32"},
- "answer": "D",
- "solution": "P = C(5,2) × (1/2)² × (1/2)³ = 10 × 1/32 = 10/32 = 5/16. Answer matches both A and D. Simplified: 5/16."
- },
- {
- "text": "The locus of a point equidistant from the points (1, 2) and (3, 4) is:",
- "options": {"A": "x + y = 5", "B": "x + y - 5 = 0", "C": "x - y = 0", "D": "2x + 2y - 9 = 0"},
- "answer": "B",
- "solution": "Midpoint = (2, 3). Slope of line joining = (4-2)/(3-1) = 1. Perpendicular bisector slope = -1. Equation: y-3 = -1(x-2) → x+y = 5."
- },
- {
- "text": "Value of lim(x→0) (sin 3x)/(5x) is:",
- "options": {"A": "3/5", "B": "5/3", "C": "1", "D": "0"},
- "answer": "A",
- "solution": "lim(x→0) sin(3x)/5x = lim(x→0) sin(3x)/(3x) × (3x/5x) = 1 × 3/5 = 3/5."
- },
- {
- "text": "The general solution of dy/dx = e^(x+y) is:",
- "options": {"A": "e^x + e^(-y) = C", "B": "e^(-y) = -e^x + C", "C": "e^x + e^y = C", "D": "e^(-x) + e^y = C"},
- "answer": "B",
- "solution": "Separating: e^(-y)dy = e^x dx. Integrating: -e^(-y) = e^x + C₁. So e^(-y) = -e^x + C where C = -C₁."
- },
- {
- "text": "If the vectors a = 2i + 3j - k and b = i - 2j + 3k are perpendicular, then what is the value of the dot product?",
- "options": {"A": "0", "B": "Not perpendicular; a·b = -7", "C": "5", "D": "11"},
- "answer": "B",
- "solution": "a·b = (2)(1) + (3)(-2) + (-1)(3) = 2 - 6 - 3 = -7 ≠ 0. So vectors are not perpendicular. The dot product is -7."
- },
- {
- "text": "The number of ways 6 books can be arranged on a shelf if 2 specific books must always be together:",
- "options": {"A": "120", "B": "240", "C": "480", "D": "720"},
- "answer": "B",
- "solution": "Treat 2 books as 1 unit: 5 units to arrange = 5! = 120 ways. The 2 books within unit can arrange in 2! = 2 ways. Total = 120 × 2 = 240."
- },
- {
- "text": "The equation of the tangent to the circle x² + y² = 25 at point (3, 4) is:",
- "options": {"A": "3x + 4y = 25", "B": "4x + 3y = 25", "C": "3x - 4y = 25", "D": "x + y = 7"},
- "answer": "A",
- "solution": "Tangent to circle x²+y²=r² at (x₁,y₁) is xx₁+yy₁=r². So tangent at (3,4) is 3x+4y=25."
- },
- {
- "text": "Evaluate ∫ (2x + 3)/(x² + 3x + 2) dx:",
- "options": {"A": "ln|x+1| + ln|x+2| + C", "B": "ln|x² + 3x + 2| + C", "C": "2ln|x+1| + C", "D": "ln|(x+1)(x+2)| + C"},
- "answer": "B",
- "solution": "Note numerator 2x+3 = d/dx(x²+3x+2). So integral = ln|x²+3x+2| + C. This equals ln|(x+1)(x+2)| + C, same as D."
- },
- {
- "text": "The value of ∑(k=1 to 10) k³ is:",
- "options": {"A": "3025", "B": "385", "C": "55", "D": "1210"},
- "answer": "A",
- "solution": "∑k³ = [n(n+1)/2]² = [10×11/2]² = 55² = 3025."
- },
- {
- "text": "If z = 1 + i, then |z²| is:",
- "options": {"A": "1", "B": "√2", "C": "2", "D": "4"},
- "answer": "C",
- "solution": "|z| = √(1²+1²) = √2. |z²| = |z|² = (√2)² = 2."
- },
-]
-
-BIOLOGY_QUESTIONS = [
- {
- "text": "Which organelle is called the 'powerhouse of the cell'?",
- "options": {"A": "Ribosome", "B": "Mitochondria", "C": "Golgi apparatus", "D": "Lysosome"},
- "answer": "B",
- "solution": "Mitochondria produce ATP through cellular respiration (oxidative phosphorylation), providing energy for cellular activities. Hence called the powerhouse."
- },
- {
- "text": "The process by which organisms maintain internal equilibrium is called:",
- "options": {"A": "Osmoregulation", "B": "Homeostasis", "C": "Thermoregulation", "D": "Excretion"},
- "answer": "B",
- "solution": "Homeostasis is the ability of an organism to maintain a stable internal environment (temperature, pH, glucose levels) despite external changes."
- },
- {
- "text": "The full form of DNA is:",
- "options": {"A": "Deoxyribonucleic Acid", "B": "Dioxyribonucleic Acid", "C": "Diribonucleic Acid", "D": "Deoxyribose Nucleic Acid"},
- "answer": "A",
- "solution": "DNA = Deoxyribonucleic Acid. It contains deoxyribose sugar, phosphate groups, and nitrogenous bases (A, T, G, C)."
- },
- {
- "text": "Which blood group is called the universal donor?",
- "options": {"A": "A+", "B": "B+", "C": "AB+", "D": "O-"},
- "answer": "D",
- "solution": "O- blood type has no A, B antigens and no Rh factor. So it can be donated to any blood group without immune reaction."
- },
- {
- "text": "Meiosis is essential for:",
- "options": {"A": "Growth and repair", "B": "Asexual reproduction", "C": "Maintaining chromosome number across generations", "D": "Cell differentiation"},
- "answer": "C",
- "solution": "Meiosis reduces chromosome number by half during gamete formation. When gametes fuse at fertilization, the diploid number is restored."
- },
- {
- "text": "The site of photosynthesis in plant cells is:",
- "options": {"A": "Nucleus", "B": "Mitochondria", "C": "Chloroplast", "D": "Ribosome"},
- "answer": "C",
- "solution": "Chloroplasts contain chlorophyll and are the sites of photosynthesis. They have thylakoid membranes for light reactions and stroma for Calvin cycle."
- },
- {
- "text": "Which of the following is an example of a vestigial organ in humans?",
- "options": {"A": "Liver", "B": "Appendix", "C": "Pancreas", "D": "Spleen"},
- "answer": "B",
- "solution": "The vermiform appendix is a vestigial organ in humans - it was functional in ancestors who digested cellulose but has reduced to a non-functional remnant."
- },
- {
- "text": "The Krebs cycle occurs in the:",
- "options": {"A": "Cytoplasm", "B": "Nucleus", "C": "Mitochondrial matrix", "D": "Chloroplast stroma"},
- "answer": "C",
- "solution": "The Krebs (citric acid) cycle occurs in the matrix of mitochondria. It oxidizes acetyl-CoA and generates NADH, FADH₂, and ATP."
- },
- {
- "text": "Darwin's theory of natural selection states that organisms with favorable variations:",
- "options": {"A": "Produce more offspring", "B": "Have better survival and reproduction", "C": "Are immune to diseases", "D": "Live longer"},
- "answer": "B",
- "solution": "Darwin proposed that organisms with traits better suited to their environment have higher fitness - better survival and reproductive success - passing traits to offspring."
- },
- {
- "text": "The enzyme responsible for DNA replication is:",
- "options": {"A": "RNA polymerase", "B": "DNA polymerase", "C": "Ligase", "D": "Helicase"},
- "answer": "B",
- "solution": "DNA polymerase synthesizes new DNA strands using the original strand as template. It adds nucleotides in 5'→3' direction."
- },
- {
- "text": "Which of these hormones is secreted by the adrenal medulla?",
- "options": {"A": "Cortisol", "B": "Aldosterone", "C": "Adrenaline", "D": "Testosterone"},
- "answer": "C",
- "solution": "Adrenal medulla secretes catecholamines: adrenaline (epinephrine) and noradrenaline. Cortisol and aldosterone are from adrenal cortex."
- },
- {
- "text": "The Rh factor is named after which organism?",
- "options": {"A": "Rattus norvegicus", "B": "Rhesus monkey", "C": "Rhinoceros", "D": "Rabbit"},
- "answer": "B",
- "solution": "The Rh (Rhesus) factor antigen was first discovered in Rhesus monkeys and was found to be present on human red blood cells."
- },
- {
- "text": "Which of the following is a genetic disease?",
- "options": {"A": "Malaria", "B": "Tuberculosis", "C": "Sickle cell anemia", "D": "Cholera"},
- "answer": "C",
- "solution": "Sickle cell anemia is caused by a mutation in the HBB gene encoding the β-globin protein, inherited in autosomal recessive pattern."
- },
- {
- "text": "The basic unit of heredity is:",
- "options": {"A": "Chromosome", "B": "Nucleotide", "C": "Gene", "D": "Codon"},
- "answer": "C",
- "solution": "A gene is the functional unit of heredity - a specific sequence of DNA that encodes for a protein or functional RNA molecule."
- },
- {
- "text": "Transpiration in plants occurs mainly through:",
- "options": {"A": "Roots", "B": "Stomata", "C": "Lenticels", "D": "Xylem"},
- "answer": "B",
- "solution": "About 90-95% of transpiration occurs through stomata (stomatal transpiration). Lenticels and cuticle account for remaining 5-10%."
- },
-]
-
-def seed_real_dpps():
- with Session(engine) as session:
- # Get or use system admin
- admin = session.exec(select(models.User).limit(1)).first()
- admin_id = admin.id if admin else uuid.UUID("11111111-1111-1111-1111-111111111111")
-
- # Remove existing DPS tests
- existing = session.exec(select(models.Test).where(models.Test.type == "DPS")).all()
- for t in existing:
- session.delete(t)
- session.commit()
- print(f"Cleared {len(existing)} old DPS tests.")
-
- subjects_data = [
- ("Physics", "Laws of Motion & Mechanics", PHYSICS_QUESTIONS, models.ExamEnum.JEE_MAIN, "dps-phy-ch4-20260707.pdf"),
- ("Chemistry", "Chemical Thermodynamics & Bonds", CHEMISTRY_QUESTIONS, models.ExamEnum.JEE_MAIN, "dps-che-ch4-20260707.pdf"),
- ("Mathematics", "Calculus, Algebra & Geometry", MATH_QUESTIONS, models.ExamEnum.JEE_MAIN, "dps-mth-ch4-20260707.pdf"),
- ("Biology", "Cell Biology & Genetics", BIOLOGY_QUESTIONS, models.ExamEnum.NEET, "dps-bio-ch4-20260707.pdf"),
- ]
-
- for subject, topic, questions, exam, pdf in subjects_data:
- question_ids = []
- for q in questions:
- new_q = models.Question(
- subject=subject,
- topic=topic,
- subtopic="Chapter 4",
- difficulty="Medium",
- type=models.QuestionTypeEnum.MCQ_SINGLE,
- question_text=q["text"],
- options=q["options"],
- correct_answer={"value": q["answer"]},
- solution_text=q["solution"],
- language=models.MediumEnum.ENGLISH,
- exam=exam,
- created_by=admin_id
- )
- session.add(new_q)
- session.flush()
- question_ids.append(str(new_q.id))
-
- # Create the DPS test (PrepPilot AI section)
- clean_exam_name = exam.value.replace("_", " ")
- test_obj = models.Test(
- title=f"{subject} Chapter 4 DPP",
- type="DPS",
- exam_pattern=exam,
- sections={
- "Section 1": {
- "questions": question_ids,
- "time_limit": 30
- }
- },
- total_time=30,
- difficulty="Medium",
- scheduled_date=datetime.utcnow(),
- is_active=True,
- created_by=admin_id
- )
- session.add(test_obj)
- print(f"✓ Created DPS test: {subject} Chapter 4 DPP ({clean_exam_name}) with {len(questions)} real questions")
-
- session.commit()
- print("\n✅ All real DPP tests seeded successfully!")
-
-if __name__ == "__main__":
- seed_real_dpps()
diff --git a/backend/seed_from_dpps.py b/backend/seed_from_dpps.py
deleted file mode 100644
index e6f73d5..0000000
--- a/backend/seed_from_dpps.py
+++ /dev/null
@@ -1,138 +0,0 @@
-import asyncio
-import os
-import uuid
-import re
-import glob
-from sqlmodel import Session, select, delete
-from datetime import datetime
-
-import fitz # PyMuPDF
-from database import engine, init_db
-import models
-
-def parse_pdf(filepath):
- doc = fitz.open(filepath)
- text = ""
- for page in doc:
- text += page.get_text() + "\n"
-
- q_block = text.split("Questions\n")[-1].split("Answer Key\n")[0]
- q_pattern = re.compile(r'^(\d+)\.\s+(.*?)(?=^\d+\.\s+|\Z)', re.DOTALL | re.MULTILINE)
-
- questions_dict = {}
- for match in q_pattern.finditer(q_block):
- q_num = int(match.group(1))
- if 1 <= q_num <= 15:
- questions_dict[q_num] = match.group(2).strip()
-
- answers_dict = {}
- if "Answer Key\n" in text:
- a_block = text.split("Answer Key\n")[-1]
- a_pattern = re.compile(r'^(\d+)\s+(.*?)(?=^\d+\s+|\Z)', re.DOTALL | re.MULTILINE)
- for match in a_pattern.finditer(a_block):
- q_num = int(match.group(1))
- if 1 <= q_num <= 15:
- # Some answers might get overwritten if footer matches the same number, but
- # answers are strictly linear 1 to 15, so we only take the first match.
- if q_num not in answers_dict:
- answers_dict[q_num] = match.group(2).strip()
-
- return questions_dict, answers_dict
-
-async def seed_pdfs():
- admin_id_str = "b0e17378-864d-439e-8806-708dc48a891b"
- pdf_files = glob.glob('/app/pdfs_to_parse/*.pdf')
-
- if not pdf_files:
- print("No PDFs found in /app/pdfs_to_parse/")
- return
-
- with Session(engine) as session:
- admin = session.exec(select(models.User).limit(1)).first()
- admin_id = admin.id if admin else uuid.UUID(admin_id_str)
-
- statement = select(models.Test).where(models.Test.type == "DPS")
- old_tests = session.exec(statement).all()
- for t in old_tests:
- session.delete(t)
- session.commit()
-
- for pdf_path in pdf_files:
- filename = os.path.basename(pdf_path)
- subject = "Physics"
- topic = "General"
- if "phy" in filename.lower():
- subject = "Physics"
- topic = "Laws of Motion"
- elif "che" in filename.lower():
- subject = "Chemistry"
- topic = "Chemical Bonding"
- elif "mth" in filename.lower():
- subject = "Mathematics"
- topic = "Calculus"
- elif "bio" in filename.lower():
- subject = "Biology"
- topic = "Genetics"
-
- print(f"Parsing {filename}...")
- q_dict, a_dict = parse_pdf(pdf_path)
- print(f"Found {len(q_dict)} questions and {len(a_dict)} answers.")
-
- if not q_dict:
- continue
-
- question_ids = []
- for i in range(1, 16):
- if i not in q_dict:
- continue
- q_text = q_dict[i].replace('\x00', '')
- ans_text = a_dict.get(i, "Solution not provided in PDF.").replace('\x00', '')
-
- options = {
- "A": "Correct Answer - Refer to Solution Key below",
- "B": "Incorrect Placeholder",
- "C": "Incorrect Placeholder",
- "D": "Incorrect Placeholder"
- }
-
- new_q = models.Question(
- subject=subject,
- topic=topic,
- subtopic="General",
- difficulty="Medium",
- type=models.QuestionTypeEnum.NUMERICAL,
- question_text=q_text,
- options=None,
- correct_answer={"value": "Subjective input"},
- solution_text=ans_text,
- language=models.MediumEnum.ENGLISH,
- exam=models.ExamEnum.JEE_MAIN,
- created_by=admin_id
- )
- session.add(new_q)
- session.flush()
- question_ids.append(str(new_q.id))
-
- if question_ids:
- test_obj = models.Test(
- title=f"DPP: {subject} - {topic}",
- type="DPS",
- exam_pattern=models.ExamEnum.JEE_MAIN,
- sections={
- "Section 1": {
- "questions": question_ids,
- "time_limit": 45
- }
- },
- total_time=45,
- difficulty="Medium",
- scheduled_date=datetime.utcnow(),
- is_active=True,
- created_by=admin_id
- )
- session.add(test_obj)
- session.commit()
- print(f"Successfully created DPP Test: {test_obj.title} with {len(question_ids)} questions.")
-
-if __name__ == "__main__":
- asyncio.run(seed_pdfs())
diff --git a/backend/test.pdf b/backend/test.pdf
deleted file mode 100644
index ce44ad4..0000000
Binary files a/backend/test.pdf and /dev/null differ
diff --git a/backend/test2.pdf b/backend/test2.pdf
deleted file mode 100644
index 896d52f..0000000
Binary files a/backend/test2.pdf and /dev/null differ
diff --git a/backend/worker.py b/backend/worker.py
index 3ee8bcf..bd99e0f 100644
--- a/backend/worker.py
+++ b/backend/worker.py
@@ -1,19 +1,23 @@
+"""
+worker.py — PragatiVeer Celery Background Tasks
+Active Tasks:
+ - generate_daily_study_plans: Runs daily at 5:00 AM IST via Celery Beat
+
+Deferred (Phase 2 — AI LLM):
+ - process_pdf_test: PDF → LLM → Questions pipeline (stubbed in pdf_parser.py)
+"""
import os
-from datetime import datetime, timedelta
+from datetime import datetime
from celery import Celery
from celery.schedules import crontab
from sqlmodel import Session, select
-import json
-import asyncio
from database import engine
import models
-import storage
-from pdf_parser import parse_pdf_to_questions
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
-# Initialize Celery connected to our local Redis broker
+# Initialize Celery connected to our Redis broker
celery_app = Celery(
"pragativeer_tasks",
broker=REDIS_URL,
@@ -29,8 +33,8 @@ celery_app.conf.update(
@celery_app.task(name="generate_daily_study_plans", bind=True, default_retry_delay=60, max_retries=3)
def generate_daily_study_plans(self):
"""
- Runs daily to evaluate students' weak areas and generate
- a personalized AI study plan for the day.
+ Runs daily to evaluate students' weak areas and generate
+ a personalized study plan for the day.
"""
try:
with Session(engine) as session:
@@ -71,87 +75,18 @@ def generate_daily_study_plans(self):
)
session.add(new_plan)
plans_created += 1
-
+
session.commit()
return f"Successfully generated {plans_created} daily study plans."
except Exception as exc:
print(f"Database error during study plan generation: {exc}")
raise self.retry(exc=exc)
-@celery_app.task(name="process_pdf_test", bind=True, max_retries=1)
-def process_pdf_test(self, test_id_str: str, object_name: str, admin_id_str: str, exam_pattern: str):
- """
- Background task to download PDF from MinIO, extract text,
- use LLM to generate questions, and attach them to a Test record.
- """
- try:
- print(f"Starting PDF processing for test {test_id_str}")
-
- # 1. Download PDF from MinIO
- pdf_bytes = storage.get_file(object_name)
- if not pdf_bytes:
- print("Failed to download PDF from MinIO")
- return
-
- # 2. Extract Questions using LLM (Async)
- questions_data = asyncio.run(parse_pdf_to_questions(pdf_bytes, exam_pattern))
- if not questions_data:
- print("LLM parsing returned empty data")
- return
-
- import uuid
- test_id = uuid.UUID(test_id_str)
- admin_id = uuid.UUID(admin_id_str)
-
- with Session(engine) as session:
- test_obj = session.get(models.Test, test_id)
- if not test_obj:
- print("Test record not found in DB")
- return
- question_ids = []
- for q_data in questions_data:
- # Create a Question DB object
- new_q = models.Question(
- subject=q_data.get("subject", "Physics"),
- topic=q_data.get("topic", "General"),
- subtopic=q_data.get("subtopic", "General"),
- difficulty=q_data.get("difficulty", "Medium"),
- type=q_data.get("type", models.QuestionTypeEnum.MCQ_SINGLE),
- question_text=q_data.get("question_text", ""),
- options=q_data.get("options", {}),
- correct_answer=q_data.get("correct_answer", {}),
- solution_text=q_data.get("solution_text", ""),
- language=models.MediumEnum.ENGLISH,
- exam=models.ExamEnum(exam_pattern),
- created_by=admin_id
- )
- session.add(new_q)
- session.flush() # To get the ID
- question_ids.append(str(new_q.id))
-
- # Update Test record with the new sections
- test_obj.sections = {
- "Section 1": {
- "questions": question_ids,
- "time_limit": test_obj.total_time
- }
- }
- # Automatically activate it when done processing
- test_obj.is_active = True
-
- session.commit()
- print(f"Successfully processed PDF and created {len(question_ids)} questions for test {test_id_str}")
- return f"Processed {len(question_ids)} questions"
- except Exception as exc:
- print(f"Error processing PDF test: {exc}")
- # Mark test as failed if we had a status field, for now just log it
- raise self.retry(exc=exc)
-
-# Schedule the tasks using Celery Beat
+# Schedule: 5:00 AM IST every day
celery_app.conf.beat_schedule = {
'generate-study-plans-every-morning': {
'task': 'generate_daily_study_plans',
- 'schedule': crontab(hour=5, minute=0), # 5:00 AM IST
+ 'schedule': crontab(hour=5, minute=0),
},
}