- 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? -
-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 0000000..a9c5eb0 Binary files /dev/null and b/backend/celerybeat-schedule differ diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..e4186cb --- /dev/null +++ b/backend/database.py @@ -0,0 +1,17 @@ +import os +from sqlmodel import create_engine, SQLModel, Session +from typing import Generator + +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod") + +# Using the standard SQLModel engine config for PostgreSQL +engine = create_engine(DATABASE_URL, echo=True) + +def init_db(): + # Automatically generates tables based on the classes imported from models.py + import models + SQLModel.metadata.create_all(engine) + +def get_session() -> 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 0000000..756db6b Binary files /dev/null and b/pragativeer-prd (4).pdf differ 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() {
Target: {questions.some(q => q.subject.toLowerCase() === 'biology') ? 'NEET' : 'JEE'} Pattern Test
+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.
+ +Physics • Section A
+{currentQuestion.subject}
- 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? -
-