feat: audit and purge legacy backend files, stub Phase 2 AI LLM modules, create comprehensive PAGEWISE_API_DOCUMENTATION.md, and wire DPS test seeding
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
> **Last Verified:** September 2026
|
> **Last Verified:** September 2026
|
||||||
> **Backend Engine:** FastAPI (Python 3.11) + SQLModel (SQLAlchemy) + Celery + Redis
|
> **Backend Engine:** FastAPI (Python 3.11) + SQLModel (SQLAlchemy) + Celery + Redis
|
||||||
> **Frontend Engine:** Next.js 16 (React 19, TypeScript, TailwindCSS)
|
> **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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
BIN
API_DOCUMENTATION.pdf
Normal file
BIN
API_DOCUMENTATION.pdf
Normal file
Binary file not shown.
724
PAGEWISE_API_DOCUMENTATION.md
Normal file
724
PAGEWISE_API_DOCUMENTATION.md
Normal file
@@ -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 `<input type="email" />`
|
||||||
|
- Password `<input type="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": "<random_uuid>" }`
|
||||||
|
- **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 `<input type="text" />`
|
||||||
|
- Email `<input type="email" />`
|
||||||
|
- Password `<input type="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": "<userId>", "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 `<input type="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": "<userId>",
|
||||||
|
"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": "<examId>",
|
||||||
|
"user_id": "<userId>",
|
||||||
|
"time_spent": 10420,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"question_id": "<qId>",
|
||||||
|
"selected_option": "B",
|
||||||
|
"time_spent": 115,
|
||||||
|
"is_marked_for_review": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "Submitted"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "<attemptId>",
|
||||||
|
"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: <binary_image>`
|
||||||
|
- **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 `<input />` 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": "<userId>", "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": "<mistakeId>", "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 `<input />`
|
||||||
|
- Exam Pattern selector (`JEE_MAIN`, `JEE_ADVANCED`, `NEET`)
|
||||||
|
- Test Type selector (`Mock`, `DPS`)
|
||||||
|
- Time Limit `<input />` (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": "<adminId>",
|
||||||
|
"questions_json": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response:** `{ "status": "created", "test_id": "<uuid>", "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 `<input />` (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: <image>` |
|
||||||
|
| **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*
|
||||||
@@ -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.')
|
|
||||||
@@ -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()
|
|
||||||
@@ -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.')
|
|
||||||
@@ -1,55 +1,23 @@
|
|||||||
import os
|
"""
|
||||||
import json
|
llm.py — AI Tutor / LLM Streaming Interface
|
||||||
from openai import AsyncOpenAI
|
STATUS: DEFERRED — Phase 2 (AI Coach LLM integration)
|
||||||
from sqlmodel import Session, select
|
|
||||||
from fastapi.responses import StreamingResponse
|
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 uuid import UUID
|
||||||
|
from typing import AsyncIterator
|
||||||
|
|
||||||
import models
|
|
||||||
|
|
||||||
# Point the standard AsyncOpenAI client to your local Ollama server
|
async def stream_tutor_response(user_id: UUID, user_message: str, session) -> AsyncIterator[str]:
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
Stub: yields a single message explaining AI is coming soon.
|
||||||
# 3. Call the LLM
|
Replace with real LLM streaming call in Phase 2.
|
||||||
response = await client.chat.completions.create(
|
"""
|
||||||
model=os.getenv("LLM_MODEL_NAME", "gemma"),
|
yield "🤖 AI Tutor is coming soon. This feature will be enabled once LLM infrastructure is provisioned."
|
||||||
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
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import models
|
|||||||
from ai_coach import calculate_sm2
|
from ai_coach import calculate_sm2
|
||||||
from analytics import generate_parent_report
|
from analytics import generate_parent_report
|
||||||
from billing import generate_mock_order, verify_webhook_signature
|
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 predictor import calculate_readiness_score
|
||||||
from growth import generate_referral_link, process_referral_reward
|
from growth import generate_referral_link, process_referral_reward
|
||||||
from email_service import send_welcome_email, send_otp_email
|
from email_service import send_welcome_email, send_otp_email
|
||||||
@@ -748,9 +748,13 @@ async def upload_admin_test(
|
|||||||
admin_id: UUID = Form(...),
|
admin_id: UUID = Form(...),
|
||||||
session: Session = Depends(get_session)
|
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 storage
|
||||||
import uuid
|
import uuid
|
||||||
from worker import process_pdf_test
|
|
||||||
|
|
||||||
pdf_bytes = await file.read()
|
pdf_bytes = await file.read()
|
||||||
current_year = datetime.utcnow().year
|
current_year = datetime.utcnow().year
|
||||||
@@ -774,14 +778,20 @@ async def upload_admin_test(
|
|||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(test_obj)
|
session.refresh(test_obj)
|
||||||
|
|
||||||
process_pdf_test.delay(
|
# Phase 2: Uncomment below to trigger AI question extraction from PDF
|
||||||
test_id_str=str(test_obj.id),
|
# from worker import process_pdf_test
|
||||||
object_name=object_name,
|
# process_pdf_test.delay(
|
||||||
admin_id_str=str(admin_id),
|
# test_id_str=str(test_obj.id),
|
||||||
exam_pattern=exam_pattern
|
# 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"])
|
@app.post("/api/admin/tests/create_manual", tags=["Admin"])
|
||||||
def create_manual_test(
|
def create_manual_test(
|
||||||
|
|||||||
@@ -1,85 +1,21 @@
|
|||||||
import fitz # PyMuPDF
|
"""
|
||||||
import json
|
pdf_parser.py — PDF-to-Question AI Parser
|
||||||
import logging
|
STATUS: DEFERRED — Phase 2 (AI Coach LLM integration)
|
||||||
import asyncio
|
|
||||||
|
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
|
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]]:
|
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 = ""
|
return []
|
||||||
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 []
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,8 +8,6 @@ pydantic-settings==2.2.1
|
|||||||
bcrypt==4.1.2
|
bcrypt==4.1.2
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
celery==5.3.6
|
celery==5.3.6
|
||||||
openai==1.14.0
|
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
boto3==1.34.0
|
boto3==1.34.0
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
PyMuPDF==1.23.26
|
|
||||||
|
|||||||
@@ -412,8 +412,9 @@ def seed_local_database():
|
|||||||
dps_session.add(q)
|
dps_session.add(q)
|
||||||
q_ids.append(str(q.id))
|
q_ids.append(str(q.id))
|
||||||
|
|
||||||
|
dps_id = uuid4()
|
||||||
dps = models.DPS(
|
dps = models.DPS(
|
||||||
id=uuid4(),
|
id=dps_id,
|
||||||
date=sheet_date,
|
date=sheet_date,
|
||||||
title=sheet_title,
|
title=sheet_title,
|
||||||
questions=q_ids,
|
questions=q_ids,
|
||||||
@@ -423,7 +424,22 @@ def seed_local_database():
|
|||||||
created_by=admin_id,
|
created_by=admin_id,
|
||||||
)
|
)
|
||||||
dps_session.add(dps)
|
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()
|
dps_session.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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())
|
|
||||||
BIN
backend/test.pdf
BIN
backend/test.pdf
Binary file not shown.
Binary file not shown.
@@ -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
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from database import engine
|
from database import engine
|
||||||
import models
|
import models
|
||||||
import storage
|
|
||||||
from pdf_parser import parse_pdf_to_questions
|
|
||||||
|
|
||||||
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
|
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(
|
celery_app = Celery(
|
||||||
"pragativeer_tasks",
|
"pragativeer_tasks",
|
||||||
broker=REDIS_URL,
|
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)
|
@celery_app.task(name="generate_daily_study_plans", bind=True, default_retry_delay=60, max_retries=3)
|
||||||
def generate_daily_study_plans(self):
|
def generate_daily_study_plans(self):
|
||||||
"""
|
"""
|
||||||
Runs daily to evaluate students' weak areas and generate
|
Runs daily to evaluate students' weak areas and generate
|
||||||
a personalized AI study plan for the day.
|
a personalized study plan for the day.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
@@ -71,87 +75,18 @@ def generate_daily_study_plans(self):
|
|||||||
)
|
)
|
||||||
session.add(new_plan)
|
session.add(new_plan)
|
||||||
plans_created += 1
|
plans_created += 1
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
return f"Successfully generated {plans_created} daily study plans."
|
return f"Successfully generated {plans_created} daily study plans."
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"Database error during study plan generation: {exc}")
|
print(f"Database error during study plan generation: {exc}")
|
||||||
raise self.retry(exc=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 = []
|
# Schedule: 5:00 AM IST every day
|
||||||
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
|
|
||||||
celery_app.conf.beat_schedule = {
|
celery_app.conf.beat_schedule = {
|
||||||
'generate-study-plans-every-morning': {
|
'generate-study-plans-every-morning': {
|
||||||
'task': 'generate_daily_study_plans',
|
'task': 'generate_daily_study_plans',
|
||||||
'schedule': crontab(hour=5, minute=0), # 5:00 AM IST
|
'schedule': crontab(hour=5, minute=0),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user