From 426b98ddab5e82118d30aef59c9de187cac5d111 Mon Sep 17 00:00:00 2001 From: ectasy663 Date: Mon, 6 Jul 2026 17:08:13 +0530 Subject: [PATCH] feat: complete pragativeer v2 frontend and backend, docker setup, and cbt engine --- .dockerignore | 12 + Dockerfile.dev | 13 + backend/.gitignore | 12 + backend/Dockerfile.dev | 15 + backend/ai_coach.py | 34 + backend/analytics.py | 63 + backend/billing.py | 45 + backend/celerybeat-schedule | Bin 0 -> 16384 bytes backend/database.py | 17 + backend/growth.py | 36 + backend/llm.py | 55 + backend/main.py | 490 +++++ backend/models.py | 168 ++ backend/predictor.py | 76 + backend/requirements.txt | 12 + backend/seed.py | 317 ++++ backend/worker.py | 84 + docker-compose.yml | 116 ++ package-lock.json | 1592 ++++++++++++++++- package.json | 11 +- pragativeer-prd (4).pdf | Bin 0 -> 108566 bytes src/app/admin/students/page.tsx | 27 +- src/app/api/og/progress/route.tsx | 67 + src/app/cbt/[examId]/page.tsx | 604 ++++++- src/app/dashboard/ai-coach/page.tsx | 170 ++ src/app/dashboard/analytics/page.tsx | 84 +- .../cbt/history/[attemptId]/page.tsx | 168 ++ src/app/dashboard/cbt/page.tsx | 196 +- src/app/dashboard/community/page.tsx | 195 ++ src/app/dashboard/dpp/page.tsx | 55 +- src/app/dashboard/layout.tsx | 55 +- src/app/dashboard/leaderboard/page.tsx | 142 ++ src/app/dashboard/materials/page.tsx | 2 +- src/app/dashboard/page.tsx | 155 +- src/app/dashboard/progress/page.tsx | 192 ++ src/app/dashboard/settings/page.tsx | 137 +- src/app/dashboard/streaks/page.tsx | 99 + src/app/globals.css | 16 + src/app/layout.tsx | 1 + src/app/page.tsx | 31 +- src/app/parent/dashboard/page.tsx | 217 +++ src/app/signup/page.tsx | 205 ++- src/app/upgrade/page.tsx | 226 +++ src/components/AI/AITutorChat.tsx | 138 ++ src/components/AI/AdaptiveStudyPlan.tsx | 103 ++ src/components/AI/CollegePredictorCard.tsx | 84 + src/components/AI/MistakeHeatmap.tsx | 80 + src/components/AI/MistakeReviewCard.tsx | 169 ++ src/components/AI/ProgressShare.tsx | 64 + src/components/AI/ReadinessGauge.tsx | 120 ++ src/components/AI/ReferralCard.tsx | 72 + src/components/AI/StudyPlanCard.tsx | 131 ++ src/components/AI/TutorChat.tsx | 143 ++ src/components/CBT/Calculator.tsx | 111 ++ src/components/CBT/ExamResult.tsx | 127 ++ src/components/CBT/PeriodicTable.tsx | 43 + src/components/CBT/Scratchpad.tsx | 177 ++ src/components/LanguageToggle.tsx | 37 + src/components/StreakCounter.tsx | 16 + src/components/ThemeToggle.tsx | 41 + src/components/TierBadge.tsx | 21 + src/components/ui/input.tsx | 23 + src/components/ui/label.tsx | 18 + src/components/ui/switch.tsx | 48 + src/hooks/useExamTimer.ts | 49 + src/store/cbtStore.ts | 285 +++ 66 files changed, 7949 insertions(+), 363 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.dev create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile.dev create mode 100644 backend/ai_coach.py create mode 100644 backend/analytics.py create mode 100644 backend/billing.py create mode 100644 backend/celerybeat-schedule create mode 100644 backend/database.py create mode 100644 backend/growth.py create mode 100644 backend/llm.py create mode 100644 backend/main.py create mode 100644 backend/models.py create mode 100644 backend/predictor.py create mode 100644 backend/requirements.txt create mode 100644 backend/seed.py create mode 100644 backend/worker.py create mode 100644 docker-compose.yml create mode 100644 pragativeer-prd (4).pdf create mode 100644 src/app/api/og/progress/route.tsx create mode 100644 src/app/dashboard/ai-coach/page.tsx create mode 100644 src/app/dashboard/cbt/history/[attemptId]/page.tsx create mode 100644 src/app/dashboard/community/page.tsx create mode 100644 src/app/dashboard/leaderboard/page.tsx create mode 100644 src/app/dashboard/progress/page.tsx create mode 100644 src/app/dashboard/streaks/page.tsx create mode 100644 src/app/parent/dashboard/page.tsx create mode 100644 src/app/upgrade/page.tsx create mode 100644 src/components/AI/AITutorChat.tsx create mode 100644 src/components/AI/AdaptiveStudyPlan.tsx create mode 100644 src/components/AI/CollegePredictorCard.tsx create mode 100644 src/components/AI/MistakeHeatmap.tsx create mode 100644 src/components/AI/MistakeReviewCard.tsx create mode 100644 src/components/AI/ProgressShare.tsx create mode 100644 src/components/AI/ReadinessGauge.tsx create mode 100644 src/components/AI/ReferralCard.tsx create mode 100644 src/components/AI/StudyPlanCard.tsx create mode 100644 src/components/AI/TutorChat.tsx create mode 100644 src/components/CBT/Calculator.tsx create mode 100644 src/components/CBT/ExamResult.tsx create mode 100644 src/components/CBT/PeriodicTable.tsx create mode 100644 src/components/CBT/Scratchpad.tsx create mode 100644 src/components/LanguageToggle.tsx create mode 100644 src/components/StreakCounter.tsx create mode 100644 src/components/ThemeToggle.tsx create mode 100644 src/components/TierBadge.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/hooks/useExamTimer.ts create mode 100644 src/store/cbtStore.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..596c325 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +.git +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..dcac166 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy the rest of the application code +COPY . . + +# Start the Next.js development server +CMD ["npm", "run", "dev"] diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..18d787d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*$py.class + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..9bbcbad --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,15 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/ai_coach.py b/backend/ai_coach.py new file mode 100644 index 0000000..3d2616f --- /dev/null +++ b/backend/ai_coach.py @@ -0,0 +1,34 @@ +from datetime import datetime, timedelta +from typing import Tuple + +def calculate_sm2(q: int, repetitions: int, ease_factor: float, previous_interval: int) -> Tuple[int, float]: + """ + Implements the standard SuperMemo-2 (SM-2) algorithm. + q: quality of the response (0-5) + 5: perfect response + 4: correct response after a hesitation + 3: correct response caught with serious difficulty + 2: incorrect response; where the correct one seemed easy to recall + 1: incorrect response; the correct one remembered + 0: complete blackout + """ + if q < 3: + # If the answer was incorrect, reset repetitions and schedule for immediate review (1 day) + return 1, ease_factor + + # Calculate new Easiness Factor + new_ef = ease_factor + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)) + if new_ef < 1.3: + new_ef = 1.3 + + # Calculate next interval (in days) + if repetitions == 0: + new_interval = 1 + elif repetitions == 1: + new_interval = 3 # Calibrated for intense JEE/NEET daily sprint cycles + elif repetitions == 2: + new_interval = 7 + else: + new_interval = round(previous_interval * new_ef) + + return new_interval, new_ef diff --git a/backend/analytics.py b/backend/analytics.py new file mode 100644 index 0000000..f0e178f --- /dev/null +++ b/backend/analytics.py @@ -0,0 +1,63 @@ +from sqlmodel import Session, select +from datetime import datetime, timedelta +from collections import Counter +from uuid import UUID +import models + +def generate_parent_report(student_id: UUID, session: Session) -> dict: + """ + Aggregates granular performance data for a specific student + into a clean, simple, parent-friendly schema. + """ + # 1. Fetch Student Baseline Info + student = session.get(models.User, student_id) + if not student: + return {"error": "Student record not found."} + + # 2. Compile Last 7 Days Time & Attempts Data + seven_days_ago = datetime.utcnow() - timedelta(days=7) + attempts = session.exec( + select(models.Attempt) + .where(models.Attempt.user_id == student_id) + .where(models.Attempt.submitted_at >= seven_days_ago) + ).all() + + total_time_spent_seconds = sum((a.time_taken or 0) for a in attempts) + total_time_spent_minutes = round(total_time_spent_seconds / 60, 1) + + # 3. Calculate 7-Day Accuracy Trend Line Data + # Group accuracy by date strings for simple frontend parsing + trend_data = [] + for i in range(6, -1, -1): + target_date = (datetime.utcnow() - timedelta(days=i)).date() + daily_attempts = [a for a in attempts if a.submitted_at.date() == target_date] + + if daily_attempts: + avg_accuracy = sum(a.accuracy for a in daily_attempts) / len(daily_attempts) + trend_data.append({"date": target_date.strftime("%b %d"), "accuracy": round(avg_accuracy, 1)}) + else: + trend_data.append({"date": target_date.strftime("%b %d"), "accuracy": 0.0}) + + # 4. Extract Top "Areas of Concern" (Active un-mastered mistakes) + active_mistakes = session.exec( + select(models.Mistake) + .where(models.Mistake.user_id == student_id) + .where(models.Mistake.is_mastered == False) + ).all() + + # Aggregate topic counts to find where they struggle most + topic_counter = Counter([m.topic for m in active_mistakes]) + top_concerns = [ + {"topic": topic, "pending_errors": count} + for topic, count in topic_counter.most_common(3) + ] + + return { + "student_name": student.name, + "streak_count": student.streak_count, + "weekly_minutes_spent": total_time_spent_minutes, + "total_tests_attempted_this_week": len(attempts), + "accuracy_trend": trend_data, + "areas_of_concern": top_concerns, + "last_active": student.last_active.strftime("%Y-%m-%d %H:%M:%S") + } diff --git a/backend/billing.py b/backend/billing.py new file mode 100644 index 0000000..bc8760a --- /dev/null +++ b/backend/billing.py @@ -0,0 +1,45 @@ +import os +import hmac +import hashlib +from uuid import UUID + +# In a live environment, these are provided by the Razorpay Dashboard. +# We use mock keys for the local sandbox. +RAZORPAY_KEY_ID = os.getenv("RAZORPAY_KEY_ID", "rzp_test_local_sandbox") +RAZORPAY_KEY_SECRET = os.getenv("RAZORPAY_KEY_SECRET", "local_secret_mock") +RAZORPAY_WEBHOOK_SECRET = os.getenv("RAZORPAY_WEBHOOK_SECRET", "local_webhook_secret") + +def generate_mock_order(user_id: UUID, tier: str, amount_in_paise: int) -> dict: + """ + Simulates the Razorpay Orders API response for local testing. + Normally this would call: razorpay_client.order.create(data) + """ + import time + mock_order_id = f"order_{int(time.time())}_{str(user_id)[:8]}" + + return { + "id": mock_order_id, + "entity": "order", + "amount": amount_in_paise, + "currency": "INR", + "status": "created", + "notes": { + "user_id": str(user_id), + "tier_requested": tier + } + } + +def verify_webhook_signature(payload_body: str, signature_header: str) -> bool: + """ + Cryptographically verifies that the webhook actually came from Razorpay. + """ + if not signature_header: + return False + + expected_signature = hmac.new( + key=RAZORPAY_WEBHOOK_SECRET.encode('utf-8'), + msg=payload_body.encode('utf-8'), + digestmod=hashlib.sha256 + ).hexdigest() + + return hmac.compare_digest(expected_signature, signature_header) diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule new file mode 100644 index 0000000000000000000000000000000000000000..a9c5eb0de4e2d7cc68bed6db2ca0aa63e0902b36 GIT binary patch literal 16384 zcmeI3&x_MQ6vq?k+TCrH!V0SvL{#t>&ANzQRYXv7Q(?h_VUmpPu-VCyOoSE+J&D42 z+S!YLfM@X^@a)Myz>`-G{sn@*>7-i3%lczM`37d*W758P{q(&rlSAIU*jVv8o;Mw2 ztX-<@OKY#&$@4xtgBb)s00ck)1V8`;KmY_l00ck)1VG?lByh4$8rFdT2XL?iPzR_3 z)B*YcIshGj4nPN>1JD8J0CeC#(}4r7tG#w@y-Ut14?FvOeRkvbZG%F5q3dUnuf!y{4`-w3F>lxU*P?BY&;bAa;SbK>> z{dxPJ3tmndV-wi(Z&8X|M$x3PJADg)GS&-Mg3G~G6Q~ibc4{EHeWMg?EzGk`rkDw= zHgn11jHi>v)8Jd!-7{1~_8J>~s?Q}anBu)sRq3#|oiSPVIBh!YZRLd&vTt-N;TbIi zF=xu?L-&#aPpgdIqCAIY$DAAZ`uHqPlroVGqiM8g8qMg%lr~Uei_g0|=H_zqWAoth z>DAV;Yjh{BM5cr+jc)e2VoDVhTQ81s$%TBBlly?&JJzN3rKpnIBH7Hid1CIGAAWo2 z+eJG Generator[Session, None, None]: + with Session(engine) as session: + yield session diff --git a/backend/growth.py b/backend/growth.py new file mode 100644 index 0000000..255d43d --- /dev/null +++ b/backend/growth.py @@ -0,0 +1,36 @@ +from sqlmodel import Session, select +from uuid import UUID +import uuid +from datetime import timedelta +import models + +def generate_referral_link(user_id: UUID) -> str: + """Generates a unique shortcode for the user to share.""" + # Using a simple deterministic slice for the local sandbox + shortcode = str(user_id).split("-")[0] + return f"https://pragativeer.com/join/{shortcode}" + +def process_referral_reward(referrer_id: UUID, new_user_id: UUID, session: Session): + """ + Grants both users 7 days of AI Coach access for free as per PRD Section 3.2. + """ + referrer = session.get(models.User, referrer_id) + new_user = session.get(models.User, new_user_id) + + if referrer and new_user: + # Upgrade both to AI Coach + referrer.tier = models.TierEnum.AI_COACH + new_user.tier = models.TierEnum.AI_COACH + + # Add 7 days to their trial/subscription end dates + from datetime import datetime + now = datetime.utcnow() + + referrer.subscription_end = (referrer.subscription_end or now) + timedelta(days=7) + new_user.trial_end = now + timedelta(days=7) + + session.add(referrer) + session.add(new_user) + session.commit() + return True + return False diff --git a/backend/llm.py b/backend/llm.py new file mode 100644 index 0000000..330da22 --- /dev/null +++ b/backend/llm.py @@ -0,0 +1,55 @@ +import os +import json +from openai import AsyncOpenAI +from sqlmodel import Session, select +from fastapi.responses import StreamingResponse +from uuid import UUID + +import models + +# Point the standard AsyncOpenAI client to your local Ollama server +client = AsyncOpenAI( + base_url=os.getenv("LLM_BASE_URL", "http://host.docker.internal:11434/v1"), + api_key=os.getenv("OPENAI_API_KEY", "ollama"), # Required by the SDK, but Ollama ignores it +) + +async def stream_tutor_response(user_id: UUID, user_message: str, session: Session): + # 1. Fetch Context: Get the student's active weak areas + active_mistakes = session.exec( + select(models.Mistake) + .where(models.Mistake.user_id == user_id) + .where(models.Mistake.is_mastered == False) + ).all() + + weak_topics = list(set([m.topic for m in active_mistakes])) + context_string = ", ".join(weak_topics) if weak_topics else "General Syllabus" + + # 2. Construct the highly-specific System Prompt + system_prompt = f""" + You are PragatiVeer's elite AI Tutor for JEE & NEET aspirants. + Your goal is to guide students to the answer, not just give it to them. + + STUDENT CONTEXT: + This student currently struggles with the following topics: {context_string}. + Keep this in mind and relate explanations back to foundational concepts in these areas if applicable. + + RULES: + 1. Respond in the student's preferred language (English or Hindi). + 2. Use LaTeX wrapped in single $ for inline math and $$ for block equations. + 3. Keep responses concise and focused on problem-solving steps. + """ + + # 3. Call the LLM + response = await client.chat.completions.create( + model=os.getenv("LLM_MODEL_NAME", "gemma"), + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message} + ], + stream=True, + ) + + # 4. Yield tokens as they arrive + async for chunk in response: + if chunk.choices[0].delta.content is not None: + yield chunk.choices[0].delta.content diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..f20490c --- /dev/null +++ b/backend/main.py @@ -0,0 +1,490 @@ +from fastapi import FastAPI, Depends, HTTPException, status, Request +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from sqlmodel import Session, select +from contextlib import asynccontextmanager +from typing import List, Dict, Any, Optional +from uuid import UUID +from datetime import datetime, timedelta +from pydantic import BaseModel + +from database import init_db, get_session +import models +from ai_coach import calculate_sm2 +from analytics import generate_parent_report +from billing import generate_mock_order, verify_webhook_signature +from llm import stream_tutor_response +from analytics import generate_parent_report +from predictor import calculate_readiness_score +from growth import generate_referral_link, process_referral_reward + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Initialize database tables locally when the container boots up + init_db() + yield + +app = FastAPI( + title="PragatiVeer Core Engine", + version="1.0.0", + lifespan=lifespan +) + +# Enable immediate communication with your Next.js frontend local environment +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/health", tags=["System"]) +def health_check(): + return {"status": "operational", "engine": "FastAPI Async", "environment": "local_dev"} + +# --- ACCESS CONTROL DEPENDENCY --- + +def require_tier(allowed_tiers: List[models.TierEnum]): + """ + Dependency to block unauthorized access to premium routes. + """ + def tier_checker(user_id: UUID, session: Session = Depends(get_session)): + user = session.get(models.User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found.") + + if user.tier not in allowed_tiers: + raise HTTPException( + status_code=403, + detail=f"Access denied. Requires one of: {[t.value for t in allowed_tiers]}" + ) + return user + return tier_checker + +# --- BASELINE AUTHENTICATION MOCKS FOR FRONTEND UI VERIFICATION --- + +@app.post("/api/auth/otp-request", tags=["Authentication"]) +def request_otp(phone_or_email: str): + # Mocking execution loop for user onboarding flow + return {"message": "Verification code dispatched successfully", "debug_otp": "123456"} + +@app.post("/api/auth/otp-verify", tags=["Authentication"]) +def verify_otp(phone_or_email: str, code: str, session: Session = Depends(get_session)): + # Add your logic matching the registration & profile onboarding flow later + return {"status": "authenticated", "token": "mock_jwt_session_token_string"} + +# --- CBT TEST ENVIRONMENT CONTENT DELIVERY ENDPOINTS --- + +@app.get("/api/tests/{test_id}", response_model=models.Test, tags=["CBT Engine"]) +def get_test_by_id(test_id: UUID, user_id: UUID, session: Session = Depends(get_session), current_user = Depends(require_tier([models.TierEnum.PRO, models.TierEnum.AI_COACH]))): + test = session.get(models.Test, test_id) + if not test: + raise HTTPException(status_code=404, detail="Requested examination structure not found.") + return test + +@app.get("/api/questions/{question_id}", response_model=models.Question, tags=["CBT Engine"]) +def get_question_by_id(question_id: UUID, session: Session = Depends(get_session)): + question = session.get(models.Question, question_id) + if not question: + raise HTTPException(status_code=404, detail="Requested question not found.") + return question + +@app.post("/api/attempts/submit", response_model=models.Attempt, tags=["CBT Engine"]) +def submit_test_attempt(payload: Dict[str, Any], session: Session = Depends(get_session)): + user_id = payload.get("user_id") + source_type = payload.get("source_type") + source_id = payload.get("source_id") + client_answers = payload.get("answers", []) # List of dicts: {"question_id": "...", "selected_answer": "..."} + + if not user_id or not source_id: + raise HTTPException(status_code=400, detail="Missing required deployment metadata parameters.") + + # 0. Enforce Timer Integrity: Verify that the test window is still active + if source_type == "Test": + test = session.get(models.Test, source_id) + if test and test.scheduled_date: + # Removed strict time cutoff for development/testing + pass + + # 1. Initialize structural baseline metrics + total_questions = len(client_answers) + correct_count = 0 + processed_answers = [] + + # Extract pre-generated attempt ID or create a new one + attempt_id_str = payload.get("id") + import uuid + attempt_id = uuid.UUID(attempt_id_str) if attempt_id_str else uuid.uuid4() + + # 2. Secure Server-Side Evaluation Loop + for item in client_answers: + q_id = item.get("question_id") + selected = item.get("selected_answer") + + # Fetch actual answer key securely from PostgreSQL (Do not trust frontend state values) + question = session.get(models.Question, q_id) + if not question: + continue + + # Check correctness against JSON database configurations + is_correct = question.correct_answer.get("correct") == selected + if is_correct: + correct_count += 1 + + processed_answers.append({ + "question_id": str(q_id), + "selected_answer": selected, + "is_correct": is_correct, + "time_spent": item.get("time_spent", 0) + }) + + # 3. AI Coach Processing: If the answer is incorrect, extract it to the Mistake queue + if not is_correct: + # Check if this user has failed this specific question before to track execution loops + existing_mistake = session.exec( + select(models.Mistake).where( + models.Mistake.user_id == user_id, + models.Mistake.question_id == q_id + ) + ).first() + + if existing_mistake: + # User failed it again: reset repetitions, assign low quality score (q=1) + interval, ef = calculate_sm2(q=1, repetitions=existing_mistake.review_count, ease_factor=2.5, previous_interval=1) + existing_mistake.review_count += 1 + existing_mistake.next_review = datetime.utcnow() + timedelta(days=interval) + existing_mistake.consecutive_correct = 0 + session.add(existing_mistake) + else: + # Brand new mistake: enter into the spaced repetition queue for the first time + new_mistake = models.Mistake( + user_id=user_id, + question_id=q_id, + attempt_id=attempt_id, # Linking back to original source file attempt mapping + error_type=models.ErrorTypeEnum.CONCEPTUAL, # Defaults to conceptual until AI tags it + topic=question.topic, + subtopic=question.subtopic, + next_review=datetime.utcnow() + timedelta(days=1), # Schedule for tomorrow review loop + review_count=1, + consecutive_correct=0, + is_mastered=False + ) + session.add(new_mistake) + + # 4. Save Core Attempt Records + accuracy = (correct_count / total_questions) * 100 if total_questions > 0 else 0.0 + + attempt_record = models.Attempt( + id=attempt_id, + user_id=user_id, + source_type=source_type, + source_id=source_id, + started_at=datetime.utcnow() - timedelta(seconds=payload.get("time_taken", 0)), + submitted_at=datetime.utcnow(), + time_taken=payload.get("time_taken", 0), + answers=processed_answers, + score=correct_count, + total_questions=total_questions, + accuracy=accuracy, + status="Submitted" + ) + + session.add(attempt_record) + session.commit() + session.refresh(attempt_record) + + return attempt_record + +# --- AI COACH SPАСED REPETITION ENDPOINTS --- + +@app.get("/api/mistakes/today", tags=["AI Coach"]) +def get_todays_mistakes(user_id: UUID, session: Session = Depends(get_session)): + # Retrieve all un-mastered mistakes for the user scheduled on or before today + now = datetime.utcnow() + statement = select(models.Mistake, models.Question).join( + models.Question, models.Mistake.question_id == models.Question.id + ).where( + models.Mistake.user_id == user_id, + models.Mistake.is_mastered == False, + models.Mistake.next_review <= now + ) + results = session.exec(statement).all() + + response_data = [] + for mistake, question in results: + response_data.append({ + "mistake": mistake, + "question": question + }) + return response_data + +@app.post("/api/mistakes/review", tags=["AI Coach"]) +def submit_mistake_review(payload: Dict[str, Any], session: Session = Depends(get_session)): + mistake_id = payload.get("mistake_id") + selected_answer = payload.get("selected_answer") + + if not mistake_id or not selected_answer: + raise HTTPException(status_code=400, detail="Missing mistake_id or selected_answer") + + mistake = session.get(models.Mistake, mistake_id) + if not mistake: + raise HTTPException(status_code=404, detail="Mistake not found.") + + question = session.get(models.Question, mistake.question_id) + + is_correct = question.correct_answer.get("correct") == selected_answer + + # Calculate intervals + q_value = 4 if is_correct else 1 + + # Very basic interval calculation for demo + days_since_creation = (datetime.utcnow() - mistake.created_at).days + prev_interval = max(1, days_since_creation) + + interval, ef = calculate_sm2(q=q_value, repetitions=mistake.review_count, ease_factor=2.5, previous_interval=prev_interval) + + if is_correct: + mistake.consecutive_correct += 1 + if mistake.consecutive_correct >= 3: + mistake.is_mastered = True + else: + mistake.consecutive_correct = 0 + + mistake.review_count += 1 + mistake.next_review = datetime.utcnow() + timedelta(days=interval) + + session.add(mistake) + session.commit() + session.refresh(mistake) + + return {"mistake": mistake, "is_correct": is_correct, "correct_answer": question.correct_answer.get("correct")} + +# --- PARENT SUBSYSTEM ENDPOINTS --- + +@app.post("/api/parent/register", response_model=models.Parent, tags=["Parent Subsystem"]) +def register_parent(parent_data: models.Parent, session: Session = Depends(get_session)): + # Persist parent baseline identity profile + session.add(parent_data) + session.commit() + session.refresh(parent_data) + return parent_data + +@app.post("/api/parent/link-student", tags=["Parent Subsystem"]) +def link_student_to_parent(parent_id: UUID, student_phone_or_email: str, session: Session = Depends(get_session)): + parent = session.get(models.Parent, parent_id) + if not parent: + raise HTTPException(status_code=404, detail="Parent account context not found.") + + # Locate the target student account by phone or email criteria + student = session.exec( + select(models.User).where( + (models.User.phone == student_phone_or_email) | + (models.User.email == student_phone_or_email) + ) + ).first() + + if not student: + raise HTTPException(status_code=404, detail="Target student profile could not be verified.") + + # Append the student's UUID string to the parent's linked array + current_links = list(parent.linked_students or []) + if str(student.id) not in current_links: + current_links.append(str(student.id)) + parent.linked_students = current_links + session.add(parent) + session.commit() + + return {"message": "Student successfully linked to parent context.", "linked_student_id": student.id} + +@app.get("/api/parent/{parent_id}/dashboard/{student_id}", tags=["Parent Subsystem"]) +def get_parent_dashboard_metrics(parent_id: UUID, student_id: UUID, session: Session = Depends(get_session)): + parent = session.get(models.Parent, parent_id) + if not parent: + raise HTTPException(status_code=404, detail="Unauthorized parent profile access attempt.") + + # Security check: Ensure this parent is explicitly authorized to view this student's data + if str(student_id) not in (parent.linked_students or []): + raise HTTPException(status_code=403, detail="Access denied to requested student dashboard data.") + + report = generate_parent_report(student_id, session) + return report + + + +# --- BILLING & SUBSCRIPTION ENDPOINTS --- + +@app.post("/api/billing/create-order", tags=["Subscription Engine"]) +def create_subscription_order(user_id: UUID, plan_tier: str, session: Session = Depends(get_session)): + user = session.get(models.User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found.") + + # Match PRD pricing parameters (amounts in paise) + pricing_map = { + "Pro": 29900, # ₹299/mo + "Al_Coach": 59900 # ₹599/mo + } + + amount = pricing_map.get(plan_tier) + if not amount: + raise HTTPException(status_code=400, detail="Invalid subscription tier requested.") + + order = generate_mock_order(user_id, plan_tier, amount) + return {"order_id": order["id"], "amount": amount, "currency": "INR"} + +@app.post("/api/billing/webhook/razorpay", tags=["Subscription Engine"]) +async def razorpay_webhook_handler(request: Request, session: Session = Depends(get_session)): + """ + Receives server-to-server notifications when a payment succeeds. + """ + payload_bytes = await request.body() + payload_str = payload_bytes.decode('utf-8') + signature = request.headers.get("X-Razorpay-Signature", "") + + # Security Check: Verify signature + if not verify_webhook_signature(payload_str, signature): + raise HTTPException(status_code=400, detail="Invalid webhook signature.") + + import json + data = json.loads(payload_str) + + # Process the successful payment event + if data.get("event") == "payment.captured": + notes = data["payload"]["payment"]["entity"]["notes"] + user_id = notes.get("user_id") + tier_requested = notes.get("tier_requested") + + user = session.get(models.User, user_id) + if user and tier_requested: + # Upgrade the user's tier in the database + user.tier = models.TierEnum(tier_requested) + session.add(user) + + # Log the subscription record + from datetime import timedelta + new_sub = models.Subscription( + user_id=user.id, + tier=models.TierEnum(tier_requested), + billing_cycle="Monthly", + start_date=datetime.utcnow(), + end_date=datetime.utcnow() + timedelta(days=30), + amount=data["payload"]["payment"]["entity"]["amount"], + payment_id=data["payload"]["payment"]["entity"]["id"], + status="Active" + ) + session.add(new_sub) + session.commit() + + return {"status": "Webhook processed successfully"} + +# --- STUDY PLAN ENDPOINTS --- +from datetime import date + +@app.get("/api/ai-coach/study-plan/today", tags=["AI Coach"]) +def get_todays_study_plan(user_id: UUID, session: Session = Depends(get_session), current_user = Depends(require_tier([models.TierEnum.AI_COACH]))): + today = datetime.utcnow().date() + plan = session.exec( + select(models.StudyPlan) + .where(models.StudyPlan.user_id == user_id) + .where(models.StudyPlan.date == today) + ).first() + + if not plan: + raise HTTPException(status_code=404, detail="No study plan generated for today yet.") + + return plan + +class ChatRequest(BaseModel): + user_id: UUID + message: str + +@app.post("/api/ai-coach/chat/stream", tags=["AI Coach"]) +async def ai_tutor_chat_stream(request: ChatRequest, session: Session = Depends(get_session)): + """ + Streams the context-aware LLM response back to the client. + """ + user = session.get(models.User, request.user_id) + if not user: + raise HTTPException(status_code=404, detail="Student profile not found.") + if user.tier != models.TierEnum.AI_COACH: + raise HTTPException(status_code=403, detail="Access denied. Gated to AI_COACH tier.") + + # Enforce Daily Limit (50 messages) as per PRD A-04 + # (In a full production build, you'd increment a Redis counter here) + + return StreamingResponse( + stream_tutor_response(request.user_id, request.message, session), + media_type="text/event-stream" + ) + +@app.get("/api/ai-coach/readiness/{user_id}", tags=["AI Coach"]) +def get_exam_readiness(user_id: UUID, session: Session = Depends(get_session), current_user = Depends(require_tier([models.TierEnum.AI_COACH]))): + """ + Returns the dynamic 0-100 Exam Readiness Score and College Prediction. + """ + readiness_data = calculate_readiness_score(user_id, session) + if not readiness_data: + raise HTTPException(status_code=404, detail="Could not calculate readiness score.") + + return readiness_data + +@app.get("/api/growth/referral/{user_id}", tags=["Viral Growth"]) +def get_referral_dashboard(user_id: UUID, session: Session = Depends(get_session)): + user = session.get(models.User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found.") + + link = generate_referral_link(user_id) + + return { + "referral_link": link, + "total_referrals": 0, # In production, query a referral tracking table + "rewards_earned_days": 0, + "share_message": f"I'm on a {user.streak_count}-day study streak on PragatiVeer! Use my link to get 7 days of the AI Coach for free: {link}" + } + +# --- USER CRUD & TEST LISTING --- +@app.get("/api/users/{user_id}", response_model=models.User, tags=["Users"]) +def get_user_profile(user_id: UUID, session: Session = Depends(get_session)): + user = session.get(models.User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + +class UserUpdate(BaseModel): + name: Optional[str] = None + target_exam: Optional[models.ExamEnum] = None + student_class: Optional[models.ClassEnum] = None + +@app.put("/api/users/{user_id}", response_model=models.User, tags=["Users"]) +def update_user_profile(user_id: UUID, payload: UserUpdate, session: Session = Depends(get_session)): + user = session.get(models.User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + if payload.name is not None: + user.name = payload.name + if payload.target_exam is not None: + user.target_exam = payload.target_exam + if payload.student_class is not None: + # Note: mapping alias 'class' in User class to 'student_class' + user.student_class = payload.student_class + + session.add(user) + session.commit() + session.refresh(user) + return user + +@app.get("/api/tests", response_model=List[models.Test], tags=["CBT Engine"]) +def list_tests(exam_pattern: Optional[models.ExamEnum] = None, session: Session = Depends(get_session)): + query = select(models.Test) + if exam_pattern is not None: + query = query.where(models.Test.exam_pattern == exam_pattern) + return session.exec(query).all() + +@app.get("/api/users/{user_id}/attempts", tags=["Users"]) +def get_user_attempts(user_id: UUID, session: Session = Depends(get_session)): + query = select(models.Attempt).where(models.Attempt.user_id == user_id) + return session.exec(query).all() + diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..9a5a98d --- /dev/null +++ b/backend/models.py @@ -0,0 +1,168 @@ +from datetime import datetime, date +from enum import Enum +from typing import List, Optional, Dict, Any +from uuid import UUID, uuid4 +from sqlmodel import Field, SQLModel, Relationship, JSON + +# --- ENUMS --- +class ClassEnum(str, Enum): + ELEVENTH = "11" + TWELFTH = "12" + DROPPER = "Dropper" + +class ExamEnum(str, Enum): + JEE_MAIN = "JEE_Main" + JEE_ADVANCED = "JEE_Advanced" + NEET = "NEET" + +class MediumEnum(str, Enum): + ENGLISH = "English" + HINDI = "Hindi" + BILINGUAL = "Bilingual" + +class TierEnum(str, Enum): + FREE = "Free" + PRO = "Pro" + AI_COACH = "Al_Coach" + +class QuestionTypeEnum(str, Enum): + MCQ_SINGLE = "MCQ_Single" + MCQ_MULTIPLE = "MCQ_Multiple" + NUMERICAL = "Numerical" + +class SourceTypeEnum(str, Enum): + DPS = "DPS" + TEST = "Test" + MICROTEST = "MicroTest" + +class ErrorTypeEnum(str, Enum): + CONCEPTUAL = "Conceptual" + CALCULATION = "Calculation" + READING = "Reading" + TIME_PRESSURE = "Time_Pressure" + +# --- CORE DB MODELS --- + +class User(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + email: Optional[str] = Field(default=None, unique=True) + phone: Optional[str] = Field(default=None, unique=True) + password_hash: str + name: str + student_class: ClassEnum = Field(alias="class") + target_exam: ExamEnum + medium: MediumEnum + school: Optional[str] = None + city: str + state: str + tier: TierEnum = Field(default=TierEnum.FREE) + trial_end: Optional[datetime] = None + subscription_end: Optional[datetime] = None + streak_count: int = Field(default=0) + longest_streak: int = Field(default=0) + created_at: datetime = Field(default_factory=datetime.utcnow) + last_active: datetime = Field(default_factory=datetime.utcnow) + +class Parent(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + email: str + phone: str + password_hash: str + name: str + language: str = Field(default="English") # e.g., Hindi, English + linked_students: List[str] = Field(default=[], sa_type=JSON) + created_at: datetime = Field(default_factory=datetime.utcnow) + +class Question(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + subject: str # Physics, Chemistry, Maths, Biology + topic: str + subtopic: str + difficulty: str # Easy, Medium, Hard + type: QuestionTypeEnum + question_text: str + question_text_hi: Optional[str] = None + options: Optional[Dict[str, Any]] = Field(default=None, sa_type=JSON) + options_hi: Optional[Dict[str, Any]] = Field(default=None, sa_type=JSON) + correct_answer: Dict[str, Any] = Field(sa_type=JSON) + solution_text: str + solution_text_hi: Optional[str] = None + language: MediumEnum + year: Optional[int] = None + exam: Optional[ExamEnum] = None + tags: Optional[str] = None + is_active: bool = Field(default=True) + created_by: UUID + verified_by: Optional[UUID] = None + +class DPS(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + date: date + title: str + questions: List[str] = Field(default=[], sa_type=JSON) # Array of question UUID strings + difficulty: str + time_limit: int + is_active: bool = Field(default=True) + created_by: UUID + +class Test(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + title: str + type: str # Weekly, Mock, PYQ + exam_pattern: ExamEnum + sections: Dict[str, Any] = Field(sa_type=JSON) # Array of {name, questions[], time_limit} + total_time: int + difficulty: str + scheduled_date: Optional[datetime] = None + is_active: bool = Field(default=True) + created_by: UUID + +class Attempt(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + user_id: UUID = Field(foreign_key="user.id") + source_type: SourceTypeEnum + source_id: UUID + started_at: datetime + submitted_at: Optional[datetime] = None + time_taken: Optional[int] = None # in seconds + answers: Optional[Dict[str, Any]] = Field(default=None, sa_type=JSON) # Array of {question_id, selected_answer, is_correct, time_spent} + score: int = Field(default=0) + total_questions: int + accuracy: float = Field(default=0.0) + status: str = Field(default="In_Progress") # In_Progress, Submitted, Timed_Out + +class Mistake(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + user_id: UUID = Field(foreign_key="user.id") + question_id: UUID = Field(foreign_key="question.id") + attempt_id: UUID = Field(foreign_key="attempt.id") + error_type: ErrorTypeEnum + topic: str + subtopic: str + next_review: datetime + review_count: int = Field(default=0) + consecutive_correct: int = Field(default=0) + is_mastered: bool = Field(default=False) + created_at: datetime = Field(default_factory=datetime.utcnow) + +class StudyPlan(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + user_id: UUID = Field(foreign_key="user.id") + date: date + tasks: Dict[str, Any] = Field(sa_type=JSON) # Array of {type, description, duration_minutes, status} + is_completed: bool = Field(default=False) + generated_at: datetime = Field(default_factory=datetime.utcnow) + feedback: Optional[str] = None + +class Subscription(SQLModel, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True) + user_id: UUID = Field(foreign_key="user.id") + tier: TierEnum + billing_cycle: str # Monthly, Quarterly, Annual + start_date: datetime + end_date: datetime + amount: int # Stored in Paise (INR) + payment_id: str # Razorpay order ID / payment ID + status: str # Active, Expired, Cancelled, Paused + auto_renew: bool = Field(default=True) + coupon_code: Optional[str] = None diff --git a/backend/predictor.py b/backend/predictor.py new file mode 100644 index 0000000..eedce53 --- /dev/null +++ b/backend/predictor.py @@ -0,0 +1,76 @@ +from sqlmodel import Session, select +from uuid import UUID +import models + +def calculate_readiness_score(user_id: UUID, session: Session) -> dict: + """ + Calculates the Exam Readiness Score (0-100) based on PRD A-05 parameters. + Weights: Accuracy (40%), Speed (20%), Syllabus Coverage (20%), + Consistency (10%), Mistake Recovery (10%). + """ + user = session.get(models.User, user_id) + if not user: + return {} + + attempts = session.exec( + select(models.Attempt).where(models.Attempt.user_id == user_id) + ).all() + + if not attempts: + return {"score": 0, "status": "Not enough data"} + + # 1. Accuracy (40% Weight) + avg_accuracy = sum(a.accuracy for a in attempts) / len(attempts) + w_accuracy = (avg_accuracy / 100) * 40 + + # 2. Speed (20% Weight) - Assuming benchmark is 120s per question + time_per_q = [ (a.time_taken / a.total_questions) for a in attempts if a.total_questions > 0 and a.time_taken ] + avg_time = sum(time_per_q) / len(time_per_q) if time_per_q else 120 + speed_factor = min(1.0, 120 / max(avg_time, 1)) # Cap at 1.0 + w_speed = speed_factor * 20 + + # 3. Consistency (10% Weight) + streak = min(user.streak_count, 30) # Max consistency score hit at 30 days + w_consistency = (streak / 30) * 10 + + # 4. Mistake Recovery (10% Weight) + mistakes = session.exec(select(models.Mistake).where(models.Mistake.user_id == user_id)).all() + mastered_count = sum(1 for m in mistakes if m.is_mastered) + total_mistakes = max(len(mistakes), 1) + w_recovery = (mastered_count / total_mistakes) * 10 + + # 5. Syllabus Coverage (20% Weight) + # For local sandbox, we will estimate based on total questions attempted + total_q_attempted = sum(a.total_questions for a in attempts) + coverage_factor = min(total_q_attempted / 500, 1.0) # Assume 500 questions = decent baseline coverage + w_coverage = coverage_factor * 20 + + # Total Calculation + total_score = round(w_accuracy + w_speed + w_consistency + w_recovery + w_coverage) + + # Calculate College Prediction based on PRD A-07 + prediction = "Keep Practicing" + target_type = "State Level" + + if total_score >= 85: + prediction = "IIT Bombay / IIT Delhi within reach!" + target_type = "Reach" + elif total_score >= 70: + prediction = "NIT Trichy / NIT Surathkal on track." + target_type = "Target" + elif total_score >= 50: + prediction = "Top GFTIs / State Engineering Colleges." + target_type = "Safety" + + return { + "score": total_score, + "breakdown": { + "accuracy": round(w_accuracy, 1), + "speed": round(w_speed, 1), + "consistency": round(w_consistency, 1), + "recovery": round(w_recovery, 1), + "coverage": round(w_coverage, 1) + }, + "college_prediction": prediction, + "prediction_category": target_type + } diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..de97ce3 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,12 @@ +fastapi==0.110.0 +uvicorn[standard]==0.28.0 +sqlmodel==0.0.16 +psycopg2-binary==2.9.9 +redis==5.0.3 +pydantic==2.6.4 +pydantic-settings==2.2.1 +bcrypt==4.1.2 +pyjwt==2.8.0 +celery==5.3.6 +openai==1.14.0 +httpx==0.27.2 diff --git a/backend/seed.py b/backend/seed.py new file mode 100644 index 0000000..a4f9722 --- /dev/null +++ b/backend/seed.py @@ -0,0 +1,317 @@ +from sqlmodel import Session, create_engine, select +from uuid import uuid4, UUID +from datetime import datetime +import os + +from database import engine, init_db +import models + +def seed_local_database(): + # Ensure tables are created + init_db() + + with Session(engine) as session: + # Prevent double seeding in local testing environment + if session.exec(select(models.Question)).first() is not None: + print("Local environment database already contains data components. Dropping all and reseeding...") + import sqlmodel + sqlmodel.SQLModel.metadata.drop_all(engine) + init_db() + + print("Seeding baseline content entities...") + + # 0. Create the baseline student user (seeded target: JEE_MAIN) + student_id = UUID("b0e17378-864d-439e-8806-708dc48a891b") + student_user = models.User( + id=student_id, + phone="9999999999", + email="student@pragativeer.com", + password_hash="mock_hash", + name="Rahul Sharma", + student_class=models.ClassEnum.TWELFTH, + medium=models.MediumEnum.ENGLISH, + target_exam=models.ExamEnum.JEE_MAIN, + city="New Delhi", + state="Delhi", + tier=models.TierEnum.AI_COACH, + streak_count=5, + last_active=datetime.utcnow() + ) + session.add(student_user) + + admin_id = uuid4() + + jee_test_id = UUID("b6faa295-3bf5-4de2-a587-d72b889629c4") + neet_test_id = UUID("e7c80521-825b-4c07-889a-ee646c05d762") + + def generate_dummy_questions(subject: str, count: int, exam: models.ExamEnum, admin_id: UUID, start_idx: int = 1): + qs = [] + for i in range(start_idx, count + start_idx): + q = models.Question( + id=uuid4(), + subject=subject, + topic=f"Generic Topic {i}", + subtopic=f"Subtopic {i}", + difficulty="Medium", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text=f"This is a simulated {exam.value} {subject} question #{i} for mock exam scaling.", + question_text_hi=f"यह एक अनुरूपित {exam.value} {subject} प्रश्न #{i} है।", + options={"A": "Option A", "B": "Option B", "C": "Option C", "D": "Option D"}, + options_hi={"A": "विकल्प A", "B": "विकल्प B", "C": "विकल्प C", "D": "विकल्प D"}, + correct_answer={"correct": "A"}, + solution_text="Simulated solution for the mock test.", + solution_text_hi="मॉक टेस्ट के लिए अनुरूपित समाधान।", + language=models.MediumEnum.BILINGUAL, + year=2024, + exam=exam, + created_by=admin_id + ) + qs.append(q) + return qs + + # --- JEE QUESTIONS --- + + # Physics 1 + jq1 = models.Question( + id=uuid4(), + subject="Physics", + topic="Electrostatics", + subtopic="Coulomb's Law", + difficulty="Medium", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text="Two point charges placed at a distance $r$ in air exert a force $F$ on each other. At what distance will they exert the same force if placed in a medium of dielectric constant $K=4$?", + question_text_hi="वायु में $r$ दूरी पर स्थित दो बिंदु आवेश एक दूसरे पर $F$ बल लगाते हैं। यदि उन्हें $K=4$ परावैद्युतांक वाले माध्यम में रखा जाए, तो वे किस दूरी पर समान बल लगाएंगे?", + options={"A": "$r$", "B": "$r/2$", "C": "$2r$", "D": "$r/4$"}, + options_hi={"A": "$r$", "B": "$r/2$", "C": "$2r$", "D": "$r/4$"}, + correct_answer={"correct": "B"}, + solution_text="Force in medium $F' = \\frac{F}{K}$. To maintain same force, $r' = \\frac{r}{\\sqrt{K}} = \\frac{r}{\\sqrt{4}} = \\frac{r}{2}$.", + solution_text_hi="माध्यम में बल $F' = \\frac{F}{K}$। समान बल बनाए रखने के लिए, $r' = \\frac{r}{\\sqrt{K}} = \\frac{r}{\\sqrt{4}} = \\frac{r}{2}$।", + language=models.MediumEnum.BILINGUAL, + year=2024, + exam=models.ExamEnum.JEE_MAIN, + created_by=admin_id + ) + + # Physics 2 + jq2 = models.Question( + id=uuid4(), + subject="Physics", + topic="Mechanics", + subtopic="Rotational Motion", + difficulty="Hard", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text="A solid cylinder of mass $M$ and radius $R$ is rolling without slipping down an inclined plane of angle $\\theta$. What is the minimum coefficient of static friction $\\mu_s$ required to prevent slipping?", + question_text_hi="द्रव्यमान $M$ और त्रिज्या $R$ का एक ठोस बेलन कोण $\\theta$ के एक झुके हुए तल पर बिना फिसले लुढ़क रहा है। फिसलने से रोकने के लिए आवश्यक न्यूनतम स्थैतिक घर्षण गुणांक $\\mu_s$ क्या है?", + options={"A": "$\\frac{1}{3} \\tan \\theta$", "B": "$\\frac{2}{3} \\tan \\theta$", "C": "$\\frac{1}{2} \\tan \\theta$", "D": "$\\tan \\theta$"}, + options_hi={"A": "$\\frac{1}{3} \\tan \\theta$", "B": "$\\frac{2}{3} \\tan \\theta$", "C": "$\\frac{1}{2} \\tan \\theta$", "D": "$\\tan \\theta$"}, + correct_answer={"correct": "A"}, + solution_text="$a = \\frac{g \\sin \\theta}{1 + \\frac{k^2}{R^2}}$. For solid cylinder $k^2 = \\frac{R^2}{2}$. So $a = \\frac{2}{3} g \\sin \\theta$. Friction $f = Mg \\sin \\theta - Ma = \\frac{1}{3} Mg \\sin \\theta$. Since $f \\le \\mu_s Mg \\cos \\theta$, $\\mu_s \\ge \\frac{1}{3} \\tan \\theta$.", + solution_text_hi="$a = \\frac{g \\sin \\theta}{1 + \\frac{k^2}{R^2}}$। ठोस बेलन के लिए $k^2 = \\frac{R^2}{2}$। इसलिए $a = \\frac{2}{3} g \\sin \\theta$। घर्षण $f = Mg \\sin \\theta - Ma = \\frac{1}{3} Mg \\sin \\theta$। चूंकि $f \\le \\mu_s Mg \\cos \\theta$, $\\mu_s \\ge \\frac{1}{3} \\tan \\theta$ है।", + language=models.MediumEnum.BILINGUAL, + year=2023, + exam=models.ExamEnum.JEE_MAIN, + created_by=admin_id + ) + + # Chemistry 1 + jq3 = models.Question( + id=uuid4(), + subject="Chemistry", + topic="Physical Chemistry", + subtopic="Chemical Kinetics", + difficulty="Medium", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text="For a first-order reaction, the time required for $99\\%$ completion is $x$ times the time required for $90\\%$ completion. The value of $x$ is:", + question_text_hi="एक प्रथम-कोटि की अभिक्रिया के लिए, $99\\%$ पूर्ण होने में लगा समय $90\\%$ पूर्ण होने में लगे समय का $x$ गुना है। $x$ का मान है:", + options={"A": "$2$", "B": "$10$", "C": "$3$", "D": "$4$"}, + options_hi={"A": "$2$", "B": "$10$", "C": "$3$", "D": "$4$"}, + correct_answer={"correct": "A"}, + solution_text="$t = \\frac{2.303}{k} \\log \\frac{100}{100-x}$. $t_{99} = \\frac{2.303}{k} \\log 100 = 2 \\times \\frac{2.303}{k}$. $t_{90} = \\frac{2.303}{k} \\log 10 = 1 \\times \\frac{2.303}{k}$. Therefore $x = 2$.", + solution_text_hi="$t = \\frac{2.303}{k} \\log \\frac{100}{100-x}$। $t_{99} = \\frac{2.303}{k} \\log 100 = 2 \\times \\frac{2.303}{k}$। $t_{90} = \\frac{2.303}{k} \\log 10 = 1 \\times \\frac{2.303}{k}$। इसलिए $x = 2$ है।", + language=models.MediumEnum.BILINGUAL, + year=2024, + exam=models.ExamEnum.JEE_MAIN, + created_by=admin_id + ) + + # Maths 1 + jq4 = models.Question( + id=uuid4(), + subject="Mathematics", + topic="Calculus", + subtopic="Definite Integration", + difficulty="Hard", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text="The value of the integral $\\int_{0}^{\\pi/2} \\frac{\\sin^3 x}{\\sin^3 x + \\cos^3 x} dx$ is:", + question_text_hi="समाकल $\\int_{0}^{\\pi/2} \\frac{\\sin^3 x}{\\sin^3 x + \\cos^3 x} dx$ का मान है:", + options={"A": "$\\pi/2$", "B": "$\\pi/4$", "C": "$0$", "D": "$\\pi/8$"}, + options_hi={"A": "$\\pi/2$", "B": "$\\pi/4$", "C": "$0$", "D": "$\\pi/8$"}, + correct_answer={"correct": "B"}, + solution_text="Let $I = \\int_{0}^{\\pi/2} \\frac{\\sin^3 x}{\\sin^3 x + \\cos^3 x} dx$. Using $\\int_0^a f(x)dx = \\int_0^a f(a-x)dx$, we get $I = \\int_{0}^{\\pi/2} \\frac{\\cos^3 x}{\\cos^3 x + \\sin^3 x} dx$. Adding both, $2I = \\int_{0}^{\\pi/2} 1 dx = \\pi/2$. Thus $I = \\pi/4$.", + solution_text_hi="माना $I = \\int_{0}^{\\pi/2} \\frac{\\sin^3 x}{\\sin^3 x + \\cos^3 x} dx$। गुणधर्म $\\int_0^a f(x)dx = \\int_0^a f(a-x)dx$ का उपयोग करने पर, $I = \\int_{0}^{\\pi/2} \\frac{\\cos^3 x}{\\cos^3 x + \\sin^3 x} dx$। दोनों को जोड़ने पर, $2I = \\int_{0}^{\\pi/2} 1 dx = \\pi/2$। इसलिए $I = \\pi/4$ है।", + language=models.MediumEnum.BILINGUAL, + year=2023, + exam=models.ExamEnum.JEE_MAIN, + created_by=admin_id + ) + + jee_physics_dummies = generate_dummy_questions("Physics", 28, models.ExamEnum.JEE_MAIN, admin_id, start_idx=3) + jee_chem_dummies = generate_dummy_questions("Chemistry", 29, models.ExamEnum.JEE_MAIN, admin_id, start_idx=2) + jee_math_dummies = generate_dummy_questions("Mathematics", 29, models.ExamEnum.JEE_MAIN, admin_id, start_idx=2) + + session.add_all([jq1, jq2, jq3, jq4]) + session.add_all(jee_physics_dummies) + session.add_all(jee_chem_dummies) + session.add_all(jee_math_dummies) + + # --- NEET QUESTIONS --- + + # Biology 1 + nq1 = models.Question( + id=uuid4(), + subject="Botany", + topic="Genetics", + subtopic="Translation", + difficulty="Easy", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text="Which of the following codons codes for Methionine and also acts as an initiator codon?", + question_text_hi="निम्नलिखित में से कौन सा कोडॉन मेथियोनिन के लिए कोड करता है और एक प्रारंभक कोडॉन के रूप में भी कार्य करता है?", + options={"A": "AUG", "B": "GUG", "C": "UAA", "D": "UAG"}, + options_hi={"A": "AUG", "B": "GUG", "C": "UAA", "D": "UAG"}, + correct_answer={"correct": "A"}, + solution_text="AUG is the starting codon (initiator codon) in eukaryotes and prokaryotes and also codes for the amino acid methionine.", + solution_text_hi="AUG यूकेरियोट्स और प्रोकैरियोट्स में प्रारंभिक कोडॉन (प्रारंभक कोडॉन) है और अमीनो एसिड मेथियोनिन के लिए भी कोड करता है।", + language=models.MediumEnum.BILINGUAL, + year=2024, + exam=models.ExamEnum.NEET, + created_by=admin_id + ) + + # Physics 1 (NEET) + nq2 = models.Question( + id=uuid4(), + subject="Physics", + topic="Kinematics", + subtopic="Motion under Gravity", + difficulty="Medium", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text="A ball is thrown vertically upward with a velocity of $20\\text{ m/s}$ from the top of a tower. If the height of the tower is $25\\text{ m}$, the time taken by the ball to reach the ground is (take $g = 10\\text{ m/s}^2$):", + question_text_hi="एक मीनार के शीर्ष से एक गेंद को $20\\text{ मी/से}$ के वेग से लंबवत ऊपर की ओर फेंका जाता है। यदि मीनार की ऊंचाई $25\\text{ मीटर}$ है, तो गेंद को जमीन पर पहुंचने में लगा समय है ($g = 10\\text{ मी/से}^2$ लें):", + options={"A": "$5\\text{ s}$", "B": "$3\\text{ s}$", "C": "$4\\text{ s}$", "D": "$2\\text{ s}$"}, + options_hi={"A": "$5\\text{ सेकंड}$", "B": "$3\\text{ सेकंड}$", "C": "$4\\text{ सेकंड}$", "D": "$2\\text{ सेकंड}$"}, + correct_answer={"correct": "A"}, + solution_text="Using $s = ut + \\frac{1}{2}at^2$. Taking upward direction positive: $-25 = 20t - \\frac{1}{2}(10)t^2 \\implies 5t^2 - 20t - 25 = 0 \\implies t^2 - 4t - 5 = 0 \\implies (t-5)(t+1) = 0$. Since time cannot be negative, $t = 5\\text{ s}$.", + solution_text_hi="$s = ut + \\frac{1}{2}at^2$ का उपयोग करने पर। ऊपर की दिशा को धनात्मक मानने पर: $-25 = 20t - \\frac{1}{2}(10)t^2 \\implies 5t^2 - 20t - 25 = 0 \\implies t^2 - 4t - 5 = 0 \\implies (t-5)(t+1) = 0$। चूंकि समय ऋणात्मक नहीं हो सकता, $t = 5$ सेकंड है।", + language=models.MediumEnum.BILINGUAL, + year=2023, + exam=models.ExamEnum.NEET, + created_by=admin_id + ) + + # Chemistry 1 (NEET) + nq3 = models.Question( + id=uuid4(), + subject="Chemistry", + topic="Periodic Table", + subtopic="Ionic Radii", + difficulty="Medium", + type=models.QuestionTypeEnum.MCQ_SINGLE, + question_text="Which of the following represents the correct order of increasing ionic radii for isoelectronic species?", + question_text_hi="समइलेक्ट्रॉनिक प्रजातियों के लिए बढ़ती हुई आयनिक त्रिज्या का सही क्रम निम्नलिखित में से कौन सा दर्शाता है?", + options={"A": "$\\text{O}^{2-} < \\text{F}^- < \\text{Na}^+$", "B": "$\\text{Na}^+ < \\text{F}^- < \\text{O}^{2-}$", "C": "$\\text{F}^- < \\text{Na}^+ < \\text{O}^{2-}$", "D": "$\\text{O}^{2-} < \\text{Na}^+ < \\text{F}^-$"}, + options_hi={"A": "$\\text{O}^{2-} < \\text{F}^- < \\text{Na}^+$", "B": "$\\text{Na}^+ < \\text{F}^- < \\text{O}^{2-}$", "C": "$\\text{F}^- < \\text{Na}^+ < \\text{O}^{2-}$", "D": "$\\text{O}^{2-} < \\text{Na}^+ < \\text{F}^-$"}, + correct_answer={"correct": "B"}, + solution_text="For isoelectronic species, ionic radii decrease with increase in atomic number (or nuclear charge). Nuclear charge: $\\text{Na}^+ (11) > \\text{F}^- (9) > \\text{O}^{2-} (8)$. Therefore, the correct order is $\\text{Na}^+ < \\text{F}^- < \\text{O}^{2-}$.", + solution_text_hi="समइलेक्ट्रॉनिक प्रजातियों के लिए, परमाणु संख्या (या नाभिकीय आवेश) में वृद्धि के साथ आयनिक त्रिज्या कम होती जाती है। नाभिकीय आवेश: $\\text{Na}^+ (11) > \\text{F}^- (9) > \\text{O}^{2-} (8)$। इसलिए, सही क्रम $\\text{Na}^+ < \\text{F}^- < \\text{O}^{2-}$ है।", + language=models.MediumEnum.BILINGUAL, + year=2024, + exam=models.ExamEnum.NEET, + created_by=admin_id + ) + + neet_physics_dummies = generate_dummy_questions("Physics", 49, models.ExamEnum.NEET, admin_id, start_idx=2) + neet_chem_dummies = generate_dummy_questions("Chemistry", 49, models.ExamEnum.NEET, admin_id, start_idx=2) + neet_botany_dummies = generate_dummy_questions("Botany", 49, models.ExamEnum.NEET, admin_id, start_idx=2) + neet_zoology_dummies = generate_dummy_questions("Zoology", 50, models.ExamEnum.NEET, admin_id, start_idx=1) + + session.add_all([nq1, nq2, nq3]) + session.add_all(neet_physics_dummies) + session.add_all(neet_chem_dummies) + session.add_all(neet_botany_dummies) + session.add_all(neet_zoology_dummies) + + # --- TESTS CREATION --- + + # 1. JEE Mock Test + jee_test = models.Test( + id=jee_test_id, + title="JEE Main Full Mock Test #1 (2025)", + type="Mock", + exam_pattern=models.ExamEnum.JEE_MAIN, + sections={ + "Physics": { + "name": "Physics", + "questions": [str(jq1.id), str(jq2.id)] + [str(q.id) for q in jee_physics_dummies], + "time_limit": 60 + }, + "Chemistry": { + "name": "Chemistry", + "questions": [str(jq3.id)] + [str(q.id) for q in jee_chem_dummies], + "time_limit": 60 + }, + "Mathematics": { + "name": "Mathematics", + "questions": [str(jq4.id)] + [str(q.id) for q in jee_math_dummies], + "time_limit": 60 + } + }, + total_time=180, + difficulty="Mixed", + scheduled_date=datetime.utcnow(), + created_by=admin_id + ) + session.add(jee_test) + + # 2. NEET Mock Test + neet_test = models.Test( + id=neet_test_id, + title="NEET UG Full Mock Test #1 (2025)", + type="Mock", + exam_pattern=models.ExamEnum.NEET, + sections={ + "Physics": { + "name": "Physics", + "questions": [str(nq2.id)] + [str(q.id) for q in neet_physics_dummies], + "time_limit": 45 + }, + "Chemistry": { + "name": "Chemistry", + "questions": [str(nq3.id)] + [str(q.id) for q in neet_chem_dummies], + "time_limit": 45 + }, + "Botany": { + "name": "Botany", + "questions": [str(nq1.id)] + [str(q.id) for q in neet_botany_dummies], + "time_limit": 45 + }, + "Zoology": { + "name": "Zoology", + "questions": [str(q.id) for q in neet_zoology_dummies], + "time_limit": 45 + } + }, + total_time=180, + difficulty="Mixed", + scheduled_date=datetime.utcnow(), + created_by=admin_id + ) + session.add(neet_test) + + session.commit() + + print("\n=== LOCAL SEED COMPLETED SUCCESSFULLY ===") + print(f"JEE Test ID: {jee_test.id}") + print(f"NEET Test ID: {neet_test.id}\n") + +if __name__ == "__main__": + seed_local_database() diff --git a/backend/worker.py b/backend/worker.py new file mode 100644 index 0000000..f04fb4b --- /dev/null +++ b/backend/worker.py @@ -0,0 +1,84 @@ +import os +from datetime import datetime, timedelta +from celery import Celery +from celery.schedules import crontab +from sqlmodel import Session, select +import json + +from database import engine +import models + +REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0") + +# Initialize Celery connected to our local Redis broker +celery_app = Celery( + "pragativeer_tasks", + broker=REDIS_URL, + backend=REDIS_URL +) + +# Configure timezone to IST for accurate 5:00 AM scheduling +celery_app.conf.update( + timezone='Asia/Kolkata', + enable_utc=True, +) + +@celery_app.task(name="generate_daily_study_plans", bind=True, default_retry_delay=60, max_retries=3) +def generate_daily_study_plans(self): + """ + Runs daily to evaluate students' weak areas and generate + a personalized AI study plan for the day. + """ + try: + with Session(engine) as session: + # Fetch all active users on the AI Coach tier + ai_students = session.exec( + select(models.User).where(models.User.tier == models.TierEnum.AI_COACH) + ).all() + + today = datetime.utcnow().date() + plans_created = 0 + + for student in ai_students: + # 1. Fetch pending spaced repetition mistakes for today + pending_mistakes = session.exec( + select(models.Mistake) + .where(models.Mistake.user_id == student.id) + .where(models.Mistake.is_mastered == False) + .where(models.Mistake.next_review <= datetime.utcnow()) + ).all() + + # 2. Extract weak topics (for revision recommendation) + topics = [m.topic for m in pending_mistakes] + most_weak_topic = max(set(topics), key=topics.count) if topics else "Mixed Syllabus" + + # 3. Construct the time-budgeted dynamic task list + tasks = [ + {"type": "Revision", "description": f"Revise {most_weak_topic}", "duration_minutes": 15, "status": "Pending"}, + {"type": "Mistake_Review", "description": "Daily Mistake Review Session", "duration_minutes": 10, "status": "Pending"}, + {"type": "DPS", "description": f"Attempt {most_weak_topic} Practice Sheet", "duration_minutes": 20, "status": "Pending"} + ] + + # 4. Save to Database + new_plan = models.StudyPlan( + user_id=student.id, + date=today, + tasks=tasks, + is_completed=False + ) + session.add(new_plan) + plans_created += 1 + + session.commit() + return f"Successfully generated {plans_created} daily study plans." + except Exception as exc: + print(f"Database error during study plan generation: {exc}") + raise self.retry(exc=exc) + +# Schedule the tasks using Celery Beat +celery_app.conf.beat_schedule = { + 'generate-study-plans-every-morning': { + 'task': 'generate_daily_study_plans', + 'schedule': crontab(hour=5, minute=0), # 5:00 AM IST + }, +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6a1b3f2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,116 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: pragativeer_db + environment: + POSTGRES_USER: pragativeer_admin + POSTGRES_PASSWORD: local_secure_password_2026 + POSTGRES_DB: pragativeer_prod + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - pragativeer_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pragativeer_admin -d pragativeer_prod"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: pragativeer_cache + ports: + - "6379:6379" + volumes: + - redisdata:/data + networks: + - pragativeer_network + + + backend: + build: + context: ./backend + dockerfile: Dockerfile.dev + container_name: pragativeer_backend + volumes: + - ./backend:/app + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod + - REDIS_URL=redis://redis:6379/0 + - ENV=development + env_file: + - ./backend/.env + depends_on: + postgres: + condition: service_healthy + networks: + - pragativeer_network + + worker: + build: + context: ./backend + dockerfile: Dockerfile.dev + container_name: pragativeer_worker + command: celery -A worker.celery_app worker --loglevel=info + volumes: + - ./backend:/app + environment: + - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod + - REDIS_URL=redis://redis:6379/0 + env_file: + - ./backend/.env + depends_on: + - postgres + - redis + networks: + - pragativeer_network + + beat: + build: + context: ./backend + dockerfile: Dockerfile.dev + container_name: pragativeer_beat + command: celery -A worker.celery_app beat --loglevel=info + volumes: + - ./backend:/app + environment: + - DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod + - REDIS_URL=redis://redis:6379/0 + env_file: + - ./backend/.env + depends_on: + - postgres + - redis + networks: + - pragativeer_network + + frontend: + build: + context: . + dockerfile: Dockerfile.dev + container_name: pragativeer_frontend + ports: + - "3000:3000" + volumes: + - .:/app + - /app/node_modules + environment: + - NEXT_PUBLIC_API_URL=http://localhost:8000 + depends_on: + - backend + networks: + - pragativeer_network + +volumes: + pgdata: + redisdata: + +networks: + pragativeer_network: + driver: bridge diff --git a/package-lock.json b/package-lock.json index 157837f..3bb36ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,20 @@ "dependencies": { "clsx": "^2.1.1", "framer-motion": "^12.41.0", + "html-to-image": "^1.11.13", + "idb-keyval": "^6.2.6", + "katex": "^0.17.0", "lucide-react": "^1.21.0", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4", "react-katex": "^3.1.0", - "recharts": "^3.9.0", - "tailwind-merge": "^3.6.0" + "react-markdown": "^10.1.0", + "recharts": "^3.9.2", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "tailwind-merge": "^3.6.0", + "zustand": "^5.0.14" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -1279,16 +1286,6 @@ } } }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.8", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", - "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1662,13 +1659,39 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1683,6 +1706,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", @@ -1697,7 +1741,6 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1723,6 +1766,12 @@ "@types/react": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -2024,6 +2073,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "license": "ISC" + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", @@ -2634,6 +2689,16 @@ "node": ">= 0.4" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2791,6 +2856,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2808,6 +2883,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2843,6 +2958,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -2885,7 +3010,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -3074,7 +3198,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3094,6 +3217,19 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3137,6 +3273,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3147,6 +3292,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3203,6 +3361,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3830,6 +4000,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3846,6 +4026,12 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4305,6 +4491,174 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -4322,6 +4676,28 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.6.tgz", + "integrity": "sha512-FY64UEhw+5liMzMQ1R9Mw6AF0+wyBrg1CIA1z4CjI/EvT5ty/SvQcWZgd8s9sgaNhX10Y8UzScTh89tEAls5nA==", + "license": "Apache-2.0" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4333,9 +4709,9 @@ } }, "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "version": "11.1.11", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.11.tgz", + "integrity": "sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==", "license": "MIT", "funding": { "type": "opencollective", @@ -4369,6 +4745,12 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4393,6 +4775,30 @@ "node": ">=12" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4551,6 +4957,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-document.all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", @@ -4626,6 +5042,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -4679,6 +5105,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4959,9 +5397,9 @@ } }, "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.17.0.tgz", + "integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -5302,6 +5740,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5353,6 +5801,178 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5363,6 +5983,483 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5419,7 +6516,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5776,6 +6872,43 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5882,6 +7015,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5953,6 +7096,49 @@ "react": ">=15.3.2 <20" } }, + "node_modules/react-katex/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", @@ -5977,9 +7163,9 @@ } }, "node_modules/recharts": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.0.tgz", - "integrity": "sha512-dCEcE9y20c8H2tkVeByrAXhhnBJk6/QLbxKmn+dJUptOfc5NMjwRh1jo0vZPRLD+5dMrHrP+hPEsfbGBMfnf5Q==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.2.tgz", + "integrity": "sha512-G4fy+Pk46RaXgwWMh+Nzhyo/lbFAVqXo9gtetlyehe6Ehge9CsgDuOTwQDD+i1+llaLktNBiNq4bhnGlDRXFtw==", "license": "MIT", "workspaces": [ "www" @@ -5990,7 +7176,7 @@ "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", - "immer": "^10.1.1", + "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "tiny-invariant": "^1.3.3", @@ -6065,6 +7251,90 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/reselect": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", @@ -6436,6 +7706,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6571,6 +7851,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6594,6 +7888,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -6741,6 +8053,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6941,6 +8273,121 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -7029,6 +8476,48 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -7051,6 +8540,16 @@ "d3-timer": "^3.0.1" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7208,6 +8707,45 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 454c97c..2a82037 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,20 @@ "dependencies": { "clsx": "^2.1.1", "framer-motion": "^12.41.0", + "html-to-image": "^1.11.13", + "idb-keyval": "^6.2.6", + "katex": "^0.17.0", "lucide-react": "^1.21.0", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4", "react-katex": "^3.1.0", - "recharts": "^3.9.0", - "tailwind-merge": "^3.6.0" + "react-markdown": "^10.1.0", + "recharts": "^3.9.2", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "tailwind-merge": "^3.6.0", + "zustand": "^5.0.14" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pragativeer-prd (4).pdf b/pragativeer-prd (4).pdf new file mode 100644 index 0000000000000000000000000000000000000000..756db6bf2d9f5cc91f4e387bed2d3e90bcb3abf6 GIT binary patch literal 108566 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 TQ2?bC6y+2sgoLz>bSVA{IH?WT literal 0 HcmV?d00001 diff --git a/src/app/admin/students/page.tsx b/src/app/admin/students/page.tsx index 931a21c..6411ff3 100644 --- a/src/app/admin/students/page.tsx +++ b/src/app/admin/students/page.tsx @@ -4,11 +4,11 @@ import { Search, Download, MoreVertical } from "lucide-react" export default function StudentsPage() { const students = [ - { id: "STU-001", name: "Rahul Sharma", email: "rahul@example.com", tier: "AI Pro", target: "NEET", joined: "Oct 12, 2025", tests: 42 }, - { id: "STU-002", name: "Priya Patel", email: "priya@example.com", tier: "Basic", target: "JEE Main", joined: "Nov 05, 2025", tests: 14 }, - { id: "STU-003", name: "Amit Kumar", email: "amit@example.com", tier: "AI Pro", target: "JEE Adv", joined: "Dec 01, 2025", tests: 89 }, - { id: "STU-004", name: "Sneha Reddy", email: "sneha@example.com", tier: "AI Pro", target: "NEET", joined: "Dec 15, 2025", tests: 34 }, - { id: "STU-005", name: "Vikram Singh", email: "vikram@example.com", tier: "Basic", target: "NEET", joined: "Jan 10, 2026", tests: 5 }, + { id: "STU-001", name: "Rahul Sharma", email: "rahul@example.com", tier: "AI Pro", target: "NEET", joined: "Oct 12, 2025", tests: 42, streak: 45, lastLogin: "Today, 08:30 AM" }, + { id: "STU-002", name: "Priya Patel", email: "priya@example.com", tier: "Basic", target: "JEE Main", joined: "Nov 05, 2025", tests: 14, streak: 2, lastLogin: "Yesterday, 14:15 PM" }, + { id: "STU-003", name: "Amit Kumar", email: "amit@example.com", tier: "AI Pro", target: "JEE Adv", joined: "Dec 01, 2025", tests: 89, streak: 0, lastLogin: "3 Days Ago" }, + { id: "STU-004", name: "Sneha Reddy", email: "sneha@example.com", tier: "AI Pro", target: "NEET", joined: "Dec 15, 2025", tests: 34, streak: 12, lastLogin: "Today, 11:45 AM" }, + { id: "STU-005", name: "Vikram Singh", email: "vikram@example.com", tier: "Basic", target: "NEET", joined: "Jan 10, 2026", tests: 5, streak: 0, lastLogin: "1 Week Ago" }, ] return ( @@ -41,8 +41,9 @@ export default function StudentsPage() { Plan Tier Target Exam Tests Taken - Joined Date - + Streak + Last Login + Actions @@ -61,8 +62,16 @@ export default function StudentsPage() { {s.target} {s.tests} - {s.joined} - + + 0 ? 'text-orange-500' : 'text-muted-foreground'}`}>{s.streak} + + {s.lastLogin} + + {s.streak === 0 && ( + + )} diff --git a/src/app/api/og/progress/route.tsx b/src/app/api/og/progress/route.tsx new file mode 100644 index 0000000..7bf83ca --- /dev/null +++ b/src/app/api/og/progress/route.tsx @@ -0,0 +1,67 @@ +import { ImageResponse } from 'next/og'; +import { NextRequest } from 'next/server'; + +export const runtime = 'edge'; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + + // Extract dynamic data from the URL query parameters + const streak = searchParams.get('streak') || '0'; + const accuracy = searchParams.get('accuracy') || '0'; + const name = searchParams.get('name') || 'Student'; + + // Renders a high-end, minimalist graphic using pure Tailwind and React + return new ImageResponse( + ( +
+
+ PragatiVeer +
+
+ {name}'s JEE Preparation Progress +
+ +
+ {/* Streak Metric */} +
+ {streak}🔥 + Day Streak +
+ + {/* Accuracy Metric */} +
+ {accuracy}% + Top Accuracy +
+
+ +
+ Join me on pragativeer.com/invite +
+
+ ), + { + width: 1200, + height: 630, + } + ); + } catch (e: any) { + return new Response(`Failed to generate image`, { status: 500 }); + } +} diff --git a/src/app/cbt/[examId]/page.tsx b/src/app/cbt/[examId]/page.tsx index 401c501..1b24e7e 100644 --- a/src/app/cbt/[examId]/page.tsx +++ b/src/app/cbt/[examId]/page.tsx @@ -1,139 +1,571 @@ +"use client" + +import { useEffect, useState, use, useRef } from "react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { Clock, Flag, ChevronLeft, ChevronRight, Pause, MoreVertical } from "lucide-react" +import { Clock, Flag, ChevronLeft, ChevronRight, Pause as PauseIcon, MoreVertical, Calculator as CalculatorIcon, FlaskConical, Edit3, XCircle, Play, Shield, AlertTriangle } from "lucide-react" import Image from "next/image" +import { useCBTStore, QuestionStatus } from "@/store/cbtStore" +import { useExamTimer } from "@/hooks/useExamTimer" +import ReactMarkdown from "react-markdown" +import remarkMath from "remark-math" +import rehypeKatex from "rehype-katex" +import "katex/dist/katex.min.css" +import { Calculator } from "@/components/CBT/Calculator" +import { Scratchpad } from "@/components/CBT/Scratchpad" +import { PeriodicTable } from "@/components/CBT/PeriodicTable" +import { ExamResult } from "@/components/CBT/ExamResult" + +// Dummy data for testing the CBT client engine +const DUMMY_QUESTIONS = Array.from({ length: 20 }).map((_, i) => ({ + id: `q_${i+1}`, + number: i + 1, + subject: "Physics", + text_en: `A solid cylinder of mass M and radius R is rolling without slipping down an inclined plane of angle θ. Question ${i+1}. What is the minimum coefficient of static friction μ_s required to prevent slipping?`, + text_hi: `द्रव्यमान M और त्रिज्या R का एक ठोस बेलन कोण θ के एक आनत तल पर बिना फिसले लुढ़क रहा है। प्रश्न ${i+1}...`, + options: ['(1/3) tan θ', '(2/3) tan θ', '(1/2) tan θ', 'tan θ'] +})); + +export default function CBTExamPage({ params }: { params: Promise<{ examId: string }> }) { + const resolvedParams = use(params); + const examId = resolvedParams.examId; + const [mounted, setMounted] = useState(false); + const [showCalc, setShowCalc] = useState(false); + const [showScratchpad, setShowScratchpad] = useState(false); + const [showPeriodicTable, setShowPeriodicTable] = useState(false); + const [isPaused, setIsPaused] = useState(false); + const [showSubmitConfirm, setShowSubmitConfirm] = useState(false); + + // Anti-cheat state + const [testStarted, setTestStarted] = useState(false); + const [violationCount, setViolationCount] = useState(0); + const [showWarning, setShowWarning] = useState(false); + const hasShownWarning = useRef(false); + const hasAutoSubmitted = useRef(false); + + const { + initExam, + questions, + answers, + currentQuestionIndex, + selectAnswer, + clearResponse, + markForReview, + goToQuestion, + addTimeSpent, + submitExam, + getAttemptPayload, + submitAttemptToServer, + resetExamState, + status + } = useCBTStore(); + + const { formattedTime, isTimedOut } = useExamTimer(); + const [questionStartTime, setQuestionStartTime] = useState(Date.now()); + + // Initialize Exam on mount + useEffect(() => { + async function fetchExamData() { + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`); + if (!testRes.ok) throw new Error("Failed to fetch test structure"); + const testData = await testRes.json(); + + // Extract all question IDs from sections + const questionIds: string[] = []; + Object.values(testData.sections).forEach((section: any) => { + questionIds.push(...section.questions); + }); + + // Fetch full question objects + const fetchedQuestions = await Promise.all( + questionIds.map(async (id, idx) => { + const qRes = await fetch(`${apiUrl}/api/questions/${id}`); + const qData = await qRes.json(); + return { + id: qData.id, + number: idx + 1, + subject: qData.subject, + text_en: qData.question_text, + text_hi: qData.question_text_hi || qData.question_text, + options: Object.values(qData.options || {}), + options_hi: Object.values(qData.options_hi || qData.options || {}), + }; + }) + ); + + initExam(examId, fetchedQuestions, testData.total_time * 60); + setMounted(true); + } catch (err) { + console.error("Failed to load exam from network, falling back to dummy data:", err); + initExam(examId, DUMMY_QUESTIONS, 10800); + setMounted(true); + } + } + fetchExamData(); + }, [examId, initExam]); + + // Track time spent per question + useEffect(() => { + if (testStarted) { + setQuestionStartTime(Date.now()); + } + }, [currentQuestionIndex, testStarted]); + + // Anti-Cheat: Tab switching listener + useEffect(() => { + if (!testStarted || status === 'SUBMITTED' || status === 'TIMED_OUT') return; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + setViolationCount((prev) => prev + 1); + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + return () => document.removeEventListener("visibilitychange", handleVisibilityChange); + }, [testStarted, status]); + + // Handle side-effects of violation count changing + useEffect(() => { + if (violationCount === 1 && !hasShownWarning.current) { + hasShownWarning.current = true; + setShowWarning(true); + } else if (violationCount >= 2 && status !== 'SUBMITTED' && status !== 'TIMED_OUT' && !hasAutoSubmitted.current) { + hasAutoSubmitted.current = true; + submitExam('SUBMITTED'); + submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b') + .then(() => { + window.alert("Test Auto-Submitted due to multiple tab switching violations."); + }) + .catch(err => { + console.error("Auto-submit fetch error:", err); + window.alert("Test Auto-Submitted due to multiple tab switching violations."); + }); + } + }, [violationCount, status, submitExam, submitAttemptToServer]); + + const startTest = async () => { + try { + if (document.documentElement.requestFullscreen) { + await document.documentElement.requestFullscreen(); + } + } catch (e) { + console.warn("Fullscreen API failed", e); + } + setTestStarted(true); + }; + + const handleNext = () => { + if (!currentQuestion) return; + const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000); + addTimeSpent(currentQuestion.id, timeSpent); + if (currentQuestionIndex < questions.length - 1) { + goToQuestion(currentQuestionIndex + 1); + } else { + goToQuestion(0); + } + } + + const handlePrev = () => { + if (!currentQuestion) return; + const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000); + addTimeSpent(currentQuestion.id, timeSpent); + if (currentQuestionIndex > 0) { + goToQuestion(currentQuestionIndex - 1); + } + } + + const handleGridClick = (index: number) => { + if (!currentQuestion) return; + const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000); + addTimeSpent(currentQuestion.id, timeSpent); + goToQuestion(index); + } + + const handleSubmit = () => { + setShowSubmitConfirm(true); + } + + const confirmSubmit = async () => { + setShowSubmitConfirm(false); + submitExam('SUBMITTED'); + const payload = getAttemptPayload('b0e17378-864d-439e-8806-708dc48a891b'); + console.log("EXAM SUBMITTED. PAYLOAD:", JSON.stringify(payload, null, 2)); + await submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b'); + } + + if (!mounted || questions.length === 0) return
Loading Exam Engine...
; + + if (!testStarted && status !== 'SUBMITTED' && status !== 'TIMED_OUT') { + return ( +
+
+

+ Secure Exam Environment +

+
+

Target: {questions.some(q => q.subject.toLowerCase() === 'biology') ? 'NEET' : 'JEE'} Pattern Test

+
    +
  • The test will open in Full Screen Mode.
  • +
  • Do not switch tabs or exit full screen.
  • +
  • First violation will result in a strict warning.
  • +
  • Second violation will automatically submit your exam to prevent cheating.
  • +
  • Ensure your internet connection is stable before starting.
  • +
+
+ +
+
+ ); + } + + if (status === 'SUBMITTED' || status === 'TIMED_OUT') { + const payload = getAttemptPayload('b0e17378-864d-439e-8806-708dc48a891b'); + if (!payload) return
Loading results...
; + + return ( + { + resetExamState(); + window.location.reload(); + }} + /> + ) + } + + const currentQuestion = questions[currentQuestionIndex]; + const currentState = answers[currentQuestion.id]; + + // Derive palette stats + const paletteStats = Object.values(answers).reduce( + (acc, val) => { + acc[val.status]++; + return acc; + }, + { UNVISITED: 0, NOT_ANSWERED: 0, ANSWERED: 0, MARKED: 0, ANSWERED_MARKED: 0 } as Record + ); + + // Group questions by subject for the side panel + const questionsBySubject = questions.reduce((acc, q, index) => { + if (!acc[q.subject]) { + acc[q.subject] = []; + } + acc[q.subject].push({ ...q, originalIndex: index }); + return acc; + }, {} as Record); -export default function CBTExamPage({ params }: { params: { examId: string } }) { return ( - <> -
+
+ {/* Warning Modal */} + {showWarning && ( +
+
+
+ +

Warning!

+

You switched tabs or left the secure window.

+

This is your final warning. Doing this again will automatically submit your exam and your score will be evaluated as is.

+ +
+
+
+ )} + +
-
+
PragatiVeer PragatiVeer
-

AITS Full Syllabus Test 4

-

Physics • Section A

+

AITS Full Syllabus Test

+

{currentQuestion.subject}

-
- - 02:45:12 +
+ + +
- - +
- {/* Main Question Area */} -
-
-
- Question 12 -
- +4 / -1 - + {/* Main Question Area - Configured for Bilingual Split Pane */} +
+
+
+
+
+ Question {currentQuestion.number} + English +
+
+ +4 / -1 + +
+
+ +
+ + {currentQuestion.text_en} + +
+ +
+ {currentQuestion.options.map((option: string, idx: number) => { + const letter = String.fromCharCode(65 + idx); + const isSelected = currentState?.selected_answer === letter; + return ( + + ) + })}
+
+ + {/* Optional Bilingual Hindi Pane */} +
+
+
+
+ प्रश्न {currentQuestion.number} + हिंदी +
+
+
+ + {currentQuestion.text_hi} + +
-
-

- A solid cylinder of mass M and radius R is rolling without slipping down an inclined plane of angle θ. - What is the minimum coefficient of static friction μ_s required to prevent slipping? -

-
- -
- {['(1/3) tan θ', '(2/3) tan θ', '(1/2) tan θ', 'tan θ'].map((option, idx) => ( - - ))} +
+ {currentQuestion.options_hi?.map((option: string, idx: number) => { + const letter = String.fromCharCode(65 + idx); + const isSelected = currentState?.selected_answer === letter; + return ( + + ) + })} +
{/* Side Panel for Question Grid */} -
) } diff --git a/src/app/dashboard/ai-coach/page.tsx b/src/app/dashboard/ai-coach/page.tsx new file mode 100644 index 0000000..2e23754 --- /dev/null +++ b/src/app/dashboard/ai-coach/page.tsx @@ -0,0 +1,170 @@ +"use client" + +import { useEffect, useState } from "react" +import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap" +import { StudyPlanCard } from "@/components/AI/StudyPlanCard" +import { ReadinessGauge } from "@/components/AI/ReadinessGauge" +import { CollegePredictorCard } from "@/components/AI/CollegePredictorCard" +import { TutorChat } from "@/components/AI/TutorChat" +import { ReferralCard } from "@/components/AI/ReferralCard" +import { ProgressShare } from "@/components/AI/ProgressShare" +import { MistakeReviewCard, MistakePayload } from "@/components/AI/MistakeReviewCard" +import { Card, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { BrainCircuit, BookOpen, Calculator, Target, RefreshCw, CheckCircle2 } from "lucide-react" + +// Hardcode mock UUID for demo fetching +const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b" + +export default function AICoachDashboard() { + const [mistakes, setMistakes] = useState([]) + const [currentIndex, setCurrentIndex] = useState(0) + const [loading, setLoading] = useState(true) + + const fetchMistakes = async () => { + setLoading(true) + try { + const res = await fetch(`http://localhost:8000/api/mistakes/today?user_id=${USER_ID}`) + if (!res.ok) throw new Error("Failed to fetch") + const data = await res.json() + setMistakes(data) + } catch (error) { + console.error(error) + // If backend fails, fallback to empty array + setMistakes([]) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchMistakes() + }, []) + + const submitReview = async (mistakeId: string, selectedAnswer: string) => { + const res = await fetch("http://localhost:8000/api/mistakes/review", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mistake_id: mistakeId, selected_answer: selectedAnswer }) + }) + + if (!res.ok) throw new Error("Failed to submit review") + return await res.json() + } + + const handleNext = () => { + if (currentIndex < mistakes.length - 1) { + setCurrentIndex(prev => prev + 1) + } else { + // Session finished + setCurrentIndex(mistakes.length) // Push out of bounds to trigger complete state + } + } + + return ( +
+
+
+

+ AI Coach +

+

Your personalized SM-2 spaced repetition and analytics engine.

+
+
+ + {/* Summary Metrics */} +
+ + +
+
+

Physics Errors

+

{mistakes.filter(m => m.question.subject === 'Physics').length}

+
+
+
+ + +
+
+

Chemistry Errors

+

{mistakes.filter(m => m.question.subject === 'Chemistry').length}

+
+
+
+ + +
+
+

Maths Errors

+

{mistakes.filter(m => m.question.subject === 'Maths').length}

+
+
+
+
+ + {/* Predictive Intelligence */} +
+ + +
+ + {/* Mistake Review Workspace */} +
+
+

Daily Mistake Review Queue

+ + {currentIndex < mistakes.length ? `${currentIndex + 1} / ${mistakes.length}` : "Finished"} + +
+ + {loading ? ( + + Loading queue... + + ) : mistakes.length === 0 ? ( + + +

You're all caught up!

+

No mistakes scheduled for review today. Go take a test!

+
+ ) : currentIndex < mistakes.length ? ( + + ) : ( + + +

Session Complete

+

Great job clearing your error queue! The SM-2 engine has scheduled your next reviews.

+ +
+ )} +
+ + {/* Heatmap & Timeline */} +
+
+ +
+
+ +
+
+ + {/* Tutor Chat Workspace */} +
+ +
+ + {/* Growth & Sharing Hub */} +
+ + +
+ +
+ ) +} diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx index 9968892..3feec3a 100644 --- a/src/app/dashboard/analytics/page.tsx +++ b/src/app/dashboard/analytics/page.tsx @@ -1,7 +1,35 @@ +"use client" + import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" -import { LineChart, Activity, BrainCircuit } from "lucide-react" +import { LineChart as LineChartIcon, Activity, BrainCircuit } from "lucide-react" +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts' export default function AnalyticsPage() { + const trendData = [ + { name: 'Week 1', score: 120 }, + { name: 'Week 2', score: 135 }, + { name: 'Week 3', score: 130 }, + { name: 'Week 4', score: 145 }, + { name: 'Week 5', score: 160 }, + { name: 'Week 6', score: 185 }, + ] + + // For heatmap-like visualization (using a barchart with different colors) + const heatmapData = [ + { topic: 'Kinematics', accuracy: 95 }, + { topic: 'Optics', accuracy: 72 }, + { topic: 'Thermodynamics', accuracy: 45 }, + { topic: 'Calculus', accuracy: 38 }, + { topic: 'Organic Chem', accuracy: 88 }, + { topic: 'Inorganic', accuracy: 65 }, + ] + + const getColor = (accuracy: number) => { + if (accuracy >= 80) return '#10b981' // emerald-500 + if (accuracy >= 60) return '#f59e0b' // amber-500 + return '#ef4444' // red-500 + } + return (
@@ -12,16 +40,22 @@ export default function AnalyticsPage() {
- Projected Score Trajectory + Projected Score Trajectory Your mock scores over the last 6 months projected towards the final exam. - - {/* Placeholder for an actual chart library like Recharts */} -
- -

Data gathering in progress...

-

Complete 2 more CBTs to unlock predictive graphing.

-
+ + + + + + + + + +
@@ -39,21 +73,25 @@ export default function AnalyticsPage() { - Subject Mastery + Topic Heatmap (Accuracy) - -
-
Biology 89%
-
-
-
-
Chemistry 68%
-
-
-
-
Physics 42%
-
-
+ + + + + + [`${value}% Accuracy`, 'Accuracy']} + /> + + {heatmapData.map((entry, index) => ( + + ))} + + +
diff --git a/src/app/dashboard/cbt/history/[attemptId]/page.tsx b/src/app/dashboard/cbt/history/[attemptId]/page.tsx new file mode 100644 index 0000000..261e4a4 --- /dev/null +++ b/src/app/dashboard/cbt/history/[attemptId]/page.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { useEffect, useState, use } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { ArrowLeft, CheckCircle2, XCircle, Clock, Award } from "lucide-react"; + +export default function AttemptHistoryPage({ params }: { params: Promise<{ attemptId: string }> }) { + const { attemptId } = use(params); + const [loading, setLoading] = useState(true); + const [attempt, setAttempt] = useState(null); + const [test, setTest] = useState(null); + const [questions, setQuestions] = useState>({}); + + const userId = "b0e17378-864d-439e-8806-708dc48a891b"; + + useEffect(() => { + async function fetchHistoryData() { + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + + // 1. Fetch all attempts to find this specific one + const attemptsRes = await fetch(`${apiUrl}/api/users/${userId}/attempts`); + const attemptsData = await attemptsRes.json(); + const foundAttempt = attemptsData.find((a: any) => a.id === attemptId); + + if (!foundAttempt) { + throw new Error("Attempt not found"); + } + setAttempt(foundAttempt); + + // 2. Fetch the test details + const testRes = await fetch(`${apiUrl}/api/tests/${foundAttempt.source_id}?user_id=${userId}`); + const testData = await testRes.json(); + setTest(testData); + + // 3. Fetch all questions for this attempt to build the answer key + const qs: Record = {}; + await Promise.all( + foundAttempt.answers.map(async (ans: any) => { + const qRes = await fetch(`${apiUrl}/api/questions/${ans.question_id}`); + if (qRes.ok) { + const qData = await qRes.json(); + qs[ans.question_id] = qData; + } + }) + ); + setQuestions(qs); + + } catch (err) { + console.error("Failed to load attempt history:", err); + } finally { + setLoading(false); + } + } + fetchHistoryData(); + }, [attemptId, userId]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!attempt) { + return ( +
+ Attempt not found. Go back +
+ ); + } + + const scoreCalc = (attempt.score * 4) - ((attempt.total_questions - attempt.score) * 1); + const maxScore = attempt.total_questions * 4; + + return ( +
+ + Back to Tests + + +
+
+

{test?.title || 'Practice Test'} Solutions

+

Review your performance, detailed solutions, and identify your weak spots.

+
+ +
+ + + +
Score
+
{scoreCalc} / {maxScore}
+
+
+ + + +
Time Taken
+
{Math.floor(attempt.time_taken / 60)}m {attempt.time_taken % 60}s
+
+
+
+
+ +
+

Answer Key & Explanations

+ + {attempt.answers.map((ans: any, idx: number) => { + const q = questions[ans.question_id]; + if (!q) return null; + + return ( + + +
+
+
+ {idx + 1} +
+
+

{q.text}

+
+ {q.topic} + {q.difficulty} +
+
+
+ {ans.is_correct ? ( + Correct + ) : ( + Incorrect + )} +
+ +
+
+

Your Answer

+
+ {q.options[ans.selected_answer] ? `${ans.selected_answer}) ${q.options[ans.selected_answer]}` : 'Not Attempted'} +
+
+
+

Correct Answer

+
+ {q.correct_answer?.correct}) {q.options[q.correct_answer?.correct]} +
+
+
+ + {q.correct_answer?.explanation && ( +
+

Explanation:

+

{q.correct_answer.explanation}

+
+ )} +
+
+ ); + })} +
+
+ ); +} diff --git a/src/app/dashboard/cbt/page.tsx b/src/app/dashboard/cbt/page.tsx index a5d603d..db309ec 100644 --- a/src/app/dashboard/cbt/page.tsx +++ b/src/app/dashboard/cbt/page.tsx @@ -1,30 +1,95 @@ -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Activity, Clock, PlayCircle, Search } from "lucide-react" +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Activity, Clock, PlayCircle, Search } from "lucide-react"; + +interface TestData { + id: string; + title: string; + type: string; + exam_pattern: string; + total_time: number; + difficulty: string; +} export default function CBTPage() { - const upcomingExams = [ - { title: "AITS Full Syllabus Test 4", date: "Tomorrow, 10:00 AM", duration: "180 Min", marks: 720 }, - { title: "Part Test 3: Mechanics", date: "Oct 24, 2026", duration: "180 Min", marks: 720 }, - { title: "Weekly Revision Test", date: "Oct 28, 2026", duration: "90 Min", marks: 360 }, - ] + const [exams, setExams] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [targetExam, setTargetExam] = useState(""); + const [attempts, setAttempts] = useState([]); - const pastExams = [ - { title: "AITS Full Syllabus Test 3", date: "Oct 15, 2026", score: 645, total: 720, percentile: "99.4" }, - { title: "Part Test 2: Electromagnetism", date: "Oct 08, 2026", score: 680, total: 720, percentile: "99.8" }, - ] + const userId = "b0e17378-864d-439e-8806-708dc48a891b"; + + useEffect(() => { + async function loadCBTExams() { + try { + // 1. Fetch user target exam + const userRes = await fetch(`http://localhost:8000/api/users/${userId}`); + if (!userRes.ok) throw new Error("Failed to load user target exam"); + const userData = await userRes.json(); + const pattern = userData.target_exam; + setTargetExam(pattern === "NEET" ? "NEET" : "JEE Main"); + + // 2. Fetch tests filtered by exam + const testRes = await fetch(`http://localhost:8000/api/tests?exam_pattern=${pattern}`); + if (!testRes.ok) throw new Error("Failed to fetch tests"); + const testsData = await testRes.json(); + setExams(testsData); + + // 3. Fetch user attempts + const attemptsRes = await fetch(`http://localhost:8000/api/users/${userId}/attempts`); + if (attemptsRes.ok) { + const attemptsData = await attemptsRes.json(); + setAttempts(attemptsData); + } + } catch (err) { + console.error("Error loading exams:", err); + } finally { + setLoading(false); + } + } + loadCBTExams(); + }, []); + + const filteredExams = exams.filter(exam => + exam.title.toLowerCase().includes(search.toLowerCase()) + ); + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' }); + } + + // Get the highest score for each test if attempted multiple times + const attemptsByTest = attempts.reduce((acc, curr) => { + if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) { + acc[curr.source_id] = curr; + } + return acc; + }, {} as Record); return (

Computer Based Tests (CBT)

-

Real exam environment simulations to conquer NEET/JEE.

+

+ Real exam environment simulations to conquer {targetExam || "NEET/JEE"}. +

- + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none" + />
@@ -32,23 +97,49 @@ export default function CBTPage() {

Upcoming & Active Exams

-
- {upcomingExams.map((exam, i) => ( - - - Mock Test -

{exam.title}

-
-
{exam.date}
-
{exam.duration} • {exam.marks} Marks
-
- -
-
- ))} -
+ + {loading ? ( +
Loading test catalog...
+ ) : filteredExams.length === 0 ? ( +
+ No active exams found for {targetExam || "your target exam"}. Try switching it in settings! +
+ ) : ( +
+ {filteredExams.map((exam) => { + const attempt = attemptsByTest[exam.id]; + const isAttempted = !!attempt; + + return ( + + +
+ + {exam.type} Test + + {isAttempted && ( + + Attempted + + )} +
+

+ {exam.title} +

+
+
Available Now
+
{exam.total_time} Min • {exam.difficulty} Difficulty
+
+ + + +
+
+ )})} +
+ )}
@@ -56,29 +147,44 @@ export default function CBTPage() { Past Attempts
- {pastExams.map((exam, i) => ( + {attempts.length === 0 ? ( +
+ You haven't attempted any CBT exams yet. +
+ ) : attempts.map((attempt, i) => { + const exam = exams.find(e => e.id === attempt.source_id); + const scoreCalc = (attempt.score * 4) - ((attempt.total_questions - attempt.score) * 1); // rough calc for display + + return ( - -
-

{exam.title}

-

{exam.date}

+ +
+

{exam?.title || 'Unknown Test'}

+

{formatDate(attempt.submitted_at)}

-
+
-
Score
-
{exam.score}/{exam.total}
+
Score
+
+ {scoreCalc} + /{attempt.total_questions * 4} +
-
Percentile
-
{exam.percentile}
+
Accuracy
+
{Math.round(attempt.accuracy)}%
- + + +
- ))} + )})}
- ) + ); } diff --git a/src/app/dashboard/community/page.tsx b/src/app/dashboard/community/page.tsx new file mode 100644 index 0000000..dd694a7 --- /dev/null +++ b/src/app/dashboard/community/page.tsx @@ -0,0 +1,195 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card" +import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle } from "lucide-react" + +interface Doubt { + id: string + author: string + avatar: string + timeAgo: string + subject: string + topic: string + title: string + content: string + upvotes: number + replies: number + isSolved: boolean + hasModeratorReply: boolean +} + +export default function CommunityPage() { + const [activeFilter, setActiveFilter] = useState<'all' | 'my-doubts' | 'solved' | 'unsolved'>('all') + const [searchQuery, setSearchQuery] = useState('') + + const mockDoubts: Doubt[] = [ + { + id: "1", + author: "Rahul Verma", + avatar: "RV", + timeAgo: "2 hours ago", + subject: "Physics", + topic: "Rotational Mechanics", + title: "Why does friction act forwards in a rolling wheel?", + content: "I'm confused about the direction of friction when a car accelerates forward. My book says friction pushes the wheel forward, but shouldn't it oppose motion?", + upvotes: 42, + replies: 5, + isSolved: true, + hasModeratorReply: true + }, + { + id: "2", + author: "Priya Sharma", + avatar: "PS", + timeAgo: "5 hours ago", + subject: "Chemistry", + topic: "Chemical Kinetics", + title: "How to quickly identify a pseudo first-order reaction?", + content: "Are there any tricks to spot these in MCQs? I always end up wasting 2 minutes trying to derive the rate law.", + upvotes: 18, + replies: 2, + isSolved: false, + hasModeratorReply: false + }, + { + id: "3", + author: "Demo Student", + avatar: "DS", + timeAgo: "1 day ago", + subject: "Mathematics", + topic: "Definite Integration", + title: "Applying King's Rule when limits are -a to a", + content: "I keep messing up the signs when applying the property f(x) + f(-x). Can someone show a step-by-step for the modulus function?", + upvotes: 12, + replies: 8, + isSolved: true, + hasModeratorReply: true + } + ] + + const filteredDoubts = mockDoubts.filter(d => { + if (activeFilter === 'my-doubts' && d.author !== "Demo Student") return false + if (activeFilter === 'solved' && !d.isSolved) return false + if (activeFilter === 'unsolved' && d.isSolved) return false + if (searchQuery && !d.title.toLowerCase().includes(searchQuery.toLowerCase())) return false + return true + }) + + return ( +
+
+
+

+ + Doubt Forum +

+

Ask questions, share knowledge, and learn together with the community.

+
+ + +
+ +
+
+
+ +
+ setSearchQuery(e.target.value)} + /> +
+ +
+ setActiveFilter('all')}>All Doubts + setActiveFilter('my-doubts')}>My Doubts + setActiveFilter('solved')}>Solved + setActiveFilter('unsolved')}>Unsolved +
+
+ +
+ {filteredDoubts.map((doubt) => ( + + + {/* Upvote Sidebar */} +
+ + {doubt.upvotes} +
+ + {/* Main Content */} +
+
+
+ {doubt.subject} + • {doubt.topic} +
+
{doubt.timeAgo}
+
+ +

{doubt.title}

+

{doubt.content}

+ +
+
+
+ {doubt.avatar} +
+ {doubt.author} + {doubt.hasModeratorReply && ( + + Expert Replied + + )} +
+ +
+ {doubt.isSolved && ( + + Solved + + )} +
+ + {doubt.replies} Replies +
+
+
+
+
+
+ ))} + {filteredDoubts.length === 0 && ( +
+ +

No doubts found matching your criteria.

+
+ )} +
+
+ ) +} + +function FilterButton({ children, active, onClick }: { children: React.ReactNode, active: boolean, onClick: () => void }) { + return ( + + ) +} diff --git a/src/app/dashboard/dpp/page.tsx b/src/app/dashboard/dpp/page.tsx index 0b38312..e90726e 100644 --- a/src/app/dashboard/dpp/page.tsx +++ b/src/app/dashboard/dpp/page.tsx @@ -1,29 +1,55 @@ import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" -import { BrainCircuit, Play, CheckCircle2, ChevronRight } from "lucide-react" +import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock } from "lucide-react" export default function DPPPage() { const dpps = [ - { title: "Rotational Dynamics: Top Overturning", subject: "Physics", count: 15, status: "Pending", priority: "High" }, - { title: "Chemical Kinetics: First Order Reactions", subject: "Chemistry", count: 20, status: "Pending", priority: "Medium" }, - { title: "Genetics: Mendelian Inheritance", subject: "Biology", count: 25, status: "Completed", score: "23/25" }, - { title: "Calculus: Definite Integration", subject: "Mathematics", count: 15, status: "Completed", score: "14/15" }, + { title: "Top Overturning", subject: "Physics", topic: "Rotational Dynamics", difficulty: "Hard", count: 15, status: "Pending", priority: "High" }, + { title: "First Order Reactions", subject: "Chemistry", topic: "Chemical Kinetics", difficulty: "Medium", count: 20, status: "Pending", priority: "Medium" }, + { title: "Mendelian Inheritance", subject: "Biology", topic: "Genetics", difficulty: "Easy", count: 25, status: "Completed", score: "23/25" }, + { title: "Definite Integration", subject: "Mathematics", topic: "Calculus", difficulty: "Mixed", count: 15, status: "Completed", score: "14/15" }, ] + const getDifficultyColor = (diff: string) => { + switch(diff) { + case 'Easy': return 'text-emerald-500 bg-emerald-500/10 border-emerald-500/20'; + case 'Medium': return 'text-amber-500 bg-amber-500/10 border-amber-500/20'; + case 'Hard': return 'text-destructive bg-destructive/10 border-destructive/20'; + default: return 'text-primary bg-primary/10 border-primary/20'; + } + } + return (
-
+

Daily Practice Problems (DPP)

Hyper-personalized problem sets generated daily by our AI Evaluator to target your specific weak areas based on past exam performance.

-
+
+
+ Difficulty: + {['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => ( + + ))} +
+ Pro +
+
+ +
+
{dpps.map((dpp, i) => ( @@ -33,14 +59,23 @@ export default function DPPPage() { {dpp.status === 'Completed' ? :
}
-
{dpp.subject}
-

{dpp.title}

+
+ {dpp.subject} + • + {dpp.topic} +
+

+ {dpp.title} + + {dpp.difficulty} + +

{dpp.count} Questions • {dpp.status === 'Completed' ? `Score: ${dpp.score}` : `${dpp.priority} Priority`}
-
+
{dpp.status === 'Completed' ? ( ) : ( diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 28cb838..30584e3 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -5,6 +5,11 @@ import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Tro import Link from "next/link" import Image from "next/image" import { usePathname } from "next/navigation" +import { LanguageToggle } from "@/components/LanguageToggle" +import { ThemeToggle } from "@/components/ThemeToggle" +import { TierBadge } from "@/components/TierBadge" +import { StreakCounter } from "@/components/StreakCounter" +import { AITutorChat } from "@/components/AI/AITutorChat" export default function DashboardLayout({ children }: { children: ReactNode }) { const pathname = usePathname() @@ -50,6 +55,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { } label="Daily Practice" active={pathname.startsWith("/dashboard/dpp")} /> } label="Study Materials" active={pathname.startsWith("/dashboard/materials")} /> +
+

Community & Tracking

+
+ } label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} /> + } label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} /> + } label="Doubt Forum" active={pathname.startsWith("/dashboard/community")} /> +

AI Intelligence

@@ -57,8 +69,24 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { } label="Rank Predictor" active={pathname.startsWith("/dashboard/rank-predictor")} /> -
- } label="Settings" active={pathname.startsWith("/dashboard/settings")} /> +
+
+ + + +
+ + + + Settings + {/* Functional User Dropdown */}
@@ -81,15 +109,15 @@ export default function DashboardLayout({ children }: { children: ReactNode }) { + + +
+
+ +
+
+
+ RANK + STUDENT +
+ SCORE + STREAK +
+
+ +
+ {rankings.map((student) => ( +
+
+ {student.rank === 1 ? : + student.rank === 2 ? : + student.rank === 3 ? : + #{student.rank}} +
+ +
+
+ {student.name.substring(0, 2).toUpperCase()} +
+
+

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

+

+ Accuracy: {student.accuracy} +

+
+
+ +
+
+ {student.score.toLocaleString()} +
+
+ {student.streak} +
+
+
+ ))} +
+
+ +
+ + + Your Standing + + +
#6
+

Top 15% of users

+

+ You are 150 points away from breaking into the Top 5. Complete today's DPS to earn your streak bonus! +

+ +
+ Anonymous Mode + +
+
+
+
+
+
+ ) +} diff --git a/src/app/dashboard/materials/page.tsx b/src/app/dashboard/materials/page.tsx index fe029f1..7b82863 100644 --- a/src/app/dashboard/materials/page.tsx +++ b/src/app/dashboard/materials/page.tsx @@ -6,7 +6,7 @@ export default function MaterialsPage() { { name: "Physics Formula Sheets", items: 12, color: "text-blue-500" }, { name: "Organic Chem Mind Maps", items: 8, color: "text-emerald-500" }, { name: "Biology NCERT Highlights", items: 22, color: "text-rose-500" }, - { name: "Past Year Papers (2015-2025)", items: 10, color: "text-amber-500" }, + { name: "Math Formulae", items: 15, color: "text-amber-500" }, ] const recentFiles = [ diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 7c668bd..d52fbad 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,76 +1,117 @@ +"use client" + +import { useRef } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { Activity, BrainCircuit, ArrowRight, Clock } from "lucide-react" +import { Activity, BrainCircuit, ArrowRight, Clock, Target, CheckCircle2, TrendingUp, Lock, Trophy, Share2 } from "lucide-react" +import * as htmlToImage from 'html-to-image' +import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap" +import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan" export default function DashboardPage() { + const progressRef = useRef(null) + + const handleExport = async () => { + if (progressRef.current) { + try { + const dataUrl = await htmlToImage.toPng(progressRef.current, { backgroundColor: '#09090b' }) + const link = document.createElement('a') + link.download = 'pragativeer-progress.png' + link.href = dataUrl + link.click() + } catch (err) { + console.error("Failed to export image", err) + } + } + } + return (
-
+
-

Welcome back, Student

-

Here is your daily AI-curated study plan.

+

Welcome back, Demo Student

+

Let's keep up the momentum for JEE Advanced 2027.

-
- {/* AI Predicted Weak Areas */} + {/* Free Tier Upgrade CTA Banner */} + +
+ +
+ +
+ Upgrade to Pro +

Unlock Unlimited DPS & Mocks

+

+ You're currently on the Free Tier. Upgrade to Pro for ₹299/mo to access unlimited Daily Practice Sheets, weekly CBT tests, and advanced analytics. +

+
+ +
+
+ + {/* Basic Performance Dashboard (F-05) */}
-

AI Focus Areas

+

Performance Overview

+
-
- - - Rotational Motion - Physics - - -
- Mastery - 42% -
-
-
-
+
+ + +
+

Today's Score

+

42 / 60

+

+5 from yesterday

- - - Chemical Kinetics - Chemistry - - -
- Mastery - 68% -
-
-
-
+ + +
+

Overall Accuracy

+

68%

+

Needs Improvement

- - - Genetics - Biology - - -
- Mastery - 89% -
-
-
-
+ + +
+

Qs Attempted

+

1,245

+

Lifetime total

+
+
+ +
+ +
+

City Rank

+

#42 / 1,200

+

Top 5% in Jaipur

+
+
+ +
+
+ +
+
+
{/* Dynamic DPP Modules */}
@@ -96,19 +137,19 @@ export default function DashboardPage() { - +
-
- +
+
-

Biology: Human Physiology

-

30 Questions • Revision

+

Biology: Human Physiology

+

Pro Feature

- @@ -136,10 +177,10 @@ export default function DashboardPage() {
-

Part Test 3: Mechanics

-

Oct 24, 2026

+

Part Test 3: Mechanics

+

Oct 24, 2026

- + Pro Only
diff --git a/src/app/dashboard/progress/page.tsx b/src/app/dashboard/progress/page.tsx new file mode 100644 index 0000000..93d498e --- /dev/null +++ b/src/app/dashboard/progress/page.tsx @@ -0,0 +1,192 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, Target } from "lucide-react" + +type ChapterStatus = 'Mastered' | 'Practiced' | 'In Progress' | 'Not Started' + +interface Topic { + name: string + status: ChapterStatus + accuracy: number + questionsAttempted: number +} + +interface Chapter { + name: string + topics: Topic[] + isExpanded: boolean +} + +interface Subject { + name: string + chapters: Chapter[] + completionPct: number +} + +const getStatusIcon = (status: ChapterStatus) => { + switch (status) { + case 'Mastered': return + case 'Practiced': return + case 'In Progress': return + case 'Not Started': return + } +} + +export default function ProgressTrackerPage() { + const [subjects, setSubjects] = useState([ + { + name: "Physics", + completionPct: 42, + chapters: [ + { + name: "Kinematics", + isExpanded: true, + topics: [ + { name: "Motion in One Dimension", status: "Mastered", accuracy: 92, questionsAttempted: 150 }, + { name: "Projectile Motion", status: "Practiced", accuracy: 75, questionsAttempted: 85 }, + { name: "Relative Velocity", status: "In Progress", accuracy: 45, questionsAttempted: 20 }, + ] + }, + { + name: "Laws of Motion", + isExpanded: false, + topics: [ + { name: "Newton's First Law", status: "Practiced", accuracy: 88, questionsAttempted: 40 }, + { name: "Friction", status: "In Progress", accuracy: 50, questionsAttempted: 35 }, + ] + }, + { + name: "Thermodynamics", + isExpanded: false, + topics: [ + { name: "First Law", status: "Not Started", accuracy: 0, questionsAttempted: 0 }, + { name: "Carnot Engine", status: "Not Started", accuracy: 0, questionsAttempted: 0 }, + ] + } + ] + }, + { + name: "Chemistry", + completionPct: 68, + chapters: [ + { + name: "Atomic Structure", + isExpanded: false, + topics: [ + { name: "Bohr Model", status: "Mastered", accuracy: 95, questionsAttempted: 110 }, + { name: "Quantum Numbers", status: "Mastered", accuracy: 88, questionsAttempted: 95 }, + ] + } + ] + }, + { + name: "Mathematics", + completionPct: 22, + chapters: [ + { + name: "Calculus", + isExpanded: false, + topics: [ + { name: "Limits", status: "In Progress", accuracy: 60, questionsAttempted: 45 }, + { name: "Derivatives", status: "Not Started", accuracy: 0, questionsAttempted: 0 }, + ] + } + ] + } + ]) + + const toggleChapter = (subjectIndex: number, chapterIndex: number) => { + const newSubjects = [...subjects] + newSubjects[subjectIndex].chapters[chapterIndex].isExpanded = !newSubjects[subjectIndex].chapters[chapterIndex].isExpanded + setSubjects(newSubjects) + } + + return ( +
+
+

+ + Syllabus Tracker +

+

Track your syllabus coverage and mastery per chapter to ensure no gaps.

+
+ +
+ {subjects.map((sub, i) => ( + + +

{sub.name}

+
{sub.completionPct}%
+
+
+
+
+
+ ))} +
+ +
+ {subjects.map((subject, subIndex) => ( + + + {subject.name} Syllabus + + +
+ {subject.chapters.map((chapter, chapIndex) => ( +
+ + + {chapter.isExpanded && ( +
+
+ TOPIC + ACCURACY + QUESTIONS +
+ {chapter.topics.map((topic, topIndex) => ( +
+
+ {getStatusIcon(topic.status)} +
+

{topic.name}

+

{topic.status}

+
+
+
+ {topic.accuracy > 0 ? ( + 80 ? 'text-emerald-500' : topic.accuracy > 60 ? 'text-amber-500' : 'text-red-500'}`}> + {topic.accuracy}% + + ) : -} +
+
+ {topic.questionsAttempted > 0 ? topic.questionsAttempted : '-'} +
+
+ ))} +
+ )} +
+ ))} +
+
+
+ ))} +
+
+ ) +} diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index f3dc1f0..600df78 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -1,7 +1,69 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; export default function SettingsPage() { + const [targetExam, setTargetExam] = useState<"JEE_Main" | "NEET">("JEE_Main"); + const [targetYear, setTargetYear] = useState("2026"); + const [firstName, setFirstName] = useState("Rahul"); + const [lastName, setLastName] = useState("Sharma"); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const userId = "b0e17378-864d-439e-8806-708dc48a891b"; + + useEffect(() => { + async function fetchUserData() { + try { + const res = await fetch(`http://localhost:8000/api/users/${userId}`); + if (res.ok) { + const data = await res.json(); + setTargetExam(data.target_exam === "NEET" ? "NEET" : "JEE_Main"); + if (data.name) { + const parts = data.name.split(" "); + setFirstName(parts[0] || ""); + setLastName(parts.slice(1).join(" ") || ""); + } + } + } catch (err) { + console.error("Failed to load user profile:", err); + } finally { + setLoading(false); + } + } + fetchUserData(); + }, []); + + const handleSave = async () => { + setSaving(true); + try { + const res = await fetch(`http://localhost:8000/api/users/${userId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: `${firstName} ${lastName}`.trim(), + target_exam: targetExam, + }), + }); + if (res.ok) { + alert("Settings saved successfully!"); + } else { + alert("Failed to save settings: " + (await res.text())); + } + } catch (err) { + console.error("Error saving settings:", err); + alert("Network error. Could not save settings."); + } finally { + setSaving(false); + } + }; + + if (loading) { + return
Loading settings...
; + } + return (
@@ -19,18 +81,30 @@ export default function SettingsPage() {
- + setFirstName(e.target.value)} + className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]" + />
- + setLastName(e.target.value)} + className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]" + />
- +
- + @@ -43,26 +117,61 @@ export default function SettingsPage() {
-
- setTargetYear(e.target.value)} + className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF] appearance-none font-medium" + >
+
- ) + ); } diff --git a/src/app/dashboard/streaks/page.tsx b/src/app/dashboard/streaks/page.tsx new file mode 100644 index 0000000..1af821b --- /dev/null +++ b/src/app/dashboard/streaks/page.tsx @@ -0,0 +1,99 @@ +"use client" + +import { Card, CardContent } from "@/components/ui/card" +import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react" + +export default function StreaksPage() { + const currentStreak = 12; + const bestStreak = 45; + + const rewards = [ + { streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: true }, + { streak: 14, title: "Dedicated Scholar", reward: "AI Performance Analysis Report", unlocked: false }, + { streak: 30, title: "Exam Warrior", reward: "1 Full Length Mock Test", unlocked: false }, + { streak: 50, title: "PragatiVeer Champion", reward: "1-on-1 AI Tutor Session", unlocked: false }, + { streak: 100, title: "Unstoppable", reward: "Merch + 50% Off Next Renewal", unlocked: false }, + ]; + + return ( +
+
+
+ +
+
+

Your Learning Streak

+

Build daily habits to unlock premium rewards and boost your rank.

+
+
+ +
+ + + +

{currentStreak} Days

+

CURRENT STREAK

+

Complete a Daily Practice Problem (DPP) or CBT Exam every day to keep your streak alive.

+
+
+ + + +
+
+

STREAK STATS

+ +
+
+
+

Personal Best

+

{bestStreak} Days

+
+
+

Total Streak Points Earned

+

1,450

+
+
+
+ +
+

+ Pro Tip: Your streak directly multiplies your leaderboard score. A longer streak gives you a higher Consistency Bonus. +

+
+
+
+
+ +
+

Streak Rewards

+
+ {rewards.map((reward, i) => ( +
+
+
+ {reward.streak} +
+
+

{reward.title}

+

{reward.reward}

+
+
+
+ {reward.unlocked ? ( +
+ Unlocked +
+ ) : ( +
+ {reward.streak - currentStreak} days to go +
+ )} +
+
+ ))} +
+
+
+ ) +} diff --git a/src/app/globals.css b/src/app/globals.css index 80a63b8..81c049d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -111,3 +111,19 @@ body { 30% { transform: translateX(200%) skewX(-15deg); } 100% { transform: translateX(200%) skewX(-15deg); } } + +/* Custom Scrollbars */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: var(--background); +} +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5adc7bb..51f5ef6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -27,6 +27,7 @@ export default function RootLayout({ {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 127216c..28a88e3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -131,7 +131,7 @@ export default function MarketingPage() { - @@ -413,16 +413,17 @@ export default function MarketingPage() { transition={{ duration: 0.6, type: "spring" }} > -

Basic

-

Perfect for self-paced revision.

-
₹999/mo
+

Pro

+

Perfect for self-paced practice.

+
₹299/mo
    -
  • 10 Full Syllabus CBTs
  • -
  • Standard Question Bank
  • -
  • Basic Analytics
  • +
  • Unlimited Daily Practice Sheets
  • +
  • Weekly Test Series & Mock Tests
  • +
  • Detailed Performance Analytics
  • +
  • Parent Dashboard Access
- +
@@ -440,17 +441,17 @@ export default function MarketingPage() { MOST POPULAR
-

AI Pro

+

AI Coach

The complete AI-driven preparation system.

-
₹2499/mo
+
₹599/mo
    -
  • Unlimited Dynamic CBTs
  • -
  • Adaptive AI DPPs
  • -
  • Interactive AI Tutor (24/7)
  • -
  • Predictive Rank Analytics
  • +
  • Everything in Pro
  • +
  • AI Spaced Repetition (Mistakes)
  • +
  • 24/7 Streaming AI Tutor Chat
  • +
  • Predictive Rank & College Predictor
- +
diff --git a/src/app/parent/dashboard/page.tsx b/src/app/parent/dashboard/page.tsx new file mode 100644 index 0000000..5e2d5c6 --- /dev/null +++ b/src/app/parent/dashboard/page.tsx @@ -0,0 +1,217 @@ +"use client" + +import { useState, useEffect } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Switch } from "@/components/ui/switch" +import { Label } from "@/components/ui/label" +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts" +import { Activity, Clock, Flame, AlertTriangle, RefreshCw } from "lucide-react" + +// Hardcoded mock UUIDs for this sprint +const PARENT_ID = "parent-123" +const STUDENT_ID = "b0e17378-864d-439e-8806-708dc48a891b" + +const DICTIONARY = { + en: { + title: "Parent Monitoring Portal", + subtitle: "Real-time analytics for your child's learning journey.", + lang_toggle: "हिंदी", + time_spent: "Weekly Time Spent", + minutes: "Minutes", + streak: "Active Streak", + days: "Days", + tests_attempted: "Tests Attempted", + tests: "This week", + accuracy_trend: "7-Day Accuracy Trend", + areas_of_concern: "Areas of Concern", + attention_needed: "Attention Needed", + pending_errors: "pending errors", + loading: "Loading child data...", + no_concerns: "No current areas of concern. Great job!" + }, + hi: { + title: "अभिभावक निगरानी पोर्टल", + subtitle: "आपके बच्चे की सीखने की यात्रा के लिए वास्तविक समय विश्लेषिकी।", + lang_toggle: "English", + time_spent: "साप्ताहिक समय व्यतीत", + minutes: "मिनट", + streak: "सक्रिय लकीर", + days: "दिन", + tests_attempted: "परीक्षण का प्रयास", + tests: "इस सप्ताह", + accuracy_trend: "7-दिवसीय सटीकता प्रवृत्ति", + areas_of_concern: "चिंता के विषय", + attention_needed: "ध्यान देने की आवश्यकता है", + pending_errors: "लंबित त्रुटियां", + loading: "बच्चे का डेटा लोड हो रहा है...", + no_concerns: "चिंता का कोई विषय नहीं है। बहुत बढ़िया!" + } +} + +export default function ParentDashboard() { + const [lang, setLang] = useState<'en' | 'hi'>('en') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + const t = DICTIONARY[lang] + + useEffect(() => { + async function fetchDashboard() { + try { + const res = await fetch(`http://localhost:8000/api/parent/${PARENT_ID}/dashboard/${STUDENT_ID}`) + if (!res.ok) throw new Error("Failed to fetch") + const payload = await res.json() + setData(payload) + } catch (err) { + console.error(err) + // Fallback mock data if server fails + setData({ + student_name: "Rahul", + streak_count: 5, + weekly_minutes_spent: 124.5, + total_tests_attempted_this_week: 3, + accuracy_trend: [ + { date: "Jul 01", accuracy: 40 }, + { date: "Jul 02", accuracy: 55 }, + { date: "Jul 03", accuracy: 50 }, + { date: "Jul 04", accuracy: 70 }, + { date: "Jul 05", accuracy: 65 }, + { date: "Jul 06", accuracy: 80 }, + { date: "Jul 07", accuracy: 85 } + ], + areas_of_concern: [ + { topic: "Thermodynamics", pending_errors: 4 }, + { topic: "Integration", pending_errors: 2 } + ] + }) + } finally { + setLoading(false) + } + } + fetchDashboard() + }, []) + + if (loading) { + return ( +
+ {t.loading} +
+ ) + } + + return ( +
+ + {/* Header & Language Toggle */} +
+
+

+ {t.title} +

+

{t.subtitle} — {data?.student_name}

+
+
+ + setLang(c ? 'hi' : 'en')} + /> +
+
+ + {/* Metric Blocks */} +
+ + +
+
+

{t.time_spent}

+

{data?.weekly_minutes_spent} {t.minutes}

+
+
+
+ + +
+
+

{t.streak}

+

{data?.streak_count} {t.days}

+
+
+
+ + +
+
+

{t.tests_attempted}

+

{data?.total_tests_attempted_this_week} {t.tests}

+
+
+
+
+ + {/* Main Grid */} +
+ + {/* Chart */} + + + {t.accuracy_trend} + + +
+ + + + + `${val}%`} /> + + + + +
+
+
+ + {/* Areas of Concern */} + + + + + {t.areas_of_concern} + + + + {!data?.areas_of_concern || data.areas_of_concern.length === 0 ? ( +
+

{t.no_concerns}

+
+ ) : ( + data.areas_of_concern.map((concern: any, idx: number) => ( +
+
+

{concern.topic}

+

+ {t.attention_needed} • {concern.pending_errors} {t.pending_errors} +

+
+ )) + )} +
+
+ +
+
+ ) +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 03fcacb..7ee3711 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -2,13 +2,24 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { ArrowLeft, Eye, EyeOff } from "lucide-react" +import { ArrowLeft, Eye, EyeOff, CheckCircle2 } from "lucide-react" import Link from "next/link" import Image from "next/image" import { useState } from "react" +import { useRouter } from "next/navigation" export default function SignupPage() { + const [step, setStep] = useState(1) const [showPassword, setShowPassword] = useState(false) + const [authMethod, setAuthMethod] = useState<"email" | "phone">("email") + const [showOTP, setShowOTP] = useState(false) + const router = useRouter() + + const handleNextStep = () => { + if (step === 1) setStep(2) + else router.push("/dashboard") + } + return (
@@ -29,85 +40,149 @@ export default function SignupPage() {
+ {/* Progress Bar */} +
+
+ = 1 ? "text-primary" : ""}>Account Details + = 2 ? "text-primary" : ""}>Personalize Profile +
+
+
+
+
+ - Create an account - Start your AI-powered preparation journey today. + + {step === 1 ? "Create an account" : "Complete your profile"} + + + {step === 1 ? "Start your AI-powered preparation journey today." : "Help us personalize your study plan."} + -
-
-
- - + + {step === 1 ? ( +
+
+
+ + +
+
+ + +
+ +
+ + +
+
- - + +
+ + {!showOTP && ( + + )} +
+
+ + {showOTP && ( +
+ +
+ {[1,2,3,4,5,6].map((i) => ( + + ))} +
+
+ )} + +
+ +
+ + +
-
- - + ) : ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
- + )} + + + + {step === 1 && ( + <>
- - +
+ +
+
+ Or continue with +
-
-
- - - - -
-
- -
-
- Or continue with -
-
- - - - + + + )} -

- Already have an account? Log in -

+ {step === 1 ? ( +

+ Already have an account? Log in +

+ ) : ( + + )}
) diff --git a/src/app/upgrade/page.tsx b/src/app/upgrade/page.tsx new file mode 100644 index 0000000..c828086 --- /dev/null +++ b/src/app/upgrade/page.tsx @@ -0,0 +1,226 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Check, X, ShieldCheck, Loader2 } from "lucide-react" + +const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b" +const WEBHOOK_SECRET = "local_webhook_secret" + +export default function UpgradePage() { + const [loadingTier, setLoadingTier] = useState(null) + const [showModal, setShowModal] = useState(false) + const [orderData, setOrderData] = useState(null) + const [processing, setProcessing] = useState(false) + const [success, setSuccess] = useState(false) + + const handleUpgrade = async (tier: string) => { + setLoadingTier(tier) + try { + const res = await fetch(`http://localhost:8000/api/billing/create-order?user_id=${USER_ID}&plan_tier=${tier}`, { + method: "POST" + }) + if (!res.ok) throw new Error("Order creation failed") + const order = await res.json() + setOrderData({ ...order, tier }) + setShowModal(true) + } catch (err) { + console.error(err) + alert("Failed to create order") + } finally { + setLoadingTier(null) + } + } + + const generateMockSignature = async (payloadStr: string) => { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(WEBHOOK_SECRET), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payloadStr)); + return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join(''); + } + + const simulatePayment = async () => { + setProcessing(true) + try { + const payloadObj = { + event: "payment.captured", + payload: { + payment: { + entity: { + id: `pay_mock_${Date.now()}`, + amount: orderData.amount, + notes: { + user_id: USER_ID, + tier_requested: orderData.tier + } + } + } + } + } + + const payloadStr = JSON.stringify(payloadObj) + const signature = await generateMockSignature(payloadStr) + + const res = await fetch(`http://localhost:8000/api/billing/webhook/razorpay`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Razorpay-Signature": signature + }, + body: payloadStr + }) + + if (!res.ok) throw new Error("Webhook failed") + + setSuccess(true) + setTimeout(() => { + window.location.href = "/dashboard" + }, 2000) + + } catch (err) { + console.error(err) + alert("Payment simulation failed") + setProcessing(false) + } + } + + return ( +
+
+

Upgrade Your Preparation

+

Unlock the full power of PragatiVeer to master JEE & NEET.

+
+ +
+ {/* Free Tier */} + + + Free + Basic access for everyone. +
₹0/mo
+
+ +
Subject-wise diagnostics
+
Basic progress tracking
+
Unlimited Tests
+
AI Spaced Repetition
+
Parent Analytics
+
+ + + +
+ + {/* Pro Tier */} + +
Most Popular
+ + Pro + For serious aspirants. +
₹299/mo
+
+ +
Everything in Free
+
Unlimited Sectional Tests
+
Performance Analytics
+
Parent Dashboard
+
AI Mistake Heatmap
+
Spaced Repetition (SM-2)
+
+ + + +
+ + {/* AI Coach Tier */} + + + AI Coach + Personalized mastery loop. +
₹599/mo
+
+ +
Everything in Pro
+
Daily AI Error Review Queue
+
SM-2 Spaced Repetition
+
Mistake Heatmap Tracking
+
AI Chat Tutor Support
+
+ + + +
+
+ + {/* Mock Razorpay Modal */} + {showModal && ( +
+ + +
+ RZP +
+ Mock Checkout Window + Razorpay Test Sandbox Simulation +
+ +
+
+ Order ID: + {orderData?.id} +
+
+ Amount: + ₹{(orderData?.amount / 100).toFixed(2)} +
+
+ Tier: + {orderData?.tier} +
+
+ + {success ? ( +
+ Payment Captured! Redirecting... +
+ ) : ( +

+ Clicking confirm will securely sign and POST a payment.captured webhook to the backend. +

+ )} +
+ + + + +
+
+ )} + +
+ ) +} diff --git a/src/components/AI/AITutorChat.tsx b/src/components/AI/AITutorChat.tsx new file mode 100644 index 0000000..c61a00c --- /dev/null +++ b/src/components/AI/AITutorChat.tsx @@ -0,0 +1,138 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Send, Bot, User, Minimize2, Maximize2, X } from "lucide-react" +import ReactMarkdown from 'react-markdown' +import remarkMath from 'remark-math' +import rehypeKatex from 'rehype-katex' +import 'katex/dist/katex.min.css' // Import Katex CSS for math rendering + +interface Message { + id: string; + role: 'user' | 'ai'; + content: string; +} + +const INITIAL_MESSAGES: Message[] = [ + { + id: '1', + role: 'ai', + content: "Hi! I noticed you struggled with **Rotational Dynamics** in the last test. Let's look at a similar problem. \n\nA solid sphere rolls without slipping. The kinetic energy is $E = \\frac{1}{2}mv^2 + \\frac{1}{2}I\\omega^2$. \n\nWhat fraction of total kinetic energy is rotational?" + }, + { + id: '2', + role: 'user', + content: "I think it is $\\frac{2}{7}$?" + }, + { + id: '3', + role: 'ai', + content: "Excellent! Yes, because $I = \\frac{2}{5}mr^2$, the rotational kinetic energy is:\n\n$$ K_{rot} = \\frac{1}{2}(\\frac{2}{5}mr^2)(\\frac{v}{r})^2 = \\frac{1}{5}mv^2 $$\n\nTotal $K = \\frac{1}{2}mv^2 + \\frac{1}{5}mv^2 = \\frac{7}{10}mv^2$.\n\nSo, $\\frac{K_{rot}}{K_{total}} = \\frac{2/10}{7/10} = \\frac{2}{7}$. You nailed it!" + } +]; + +export function AITutorChat() { + const [messages, setMessages] = useState(INITIAL_MESSAGES); + const [input, setInput] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + + const handleSend = () => { + if (!input.trim()) return; + const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input }; + setMessages([...messages, newMessage]); + setInput(''); + + // Mock AI response + setTimeout(() => { + setMessages(prev => [...prev, { + id: (Date.now() + 1).toString(), + role: 'ai', + content: "That's an interesting point! Let's explore the math behind it: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$" + }]); + }, 1000); + }; + + if (!isOpen) { + return ( + + ) + } + + return ( + + +
+
+ +
+
+ AI Coach +

+ Online +

+
+
+
+ + +
+
+ + + {messages.map((msg) => ( +
+
+ {msg.role === 'user' ? : } +
+ + {/* Markdown Container with Overflow Handling */} +
+
+ + {msg.content} + +
+
+
+ ))} +
+ + +
{ e.preventDefault(); handleSend(); }} className="flex w-full items-center gap-2"> + setInput(e.target.value)} + className="flex-1 bg-secondary/30 border-border focus-visible:ring-primary h-12 rounded-xl" + /> + +
+
+
+ ) +} diff --git a/src/components/AI/AdaptiveStudyPlan.tsx b/src/components/AI/AdaptiveStudyPlan.tsx new file mode 100644 index 0000000..89f4a04 --- /dev/null +++ b/src/components/AI/AdaptiveStudyPlan.tsx @@ -0,0 +1,103 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react" + +type PlanState = 'PENDING' | 'DONE' | 'SKIPPED'; + +interface PlanItem { + id: string; + title: string; + duration: string; + type: string; + status: PlanState; +} + +const INITIAL_PLAN: PlanItem[] = [ + { id: '1', title: 'Revise Thermodynamics Concepts', duration: '45 min', type: 'Theory', status: 'PENDING' }, + { id: '2', title: 'Solve 15 Rotational Dynamics DPPs', duration: '30 min', type: 'Practice', status: 'PENDING' }, + { id: '3', title: 'Review Mistakes in Optics (T-3)', duration: '20 min', type: 'Review', status: 'PENDING' }, +]; + +export function AdaptiveStudyPlan() { + const [items, setItems] = useState(INITIAL_PLAN); + + const updateStatus = (id: string, newStatus: PlanState) => { + setItems(items.map(item => item.id === id ? { ...item, status: newStatus } : item)); + }; + + const totalTime = items.reduce((acc, curr) => { + if (curr.status === 'DONE' || curr.status === 'PENDING') { + return acc + parseInt(curr.duration); + } + return acc; + }, 0); + + return ( + + +
+ + Adaptive Study Plan + +
+ Today: {totalTime} min +
+
+

Dynamically generated based on your weak areas.

+
+ +
+ {items.map((item, index) => { + const isDone = item.status === 'DONE'; + const isSkipped = item.status === 'SKIPPED'; + + return ( +
+ {/* Timeline Icon */} +
+ {isDone ? : isSkipped ? :
} +
+ + {/* Card */} +
+
+ + {item.type} + + {item.duration} +
+

+ {item.title} +

+ + {/* Actions */} +
+ {item.status === 'PENDING' ? ( + <> + + + + ) : ( +
+ {isDone ? 'Completed' : 'Rescheduled'} +
+ )} +
+
+
+ ) + })} +
+ + + ) +} diff --git a/src/components/AI/CollegePredictorCard.tsx b/src/components/AI/CollegePredictorCard.tsx new file mode 100644 index 0000000..b51cc38 --- /dev/null +++ b/src/components/AI/CollegePredictorCard.tsx @@ -0,0 +1,84 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { GraduationCap, MapPin, RefreshCw } from "lucide-react" + +const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b" + +export function CollegePredictorCard() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + async function fetchReadiness() { + try { + const res = await fetch(`http://localhost:8000/api/ai-coach/readiness/${USER_ID}`) + if (!res.ok) throw new Error("Failed to fetch") + const json = await res.json() + setData(json) + } catch (err) { + console.warn("Using mock fallback data for College Predictor.") + setData({ + college_prediction: "NIT Trichy / NIT Surathkal on track.", + prediction_category: "Target" + }) + } finally { + setLoading(false) + } + } + fetchReadiness() + }, []) + + if (loading) { + return ( + + + + ) + } + + const prediction = data?.college_prediction || "Keep Practicing" + const category = data?.prediction_category || "State Level" + + const getCategoryColor = (cat: string) => { + switch(cat.toUpperCase()) { + case "REACH": return "bg-purple-500/10 text-purple-600 border-purple-500/20" + case "TARGET": return "bg-blue-500/10 text-blue-600 border-blue-500/20" + case "SAFETY": return "bg-green-500/10 text-green-600 border-green-500/20" + default: return "bg-secondary text-muted-foreground border-transparent" + } + } + + return ( + +
+ +
+ + + +
+ + Admissions Radar +
+ + {category} + +
+ AI-projected admission trajectory. +
+ + +
+

+ "{prediction}" +

+
+

+ Prediction updates dynamically after every test. +

+
+
+ ) +} diff --git a/src/components/AI/MistakeHeatmap.tsx b/src/components/AI/MistakeHeatmap.tsx new file mode 100644 index 0000000..8901a00 --- /dev/null +++ b/src/components/AI/MistakeHeatmap.tsx @@ -0,0 +1,80 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Flame, Info } from "lucide-react" + +const HEATMAP_DATA = [ + { topic: "Rotational Dynamics", attempts: [80, 90, 40, 20, 10] }, // 10 is critical error rate + { topic: "Electromagnetism", attempts: [30, 25, 20, 15, 10] }, + { topic: "Thermodynamics", attempts: [5, 10, 5, 2, 0] }, + { topic: "Optics", attempts: [60, 50, 45, 60, 55] }, + { topic: "Modern Physics", attempts: [10, 5, 2, 0, 0] } +]; + +export function MistakeHeatmap() { + const getColor = (errorRate: number) => { + if (errorRate >= 50) return 'bg-red-500/80 border-red-500 shadow-[0_0_10px_-2px_rgba(239,68,68,0.5)]'; // Critical + if (errorRate >= 30) return 'bg-amber-500/60 border-amber-500'; // Warning + if (errorRate >= 10) return 'bg-yellow-500/30 border-yellow-500/50'; // Moderate + return 'bg-green-500/20 border-green-500/30'; // Good + }; + + return ( + + +
+ + Mistake Heatmap + + +
+

Topic-wise error rate across last 5 tests.

+
+ +
+
+ {/* Header row */} +
+
Topic
+
+ T-4 + T-3 + T-2 + T-1 + Latest +
+
+ + {/* Data rows */} +
+ {HEATMAP_DATA.map((row, idx) => ( +
+
+ {row.topic} +
+
+ {row.attempts.map((errRate, i) => ( +
+ {errRate}% +
+ ))} +
+
+ ))} +
+
+
+ + {/* Legend */} +
+
{"< 10%"}
+
{"10-30%"}
+
{"30-50%"}
+
{"> 50%"}
+
+
+
+ ) +} diff --git a/src/components/AI/MistakeReviewCard.tsx b/src/components/AI/MistakeReviewCard.tsx new file mode 100644 index 0000000..ac110f6 --- /dev/null +++ b/src/components/AI/MistakeReviewCard.tsx @@ -0,0 +1,169 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { CheckCircle2, XCircle, BrainCircuit } from "lucide-react" +import ReactMarkdown from 'react-markdown' +import remarkMath from 'remark-math' +import rehypeKatex from 'rehype-katex' +import 'katex/dist/katex.min.css' + +export interface MistakePayload { + mistake: { + id: string + user_id: string + question_id: string + error_type: string + topic: string + subtopic: string + review_count: number + consecutive_correct: number + is_mastered: boolean + } + question: { + id: string + subject: string + question_text: string + options: Record + correct_answer: { correct: string } + solution_text: string + } +} + +interface MistakeReviewCardProps { + data: MistakePayload + onNext: () => void + onSubmitReview: (mistakeId: string, selectedAnswer: string) => Promise<{is_correct: boolean, correct_answer: string}> +} + +export function MistakeReviewCard({ data, onNext, onSubmitReview }: MistakeReviewCardProps) { + const [selected, setSelected] = useState(null) + const [result, setResult] = useState<'success' | 'error' | null>(null) + const [correctAnswer, setCorrectAnswer] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + + const handleCheck = async () => { + if (!selected) return; + setIsSubmitting(true) + + try { + const res = await onSubmitReview(data.mistake.id, selected) + setResult(res.is_correct ? 'success' : 'error') + setCorrectAnswer(res.correct_answer) + } catch (err) { + console.error(err) + } finally { + setIsSubmitting(false) + } + } + + const handleNext = () => { + setSelected(null) + setResult(null) + setCorrectAnswer(null) + onNext() + } + + const { mistake, question } = data; + + return ( + +
+
+ + Attempt {mistake.review_count + 1} + + {mistake.topic} • {mistake.subtopic} +
+
+ 0 ? "text-amber-500" : ""} /> + Consecutive Correct: {mistake.consecutive_correct}/3 before mastery +
+
+ + +
+ + {question.question_text} + +
+ +
+ {Object.entries(question.options).map(([letter, text]) => { + const isSelected = selected === letter + + let btnClass = "border-border bg-background hover:border-primary/50 hover:bg-secondary/20" + let iconClass = "border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary" + + if (result) { + // Post-check styling + if (letter === correctAnswer) { + btnClass = "border-green-500 bg-green-500/10 shadow-[0_0_15px_-5px_#22c55e]" + iconClass = "border-green-500 bg-green-500 text-white" + } else if (isSelected && letter !== correctAnswer) { + btnClass = "border-red-500 bg-red-500/10" + iconClass = "border-red-500 bg-red-500 text-white" + } + } else if (isSelected) { + btnClass = "border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]" + iconClass = "border-primary bg-primary text-primary-foreground" + } + + return ( + + ) + })} +
+ +
+ {!result ? ( + + ) : ( + + )} +
+ + {/* Solution Block */} + {result && ( +
+

AI Coach Explanation

+
+ + {question.solution_text} + +
+
+ )} +
+
+ ) +} diff --git a/src/components/AI/ProgressShare.tsx b/src/components/AI/ProgressShare.tsx new file mode 100644 index 0000000..50590a9 --- /dev/null +++ b/src/components/AI/ProgressShare.tsx @@ -0,0 +1,64 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Download, Image as ImageIcon, Loader2 } from "lucide-react" +import { useState } from "react" + +export function ProgressShare() { + const [downloading, setDownloading] = useState(false) + + // Example dynamic params + const streak = "12" + const accuracy = "85" + const name = "Student" + const imageUrl = `/api/og/progress?streak=${streak}&accuracy=${accuracy}&name=${name}` + + const handleDownload = async () => { + try { + setDownloading(true) + const res = await fetch(imageUrl) + const blob = await res.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement("a") + a.style.display = "none" + a.href = url + a.download = "PragatiVeer_Progress.png" + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + } catch (err) { + console.error("Failed to download image", err) + } finally { + setDownloading(false) + } + } + + return ( + + + + + Share Progress + + Auto-generated dynamic scorecard. + + + +
+ Progress Scorecard +
+ + +
+
+ ) +} diff --git a/src/components/AI/ReadinessGauge.tsx b/src/components/AI/ReadinessGauge.tsx new file mode 100644 index 0000000..63e9630 --- /dev/null +++ b/src/components/AI/ReadinessGauge.tsx @@ -0,0 +1,120 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Activity, RefreshCw } from "lucide-react" + +const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b" + +export function ReadinessGauge() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + async function fetchReadiness() { + try { + const res = await fetch(`http://localhost:8000/api/ai-coach/readiness/${USER_ID}`) + if (!res.ok) throw new Error("Failed to fetch") + const json = await res.json() + setData(json) + } catch (err) { + console.warn("Using mock fallback data for Readiness Gauge.") + setData({ + score: 72, + breakdown: { + accuracy: 30, + speed: 15, + consistency: 8, + recovery: 5, + coverage: 14 + } + }) + } finally { + setLoading(false) + } + } + fetchReadiness() + }, []) + + if (loading) { + return ( + + + + ) + } + + const score = data?.score || 0 + const breakdown = data?.breakdown || {} + const circumference = 2 * Math.PI * 45 // r=45 + const strokeDashoffset = circumference - (score / 100) * circumference + + const getColor = (s: number) => { + if (s >= 75) return "text-green-500" + if (s >= 50) return "text-yellow-500" + return "text-red-500" + } + + return ( + + + + + Exam Readiness + + Real-time AI composite score. + + + + + {/* SVG Circular Gauge */} +
+ + {/* Background Ring */} + + {/* Progress Ring */} + + +
+ {score} + / 100 +
+
+ + {/* Breakdown Bars */} +
+ {[ + { label: "Accuracy", value: breakdown.accuracy, max: 40 }, + { label: "Speed", value: breakdown.speed, max: 20 }, + { label: "Coverage", value: breakdown.coverage, max: 20 }, + { label: "Consistency", value: breakdown.consistency, max: 10 }, + { label: "Recovery", value: breakdown.recovery, max: 10 }, + ].map((item, idx) => ( +
+
+ {item.label} + {item.value} / {item.max} +
+
+
+
+
+ ))} +
+ + + + ) +} diff --git a/src/components/AI/ReferralCard.tsx b/src/components/AI/ReferralCard.tsx new file mode 100644 index 0000000..6741f5b --- /dev/null +++ b/src/components/AI/ReferralCard.tsx @@ -0,0 +1,72 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Link as LinkIcon, Share2, Check, Gift } from "lucide-react" + +const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b" + +export function ReferralCard() { + const [data, setData] = useState(null) + const [copied, setCopied] = useState(false) + + useEffect(() => { + async function fetchReferral() { + try { + const res = await fetch(`http://localhost:8000/api/growth/referral/${USER_ID}`) + if (res.ok) { + const json = await res.json() + setData(json) + } + } catch (err) { + console.warn("Using mock fallback data for Referral Card.") + } + } + fetchReferral() + }, []) + + const link = data?.referral_link || "https://pragativeer.com/join/demo" + const message = data?.share_message || "Join me on PragatiVeer! Use my link to get 7 days of the AI Coach for free." + + const copyToClipboard = () => { + navigator.clipboard.writeText(link) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + const shareToWhatsApp = () => { + const waUrl = `https://wa.me/?text=${encodeURIComponent(message)}` + window.open(waUrl, "_blank") + } + + return ( + +
+ +
+ + + + + Invite & Earn + + Give 7 days of AI Coach, get 7 days free. + + + +
+ {link} + +
+ + +
+
+ ) +} diff --git a/src/components/AI/StudyPlanCard.tsx b/src/components/AI/StudyPlanCard.tsx new file mode 100644 index 0000000..abf3bbc --- /dev/null +++ b/src/components/AI/StudyPlanCard.tsx @@ -0,0 +1,131 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { CalendarClock, CheckCircle2, Circle, Clock, Loader2, PlayCircle, RefreshCw, XCircle } from "lucide-react" + +const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b" + +export function StudyPlanCard() { + const [tasks, setTasks] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + async function fetchPlan() { + try { + const res = await fetch(`http://localhost:8000/api/ai-coach/study-plan/today?user_id=${USER_ID}`) + if (!res.ok) throw new Error("No plan") + const data = await res.json() + setTasks(data.tasks || []) + } catch (err) { + console.error(err) + // Mock fallback if celery hasn't seeded yet + setTasks([ + { type: "Revision", description: "Revise Thermodynamics", duration_minutes: 15, status: "Pending" }, + { type: "Mistake_Review", description: "Daily Mistake Review Session", duration_minutes: 10, status: "Pending" }, + { type: "DPS", description: "Attempt Thermodynamics Practice Sheet", duration_minutes: 20, status: "Pending" } + ]) + } finally { + setLoading(false) + } + } + fetchPlan() + }, []) + + const toggleTask = (idx: number, newStatus: string) => { + setTasks(prev => { + const copy = [...prev] + copy[idx] = { ...copy[idx], status: newStatus } + return copy + }) + } + + return ( + + +
+ + + Today's Plan + + + {tasks.reduce((acc, curr) => acc + curr.duration_minutes, 0)} min total + +
+ AI-generated curriculum targeting your weak spots. +
+ + {loading ? ( +
+ Loading schedule... +
+ ) : ( +
+ {tasks.map((task, idx) => { + const isDone = task.status === "Done" + const isSkipped = task.status === "Skipped" + const isPending = task.status === "Pending" + + return ( +
+
+
+ {task.type} + + {task.duration_minutes} min + +
+

+ {task.description} +

+
+ + {/* Controls */} +
+ {isPending && ( + <> + + + + )} + {(isDone || isSkipped) && ( + + )} +
+
+ ) + })} +
+ )} +
+
+ ) +} diff --git a/src/components/AI/TutorChat.tsx b/src/components/AI/TutorChat.tsx new file mode 100644 index 0000000..616729e --- /dev/null +++ b/src/components/AI/TutorChat.tsx @@ -0,0 +1,143 @@ +"use client" + +import { useState, useRef, useEffect } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Send, Bot, User, Loader2 } from "lucide-react" +import ReactMarkdown from "react-markdown" +import remarkMath from "remark-math" +import rehypeKatex from "rehype-katex" +import "katex/dist/katex.min.css" + +const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b" + +interface Message { + role: "user" | "assistant" + content: string +} + +export function TutorChat() { + const [messages, setMessages] = useState([ + { role: "assistant", content: "Hello! I am your PragatiVeer AI Coach. What topic would you like to review today?" } + ]) + const [input, setInput] = useState("") + const [streaming, setStreaming] = useState(false) + const messagesEndRef = useRef(null) + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + } + + useEffect(() => { + scrollToBottom() + }, [messages]) + + const handleSend = async (e?: React.FormEvent) => { + e?.preventDefault() + if (!input.trim() || streaming) return + + const userMsg = input.trim() + setInput("") + setMessages(prev => [...prev, { role: "user", content: userMsg }]) + + // Add empty assistant message placeholder + setMessages(prev => [...prev, { role: "assistant", content: "" }]) + setStreaming(true) + + try { + const res = await fetch("http://localhost:8000/api/ai-coach/chat/stream", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_id: USER_ID, message: userMsg }) + }) + + if (!res.ok) { + throw new Error("Backend streaming failed. Using mock fallback.") + } + + if (!res.body) throw new Error("No readable stream") + + const reader = res.body.getReader() + const decoder = new TextDecoder() + + while (true) { + const { done, value } = await reader.read() + if (done) break + const chunk = decoder.decode(value, { stream: true }) + setMessages(prev => { + const newMessages = [...prev] + newMessages[newMessages.length - 1].content += chunk + return newMessages + }) + } + } catch (err) { + console.warn("Falling back to mock stream due to error:", err) + // Mock Fallback Streaming logic + const mockResponse = "It looks like my backend connection is restricted right now. However, I can still render math for you! Here is the quadratic formula:\n\n$$ x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} $$\n\nAnd inline math: $E = mc^2$." + const words = mockResponse.split(" ") + + for (let i = 0; i < words.length; i++) { + await new Promise(resolve => setTimeout(resolve, 50)) // Simulate network delay + setMessages(prev => { + const newMessages = [...prev] + newMessages[newMessages.length - 1].content += (i === 0 ? "" : " ") + words[i] + return newMessages + }) + } + } finally { + setStreaming(false) + } + } + + return ( + + + + + AI Tutor Chat + + + + + {messages.map((msg, idx) => ( +
+
+ {msg.role === "assistant" && } +
+ {msg.role === "assistant" ? ( + + {msg.content || "..."} + + ) : ( + msg.content + )} +
+
+
+ ))} +
+ + +
+
+ setInput(e.target.value)} + placeholder="Ask me to explain a concept..." + className="flex-1 bg-secondary/50 border-transparent focus-visible:ring-primary" + disabled={streaming} + /> + +
+
+ + ) +} diff --git a/src/components/CBT/Calculator.tsx b/src/components/CBT/Calculator.tsx new file mode 100644 index 0000000..4e651ae --- /dev/null +++ b/src/components/CBT/Calculator.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { X, Delete } from "lucide-react"; + +interface CalculatorProps { + isOpen: boolean; + onClose: () => void; +} + +export function Calculator({ isOpen, onClose }: CalculatorProps) { + const [display, setDisplay] = useState(""); + + if (!isOpen) return null; + + const handleInput = (val: string) => { + setDisplay((prev) => prev + val); + }; + + const handleCalculate = () => { + try { + // Map functions to JS Math + let mappedExp = display + .replace(/sin\(/g, 'Math.sin(') + .replace(/cos\(/g, 'Math.cos(') + .replace(/tan\(/g, 'Math.tan(') + .replace(/log\(/g, 'Math.log10(') + .replace(/ln\(/g, 'Math.log(') + .replace(/sqrt\(/g, 'Math.sqrt(') + .replace(/\^/g, '**'); + + const result = new Function('return ' + mappedExp)(); + + // Handle floating point precision issues simply + if (typeof result === 'number' && !Number.isInteger(result)) { + setDisplay(String(parseFloat(result.toFixed(6)))); + } else { + setDisplay(String(result)); + } + } catch { + setDisplay("Error"); + } + }; + + const handleClear = () => setDisplay(""); + const handleBackspace = () => setDisplay((prev) => prev.slice(0, -1)); + + return ( +
+
+
+

Scientific Calculator

+ +
+
+
+ {display || "0"} +
+
+ {/* Scientific Row 1 */} + + + + + + + {/* Scientific Row 2 */} + + + + + + + {/* Number Row 1 */} + + + + + + + {/* Number Row 2 */} + + + + + + + {/* Number Row 3 */} + + + + + + + {/* Number Row 4 */} +
+ + + + + {/* Equals */} + +
+
+
+
+ ); +} diff --git a/src/components/CBT/ExamResult.tsx b/src/components/CBT/ExamResult.tsx new file mode 100644 index 0000000..d0b75cb --- /dev/null +++ b/src/components/CBT/ExamResult.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { CheckCircle2, XCircle, AlertCircle, Clock, Award, ArrowRight, RotateCcw } from "lucide-react"; +import Link from 'next/link'; + +interface ExamAttemptPayload { + score: number; + total_questions: number; + accuracy: number; + time_taken: number; + answers: Array<{ + question_id: string; + selected_answer: string | null; + is_correct: boolean; + }>; +} + +interface ExamResultProps { + payload: ExamAttemptPayload; + onRetake: () => void; +} + +export function ExamResult({ payload, onRetake }: ExamResultProps) { + const answered = payload.answers.filter(a => a.selected_answer !== null).length; + const unanswered = payload.total_questions - answered; + + // Calculate correct based on score formula (+4 for correct, -1 for wrong). + // Wait, payload score is just correct count according to seed script right now, let's assume it. + const correctCount = payload.answers.filter(a => a.is_correct).length; + const incorrectCount = answered - correctCount; + + // Real score calculation: +4 correct, -1 incorrect + const finalScore = (correctCount * 4) - (incorrectCount * 1); + const maxPossibleScore = payload.total_questions * 4; + + const formatTime = (seconds: number) => { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) return `${h}h ${m}m ${s}s`; + return `${m}m ${s}s`; + }; + + return ( +
+
+ +
+
+ +
+

Exam Successfully Submitted

+

Here is a quick snapshot of your performance.

+
+ +
+ + + Final Score +
+ {finalScore} / {maxPossibleScore} +
+
+
+ + + + Accuracy +
+ {Math.round(payload.accuracy)}% +
+
+
+ + + + Time Taken +
+ {formatTime(payload.time_taken)} +
+
+
+ + + + Attempted +
+ {answered} / {payload.total_questions} +
+
+
+
+ +
+
+ +
{correctCount}
+
Correct
+
+
+ +
{incorrectCount}
+
Incorrect
+
+
+ +
{unanswered}
+
Unanswered
+
+
+ +
+ + + + +
+ +
+
+ ); +} diff --git a/src/components/CBT/PeriodicTable.tsx b/src/components/CBT/PeriodicTable.tsx new file mode 100644 index 0000000..7bd7e9f --- /dev/null +++ b/src/components/CBT/PeriodicTable.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { X, ExternalLink } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import Image from "next/image"; + +interface PeriodicTableProps { + isOpen: boolean; + onClose: () => void; +} + +export function PeriodicTable({ isOpen, onClose }: PeriodicTableProps) { + if (!isOpen) return null; + + return ( +
+
+
+

Periodic Table of Elements

+
+ + +
+
+
+ {/* We will use a high quality remote SVG/image for the periodic table reference */} +
+ {/* Using a reliable wikimedia SVG for Periodic Table as a reference */} + Periodic Table +
+
+
+
+ ); +} diff --git a/src/components/CBT/Scratchpad.tsx b/src/components/CBT/Scratchpad.tsx new file mode 100644 index 0000000..6177102 --- /dev/null +++ b/src/components/CBT/Scratchpad.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { X, Eraser, Pen } from "lucide-react"; + +interface ScratchpadProps { + isOpen: boolean; + onClose: () => void; +} + +export function Scratchpad({ isOpen, onClose }: ScratchpadProps) { + const canvasRef = useRef(null); + const [isDrawing, setIsDrawing] = useState(false); + const [mode, setMode] = useState<"pen" | "eraser">("pen"); + const [notes, setNotes] = useState(""); + + // Initialize and load saved session memory + useEffect(() => { + if (isOpen) { + const savedNotes = localStorage.getItem("cbt_scratchpad_notes"); + if (savedNotes) setNotes(savedNotes); + + if (canvasRef.current) { + const canvas = canvasRef.current; + canvas.width = 500; + canvas.height = 500; + const ctx = canvas.getContext("2d"); + if (ctx) { + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.strokeStyle = "white"; + ctx.lineWidth = 2; + + const savedDrawing = localStorage.getItem("cbt_scratchpad_drawing"); + if (savedDrawing) { + const img = new window.Image(); + img.onload = () => { + ctx.drawImage(img, 0, 0); + }; + img.src = savedDrawing; + } + } + } + } + }, [isOpen]); + + if (!isOpen) return null; + + const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { + setIsDrawing(true); + draw(e); + }; + + const stopDrawing = () => { + setIsDrawing(false); + const canvas = canvasRef.current; + if (canvas) { + const ctx = canvas.getContext("2d"); + ctx?.beginPath(); + localStorage.setItem("cbt_scratchpad_drawing", canvas.toDataURL()); + } + }; + + const draw = (e: React.MouseEvent | React.TouchEvent) => { + if (!isDrawing || !canvasRef.current) return; + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let clientX, clientY; + if ('touches' in e) { + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + } else { + clientX = (e as React.MouseEvent).clientX; + clientY = (e as React.MouseEvent).clientY; + } + + const rect = canvas.getBoundingClientRect(); + const x = clientX - rect.left; + const y = clientY - rect.top; + + ctx.lineTo(x, y); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(x, y); + }; + + const clearCanvas = () => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (ctx && canvas) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + localStorage.removeItem("cbt_scratchpad_drawing"); + } + }; + + const clearNotes = () => { + setNotes(""); + localStorage.removeItem("cbt_scratchpad_notes"); + }; + + const handleModeSwitch = (newMode: "pen" | "eraser") => { + setMode(newMode); + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (ctx) { + if (newMode === "eraser") { + ctx.globalCompositeOperation = "destination-out"; + ctx.lineWidth = 20; + } else { + ctx.globalCompositeOperation = "source-over"; + ctx.lineWidth = 2; + } + } + }; + + const handleNotesChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setNotes(val); + localStorage.setItem("cbt_scratchpad_notes", val); + }; + + return ( +
+
+
+

Split Workspace: Draw & Type

+
+ + + +
+
+
+ {/* Drawing Column */} +
+
+ +
+
+ +
+
+ {/* Typing Column */} +
+
+ +
+
+