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), }, }