Skip to content

Build AI-Powered Study Assistant Application#1

Open
qwertystars wants to merge 1 commit into
mainfrom
claude/ai-study-assistant-build-011CUu1CufkxTYAMRgyK96M4
Open

Build AI-Powered Study Assistant Application#1
qwertystars wants to merge 1 commit into
mainfrom
claude/ai-study-assistant-build-011CUu1CufkxTYAMRgyK96M4

Conversation

@qwertystars

@qwertystars qwertystars commented Nov 8, 2025

Copy link
Copy Markdown
Owner

Built a full-stack production-ready study assistant application with:

Backend (FastAPI + PostgreSQL + Ollama):

  • RESTful API with FastAPI and SQLAlchemy ORM
  • Database models for subjects, flashcards, quizzes, practice problems, progress tracking
  • AI service integration with Ollama for content generation
  • Spaced repetition algorithm (SM-2) for flashcard scheduling
  • Comprehensive API endpoints for all features
  • Alembic migrations for database schema management
  • Docker support for containerized deployment

Frontend (React + Vite + TailwindCSS):

  • Modern React 18 with functional components and hooks
  • Responsive UI with TailwindCSS utility classes
  • Subject management with custom colors
  • AI-powered flashcard generation and review system
  • Quiz mode with multiple-choice questions and instant feedback
  • Practice problem generator with detailed solutions
  • Progress tracking dashboard with analytics
  • Real-time progress updates and mastery level calculation

Key Features:

  • Generate flashcards automatically from topics using local LLM
  • Smart spaced repetition for optimal learning
  • Adaptive quiz generation with difficulty levels
  • Practice problems with step-by-step solutions
  • Comprehensive progress tracking and analytics
  • Study session logging
  • No external API dependencies (fully local with Ollama)

Documentation:

  • Comprehensive README with installation and usage guides
  • Quick setup guide for first-time users
  • Complete API documentation with examples
  • Docker compose for easy deployment
  • Environment configuration templates

Tech Stack:

  • Backend: FastAPI, SQLAlchemy, PostgreSQL, Alembic, Ollama
  • Frontend: React, Vite, TailwindCSS, Axios, React Router
  • Deployment: Docker, Docker Compose
  • AI: Ollama (local LLM - llama3.2)

The application is designed for B.Tech CSE students at VIT Chennai but can be used for any educational context. All AI features run locally for privacy and no API costs.

Summary by CodeRabbit

Release Notes

New Features

  • AI Study Assistant Application: Complete platform for creating and managing study materials using local AI.
  • Subject Management: Create and organize study topics with custom colors and descriptions.
  • AI-Generated Content: Automatically generate flashcards, quizzes, and practice problems by topic.
  • Spaced Repetition: Smart flashcard scheduling optimizes learning retention.
  • Progress Tracking: Monitor mastery levels, study time, and performance metrics per subject.
  • Interactive Dashboard: User-friendly interface to access all study tools and track learning progress.

Documentation

  • Comprehensive API documentation with all endpoints and usage examples.
  • Full setup and deployment guides for local development and Docker.
  • Detailed project README with architecture overview.

Built a full-stack production-ready study assistant application with:

Backend (FastAPI + PostgreSQL + Ollama):
- RESTful API with FastAPI and SQLAlchemy ORM
- Database models for subjects, flashcards, quizzes, practice problems, progress tracking
- AI service integration with Ollama for content generation
- Spaced repetition algorithm (SM-2) for flashcard scheduling
- Comprehensive API endpoints for all features
- Alembic migrations for database schema management
- Docker support for containerized deployment

Frontend (React + Vite + TailwindCSS):
- Modern React 18 with functional components and hooks
- Responsive UI with TailwindCSS utility classes
- Subject management with custom colors
- AI-powered flashcard generation and review system
- Quiz mode with multiple-choice questions and instant feedback
- Practice problem generator with detailed solutions
- Progress tracking dashboard with analytics
- Real-time progress updates and mastery level calculation

Key Features:
- Generate flashcards automatically from topics using local LLM
- Smart spaced repetition for optimal learning
- Adaptive quiz generation with difficulty levels
- Practice problems with step-by-step solutions
- Comprehensive progress tracking and analytics
- Study session logging
- No external API dependencies (fully local with Ollama)

Documentation:
- Comprehensive README with installation and usage guides
- Quick setup guide for first-time users
- Complete API documentation with examples
- Docker compose for easy deployment
- Environment configuration templates

Tech Stack:
- Backend: FastAPI, SQLAlchemy, PostgreSQL, Alembic, Ollama
- Frontend: React, Vite, TailwindCSS, Axios, React Router
- Deployment: Docker, Docker Compose
- AI: Ollama (local LLM - llama3.2)

The application is designed for B.Tech CSE students at VIT Chennai but can be used
for any educational context. All AI features run locally for privacy and no API costs.
@coderabbitai

coderabbitai Bot commented Nov 8, 2025

Copy link
Copy Markdown

Walkthrough

This PR introduces a complete full-stack AI-powered study assistant application featuring a FastAPI backend with PostgreSQL database, Ollama-based content generation, spaced repetition learning algorithms, a React frontend with Vite, Docker orchestration, and comprehensive documentation for setup and deployment.

Changes

Cohort / File(s) Summary
Documentation
.gitignore, API_DOCUMENTATION.md, README.md, SETUP_GUIDE.md
Configuration files and comprehensive guides covering API endpoints, setup instructions, prerequisites, troubleshooting, and project overview for users and developers.
Backend Infrastructure & Deployment
backend/.env.example, backend/Dockerfile, backend/requirements.txt, docker-compose.yml, frontend/Dockerfile
Configuration and containerization files defining environment variables, Python/Node dependencies, Docker builds for multi-service local and production deployment.
Backend Database & Migrations
backend/alembic.ini, backend/alembic/env.py, backend/alembic/script.py.mako, backend/alembic/versions/001_initial_migration.py
Alembic configuration and initial database schema including seven tables for subjects, flashcards, quizzes, quiz questions, practice problems, study sessions, and progress tracking with foreign keys and cascade rules.
Backend Core Application
backend/app/__init__.py, backend/app/main.py, backend/app/config.py, backend/app/database.py
FastAPI application initialization, settings management via Pydantic, SQLAlchemy database engine, session factory, and dependency injection for request-scoped database access.
Backend Models
backend/app/models/__init__.py, backend/app/models/subject.py, backend/app/models/flashcard.py, backend/app/models/quiz.py, backend/app/models/practice_problem.py, backend/app/models/study_session.py, backend/app/models/progress.py
SQLAlchemy ORM models defining entity schemas with relationships, cascade delete rules, timestamps, and fields for spaced repetition (ease_factor, interval, repetitions, next_review), quiz scoring, and progress metrics.
Backend Schemas (Pydantic)
backend/app/schemas/__init__.py, backend/app/schemas/subject.py, backend/app/schemas/flashcard.py, backend/app/schemas/quiz.py, backend/app/schemas/practice_problem.py, backend/app/schemas/study_session.py, backend/app/schemas/progress.py
Request/response validation schemas for all resources with base, create, update, and response models; includes ORM attribute mapping configuration.
Backend API Routing
backend/app/api/__init__.py, backend/app/api/endpoints/__init__.py, backend/app/api/endpoints/subjects.py, backend/app/api/endpoints/flashcards.py, backend/app/api/endpoints/quizzes.py, backend/app/api/endpoints/practice_problems.py, backend/app/api/endpoints/study_sessions.py, backend/app/api/endpoints/progress.py
REST API endpoints for CRUD operations, AI-powered content generation, spaced repetition review flows, quiz submission with scoring, and progress tracking across all study resources.
Backend Services
backend/app/services/__init__.py, backend/app/services/ai_service.py, backend/app/services/spaced_repetition.py
Core business logic for Ollama-based AI generation of flashcards, practice problems, and quiz questions; SM-2-based spaced repetition algorithm for calculating ease factor, review intervals, and next review dates.
Frontend Configuration & Build
frontend/package.json, frontend/Dockerfile, frontend/index.html, frontend/vite.config.js, frontend/tailwind.config.js, frontend/postcss.config.js
Node.js dependencies, Vite dev server configuration with API proxy, TailwindCSS and PostCSS setup, and HTML entry point for React application.
Frontend Core & Styling
frontend/src/main.jsx, frontend/src/App.jsx, frontend/src/index.css, frontend/src/components/Layout.jsx
React application bootstrap, top-level routing, global TailwindCSS styling with custom utility classes, and reusable layout component with responsive header and navigation.
Frontend Pages
frontend/src/pages/Dashboard.jsx, frontend/src/pages/SubjectDetails.jsx, frontend/src/pages/Flashcards.jsx, frontend/src/pages/Quiz.jsx, frontend/src/pages/QuizTake.jsx, frontend/src/pages/PracticeProblems.jsx, frontend/src/pages/Progress.jsx
Full-featured React pages for subject management, study tool interactions (flashcards with flip-to-reveal and review ratings, quiz creation and submission, practice problems with solutions), progress visualization with mastery metrics per subject.
Frontend API Client
frontend/src/services/api.js
Axios-based HTTP client with helper functions for all backend resource operations: subjects, flashcards, quizzes, practice problems, progress, and study sessions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant React
    participant FastAPI
    participant Ollama
    participant PostgreSQL

    User->>React: Click "Generate Flashcards"
    React->>React: Open modal, collect topic & count
    User->>React: Submit
    React->>FastAPI: POST /api/flashcards/generate<br/>(subject_id, topic, num_cards)
    FastAPI->>PostgreSQL: Verify Subject exists
    PostgreSQL-->>FastAPI: Subject found
    FastAPI->>Ollama: generate_flashcards(subject, topic, num_cards)
    Ollama-->>FastAPI: JSON array of Q&A pairs
    FastAPI->>PostgreSQL: INSERT Flashcards (with defaults:<br/>ease_factor=2.5, interval=1,<br/>next_review=now())
    PostgreSQL-->>FastAPI: Flashcards created
    FastAPI-->>React: List[FlashcardResponse]
    React-->>User: Display new flashcard deck

    User->>React: Flip & Rate Card (quality=4)
    React->>FastAPI: POST /flashcards/{id}/review<br/>(quality=4)
    FastAPI->>PostgreSQL: Load Flashcard
    PostgreSQL-->>FastAPI: Flashcard + current values
    FastAPI->>FastAPI: calculate_next_review(quality, <br/>ease_factor, interval, repetitions)<br/>→ (new_ease, new_interval, <br/>new_repetitions, next_date)
    FastAPI->>PostgreSQL: UPDATE Flashcard<br/>(ease_factor, interval, repetitions,<br/>next_review, last_reviewed)
    FastAPI->>PostgreSQL: UPDATE Progress<br/>(flashcards_reviewed++)
    PostgreSQL-->>FastAPI: Changes committed
    FastAPI-->>React: FlashcardResponse (updated)
    React-->>User: Show next card
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Areas requiring extra attention:

  • Spaced repetition implementation (backend/app/services/spaced_repetition.py): Verify SM-2 algorithm logic, boundary conditions (quality validation 0–5), and correct interval calculations for first/second/subsequent reviews.
  • AI service integration (backend/app/services/ai_service.py): JSON parsing robustness from Ollama responses, fallback handling, and error propagation; test with actual Ollama output formats.
  • Database cascade relationships (backend/app/models/subject.py and related): Confirm all cascade delete rules are correct and foreign key constraints properly defined; verify migration integrity.
  • API endpoint logic (backend/app/api/endpoints/flashcards.py, quizzes.py, practice_problems.py): Review session lifecycle (commit/refresh patterns), error handling consistency, progress update timing, and validation of subject/resource existence.
  • Frontend API client and pages (frontend/src/services/api.js, frontend/src/pages/*.jsx): Verify all endpoint paths match backend routes; check state management and loading/error handling across async operations; validate form submission flows.
  • Docker and Ollama integration (docker-compose.yml, backend/Dockerfile): Confirm service dependency ordering, healthchecks, environment variable propagation, and volume mounts; test end-to-end local deployment.
  • TypeScript/JavaScript typing: No type safety in frontend (plain JS); review for potential runtime errors.

Poem

🐰 A full stack springs to life, hooray!
Flashcards flipped, spaced just right each day,
From Ollama's wisdom to PostgreSQL deep,
React serves the frontend, learning heaps to keep.
Docker orchestrates the dance so neat—
This study assistant is utterly sweet!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.47% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: building a complete AI-powered study assistant application, which matches the extensive changeset introducing backend, frontend, documentation, and infrastructure.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/ai-study-assistant-build-011CUu1CufkxTYAMRgyK96M4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 26

🧹 Nitpick comments (15)
docker-compose.yml (2)

27-28: Use environment variable references for database credentials.

The DATABASE_URL contains the same hardcoded credentials as the postgres service. This creates duplication and maintenance burden.

Apply this diff to reference environment variables:

     environment:
-      DATABASE_URL: postgresql://studyuser:studypass@postgres:5432/studyassistant
+      DATABASE_URL: postgresql://${POSTGRES_USER:-studyuser}:${POSTGRES_PASSWORD:-studypass}@postgres:5432/${POSTGRES_DB:-studyassistant}
       OLLAMA_BASE_URL: http://host.docker.internal:11434
       OLLAMA_MODEL: llama3.2

28-28: Document platform-specific Docker networking assumption.

The host.docker.internal hostname is specific to Docker Desktop and may not work in other environments (Linux Docker Engine, production deployments, CI/CD).

Consider documenting in the README:

  1. This setup requires Docker Desktop on macOS/Windows
  2. For Linux, users need to add --add-host=host.docker.internal:host-gateway to the docker run command or use network_mode: "host"
  3. For production, Ollama should be containerized or accessed via a proper service URL
frontend/src/components/Layout.jsx (1)

9-12: Consider moving navigation array outside component.

The navigation array is re-created on every render. Moving it outside the component improves performance by avoiding unnecessary object allocations.

Apply this diff:

+const navigation = [
+  { name: 'Dashboard', href: '/', icon: Home },
+  { name: 'Progress', href: '/progress', icon: TrendingUp },
+];
+
 function Layout({ children }) {
   const location = useLocation();
   const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
 
-  const navigation = [
-    { name: 'Dashboard', href: '/', icon: Home },
-    { name: 'Progress', href: '/progress', icon: TrendingUp },
-  ];
-
   const isActive = (path) => location.pathname === path;
API_DOCUMENTATION.md (1)

364-366: Wrap bare URLs in angle brackets for proper Markdown formatting.

Markdown linters prefer URLs to be wrapped in angle brackets or formatted as links.

Based on static analysis hints

Apply this diff:

 ## Interactive Documentation
 
-Visit http://localhost:8000/docs for interactive Swagger UI documentation where you can test all endpoints directly.
+Visit <http://localhost:8000/docs> for interactive Swagger UI documentation where you can test all endpoints directly.
 
-Alternative documentation: http://localhost:8000/redoc
+Alternative documentation: <http://localhost:8000/redoc>
backend/app/database.py (2)

1-9: Consider upgrading to SQLAlchemy 2.0 DeclarativeBase style.

The code uses the legacy declarative_base() function. SQLAlchemy 2.0+ (version 2.0.25 is in use) recommends using the new DeclarativeBase class for better type checking and modern patterns.

Apply this diff to use the modern SQLAlchemy 2.0 style:

 from sqlalchemy import create_engine
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import DeclarativeBase
 from sqlalchemy.orm import sessionmaker
 from .config import settings
 
 engine = create_engine(settings.DATABASE_URL)
 SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
 
-Base = declarative_base()
+class Base(DeclarativeBase):
+    pass

This provides better IDE support and aligns with SQLAlchemy 2.0 best practices.


6-7: Consider adding connection pool configuration for production.

The engine uses default connection pooling settings. For production deployments, you may want to configure pool size, timeouts, and overflow settings based on your load requirements.

Example configuration for production:

engine = create_engine(
    settings.DATABASE_URL,
    pool_size=5,          # Number of connections to maintain
    max_overflow=10,      # Additional connections under load
    pool_timeout=30,      # Timeout for getting a connection
    pool_recycle=3600,    # Recycle connections after 1 hour
    pool_pre_ping=True,   # Verify connections before using
)

Monitor your application's connection usage and adjust these values accordingly.

backend/app/services/spaced_repetition.py (1)

8-26: Consider adding input validation for the quality parameter.

The docstring specifies quality: Response quality (0-5), but there's no validation to ensure the value is within this range. Invalid values could produce incorrect ease factor calculations.

Add validation at the start of the method:

     def calculate_next_review(
         quality: int,
         ease_factor: float,
         interval: int,
         repetitions: int
     ) -> Tuple[float, int, int, datetime]:
         """
         Calculate next review date using SM-2 algorithm
 
         Args:
             quality: Response quality (0-5)
             ease_factor: Current ease factor
             interval: Current interval in days
             repetitions: Number of consecutive successful repetitions
 
         Returns:
             Tuple of (new_ease_factor, new_interval, new_repetitions, next_review_date)
         """
+        if not 0 <= quality <= 5:
+            raise ValueError(f"Quality must be between 0 and 5, got {quality}")
+        
         # Update ease factor
         new_ease_factor = ease_factor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
frontend/src/pages/SubjectDetails.jsx (1)

24-25: Display user-facing error messages.

Errors are logged to the console but not displayed to the user. If the API call fails, users will see a generic "Subject not found" message or no feedback at all.

Add error state and display it to users:

   const [subject, setSubject] = useState(null);
   const [progress, setProgress] = useState(null);
   const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(null);

   useEffect(() => {
     const loadData = async () => {
       try {
+        setError(null);
         const [subjectRes, progressRes] = await Promise.all([
           getSubject(id),
           getSubjectProgress(id)
         ]);
         setSubject(subjectRes.data);
         setProgress(progressRes.data);
       } catch (error) {
         console.error('Error loading data:', error);
+        setError('Failed to load subject data. Please try again.');
       } finally {
         setLoading(false);
       }
     };
     
     loadData();
   }, [id]);

   if (loading) {
     return <div className="text-center py-12">Loading...</div>;
   }

+  if (error) {
+    return <div className="text-center py-12 text-red-600">{error}</div>;
+  }
backend/app/models/__init__.py (1)

1-16: LGTM! Clean model export structure.

The file properly consolidates all ORM model exports into a single namespace, making imports clean throughout the codebase.

Optional: The static analysis tool suggests sorting __all__ alphabetically, but the current logical grouping (Subject first, then related entities) is also reasonable. If you prefer consistency with import sorting tools, you could apply:

 __all__ = [
-    "Subject",
     "Flashcard",
-    "Quiz",
-    "QuizQuestion",
     "PracticeProblem",
+    "Progress",
+    "Quiz",
+    "QuizQuestion",
     "StudySession",
-    "Progress",
+    "Subject",
 ]
backend/app/schemas/study_session.py (1)

6-10: Consider adding validation for the duration field.

The duration field (study session length in minutes) should be positive, but there's no validation to prevent negative or zero values, which could corrupt progress metrics.

Add validation using Pydantic's Field:

-from pydantic import BaseModel
+from pydantic import BaseModel, Field
 from typing import Optional
 from datetime import datetime


 class StudySessionCreate(BaseModel):
     subject_id: int
-    duration: int  # in minutes
+    duration: int = Field(gt=0, description="Duration in minutes")
     topics_covered: Optional[str] = None
     notes: Optional[str] = None
backend/app/schemas/progress.py (1)

6-17: Consider adding field validation for numeric ranges.

Several fields should have specific ranges or constraints to prevent invalid data:

  • mastery_level should be 0-100 (as indicated in the ORM model comment)
  • average_quiz_score should be 0-100
  • Counters (total_study_time, flashcards_reviewed, etc.) should be non-negative

Add validation using Pydantic's Field:

-from pydantic import BaseModel
+from pydantic import BaseModel, Field
 from typing import Optional
 from datetime import datetime


 class ProgressResponse(BaseModel):
     id: int
     subject_id: int
-    mastery_level: float
-    total_study_time: int
-    flashcards_reviewed: int
-    quizzes_completed: int
-    problems_solved: int
-    average_quiz_score: float
+    mastery_level: float = Field(ge=0, le=100, description="Mastery level percentage")
+    total_study_time: int = Field(ge=0, description="Total study time in minutes")
+    flashcards_reviewed: int = Field(ge=0)
+    quizzes_completed: int = Field(ge=0)
+    problems_solved: int = Field(ge=0)
+    average_quiz_score: float = Field(ge=0, le=100, description="Average quiz score percentage")
     last_studied: Optional[datetime] = None
     created_at: datetime
     updated_at: Optional[datetime] = None
backend/app/models/quiz.py (2)

34-34: Consider using Boolean type instead of Integer for is_correct.

The is_correct field uses Integer (with values 1/0), but SQLAlchemy's Boolean type would be more idiomatic and type-safe. SQLAlchemy handles the database-specific representation automatically.

Apply this diff:

-from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Float, JSON
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Float, JSON, Boolean
 from sqlalchemy.orm import relationship
 from sqlalchemy.sql import func
 from ..database import Base


 class Quiz(Base):
     __tablename__ = "quizzes"

     id = Column(Integer, primary_key=True, index=True)
     subject_id = Column(Integer, ForeignKey("subjects.id", ondelete="CASCADE"), nullable=False)
     title = Column(String, nullable=False)
     difficulty = Column(String, default="medium")  # easy, medium, hard
     score = Column(Float, default=0.0)
     total_questions = Column(Integer, default=0)
     completed_at = Column(DateTime(timezone=True), nullable=True)
     created_at = Column(DateTime(timezone=True), server_default=func.now())

     # Relationships
     subject = relationship("Subject", back_populates="quizzes")
     questions = relationship("QuizQuestion", back_populates="quiz", cascade="all, delete-orphan")


 class QuizQuestion(Base):
     __tablename__ = "quiz_questions"

     id = Column(Integer, primary_key=True, index=True)
     quiz_id = Column(Integer, ForeignKey("quizzes.id", ondelete="CASCADE"), nullable=False)
     question = Column(Text, nullable=False)
     options = Column(JSON, nullable=False)  # List of options
     correct_answer = Column(String, nullable=False)
     user_answer = Column(String, nullable=True)
     explanation = Column(Text, nullable=True)
-    is_correct = Column(Integer, nullable=True)  # 1 for correct, 0 for incorrect
+    is_correct = Column(Boolean, nullable=True)

     # Relationships
     quiz = relationship("Quiz", back_populates="questions")

Note: If you're using a database without native boolean support (e.g., older MySQL), SQLAlchemy will automatically map Boolean to TINYINT(1), so this change is safe.


7-21: LGTM! Well-structured ORM models with proper relationships.

The Quiz and QuizQuestion models are well-designed with appropriate cascade behavior and relationships. The use of JSON for options provides flexibility for varying numbers of choices.

Optional: You could enhance type safety for the difficulty field by using an enum instead of a free-form string:

-from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Float, JSON
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Float, JSON, Enum as SQLEnum
+from enum import Enum
 from sqlalchemy.orm import relationship
 from sqlalchemy.sql import func
 from ..database import Base


+class DifficultyLevel(str, Enum):
+    EASY = "easy"
+    MEDIUM = "medium"
+    HARD = "hard"
+
+
 class Quiz(Base):
     __tablename__ = "quizzes"

     id = Column(Integer, primary_key=True, index=True)
     subject_id = Column(Integer, ForeignKey("subjects.id", ondelete="CASCADE"), nullable=False)
     title = Column(String, nullable=False)
-    difficulty = Column(String, default="medium")  # easy, medium, hard
+    difficulty = Column(SQLEnum(DifficultyLevel), default=DifficultyLevel.MEDIUM)
     score = Column(Float, default=0.0)
     total_questions = Column(Integer, default=0)
backend/app/api/endpoints/subjects.py (1)

13-66: Adopt Annotated[..., Depends(...)] to satisfy modern FastAPI guidance

Consider switching the dependency parameters to use Annotated[Session, Depends(get_db)]. This silences Ruff’s B008 warning and follows FastAPI’s current recommendation for dependency injection.(docs.astral.sh)

backend/app/schemas/quiz.py (1)

1-45: Avoid the shared list default on questions

The empty list literal is shared across model instances. Swap to a default factory so each response gets its own list.

Apply this diff:

-from pydantic import BaseModel
+from pydantic import BaseModel, Field
@@
-    questions: List[QuizQuestionResponse] = []
+    questions: List[QuizQuestionResponse] = Field(default_factory=list)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3b6dfd and 8877aad.

📒 Files selected for processing (59)
  • .gitignore (1 hunks)
  • API_DOCUMENTATION.md (1 hunks)
  • README.md (1 hunks)
  • SETUP_GUIDE.md (1 hunks)
  • backend/.env.example (1 hunks)
  • backend/Dockerfile (1 hunks)
  • backend/alembic.ini (1 hunks)
  • backend/alembic/env.py (1 hunks)
  • backend/alembic/script.py.mako (1 hunks)
  • backend/alembic/versions/001_initial_migration.py (1 hunks)
  • backend/app/__init__.py (1 hunks)
  • backend/app/api/__init__.py (1 hunks)
  • backend/app/api/endpoints/__init__.py (1 hunks)
  • backend/app/api/endpoints/flashcards.py (1 hunks)
  • backend/app/api/endpoints/practice_problems.py (1 hunks)
  • backend/app/api/endpoints/progress.py (1 hunks)
  • backend/app/api/endpoints/quizzes.py (1 hunks)
  • backend/app/api/endpoints/study_sessions.py (1 hunks)
  • backend/app/api/endpoints/subjects.py (1 hunks)
  • backend/app/config.py (1 hunks)
  • backend/app/database.py (1 hunks)
  • backend/app/main.py (1 hunks)
  • backend/app/models/__init__.py (1 hunks)
  • backend/app/models/flashcard.py (1 hunks)
  • backend/app/models/practice_problem.py (1 hunks)
  • backend/app/models/progress.py (1 hunks)
  • backend/app/models/quiz.py (1 hunks)
  • backend/app/models/study_session.py (1 hunks)
  • backend/app/models/subject.py (1 hunks)
  • backend/app/schemas/__init__.py (1 hunks)
  • backend/app/schemas/flashcard.py (1 hunks)
  • backend/app/schemas/practice_problem.py (1 hunks)
  • backend/app/schemas/progress.py (1 hunks)
  • backend/app/schemas/quiz.py (1 hunks)
  • backend/app/schemas/study_session.py (1 hunks)
  • backend/app/schemas/subject.py (1 hunks)
  • backend/app/services/__init__.py (1 hunks)
  • backend/app/services/ai_service.py (1 hunks)
  • backend/app/services/spaced_repetition.py (1 hunks)
  • backend/requirements.txt (1 hunks)
  • docker-compose.yml (1 hunks)
  • frontend/Dockerfile (1 hunks)
  • frontend/index.html (1 hunks)
  • frontend/package.json (1 hunks)
  • frontend/postcss.config.js (1 hunks)
  • frontend/src/App.jsx (1 hunks)
  • frontend/src/components/Layout.jsx (1 hunks)
  • frontend/src/index.css (1 hunks)
  • frontend/src/main.jsx (1 hunks)
  • frontend/src/pages/Dashboard.jsx (1 hunks)
  • frontend/src/pages/Flashcards.jsx (1 hunks)
  • frontend/src/pages/PracticeProblems.jsx (1 hunks)
  • frontend/src/pages/Progress.jsx (1 hunks)
  • frontend/src/pages/Quiz.jsx (1 hunks)
  • frontend/src/pages/QuizTake.jsx (1 hunks)
  • frontend/src/pages/SubjectDetails.jsx (1 hunks)
  • frontend/src/services/api.js (1 hunks)
  • frontend/tailwind.config.js (1 hunks)
  • frontend/vite.config.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (29)
frontend/src/pages/SubjectDetails.jsx (1)
frontend/src/services/api.js (4)
  • getSubject (14-14)
  • getSubject (14-14)
  • getSubjectProgress (50-50)
  • getSubjectProgress (50-50)
frontend/src/pages/Quiz.jsx (2)
backend/app/models/quiz.py (1)
  • Quiz (7-21)
frontend/src/services/api.js (6)
  • getQuizzes (31-32)
  • getQuizzes (31-32)
  • generateQuiz (34-34)
  • generateQuiz (34-34)
  • deleteQuiz (36-36)
  • deleteQuiz (36-36)
backend/app/schemas/flashcard.py (5)
backend/app/schemas/practice_problem.py (1)
  • Config (28-29)
backend/app/schemas/progress.py (1)
  • Config (19-20)
backend/app/schemas/quiz.py (2)
  • Config (18-19)
  • Config (44-45)
backend/app/schemas/study_session.py (1)
  • Config (17-18)
backend/app/schemas/subject.py (1)
  • Config (27-28)
backend/app/models/quiz.py (2)
frontend/src/pages/SubjectDetails.jsx (1)
  • subject (8-8)
frontend/src/pages/QuizTake.jsx (1)
  • quiz (9-9)
frontend/src/pages/Flashcards.jsx (1)
frontend/src/services/api.js (8)
  • getDueFlashcards (22-23)
  • getDueFlashcards (22-23)
  • getFlashcards (20-21)
  • getFlashcards (20-21)
  • generateFlashcards (24-25)
  • generateFlashcards (24-25)
  • reviewFlashcard (26-27)
  • reviewFlashcard (26-27)
backend/app/schemas/progress.py (5)
backend/app/schemas/flashcard.py (1)
  • Config (38-39)
backend/app/schemas/practice_problem.py (1)
  • Config (28-29)
backend/app/schemas/quiz.py (2)
  • Config (18-19)
  • Config (44-45)
backend/app/schemas/study_session.py (1)
  • Config (17-18)
backend/app/schemas/subject.py (1)
  • Config (27-28)
frontend/src/pages/Progress.jsx (3)
backend/app/models/progress.py (1)
  • Progress (7-23)
frontend/src/pages/SubjectDetails.jsx (4)
  • progress (9-9)
  • loading (10-10)
  • loadData (16-29)
  • subject (8-8)
frontend/src/services/api.js (4)
  • getAllProgress (49-49)
  • getAllProgress (49-49)
  • getSubjects (13-13)
  • getSubjects (13-13)
backend/app/schemas/quiz.py (6)
backend/app/schemas/flashcard.py (1)
  • Config (38-39)
backend/app/schemas/practice_problem.py (1)
  • Config (28-29)
backend/app/schemas/progress.py (1)
  • Config (19-20)
backend/app/schemas/study_session.py (1)
  • Config (17-18)
backend/app/schemas/subject.py (1)
  • Config (27-28)
frontend/src/pages/QuizTake.jsx (1)
  • answers (10-10)
frontend/src/App.jsx (2)
backend/app/models/quiz.py (1)
  • Quiz (7-21)
backend/app/models/progress.py (1)
  • Progress (7-23)
backend/app/services/ai_service.py (2)
backend/app/api/endpoints/flashcards.py (1)
  • generate_flashcards (47-81)
backend/app/api/endpoints/practice_problems.py (1)
  • generate_practice_problems (42-84)
frontend/src/pages/QuizTake.jsx (1)
frontend/src/services/api.js (4)
  • getQuiz (33-33)
  • getQuiz (33-33)
  • submitQuiz (35-35)
  • submitQuiz (35-35)
backend/app/api/endpoints/subjects.py (4)
backend/app/database.py (1)
  • get_db (12-18)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/models/progress.py (1)
  • Progress (7-23)
backend/app/schemas/subject.py (3)
  • SubjectCreate (12-13)
  • SubjectUpdate (16-19)
  • SubjectResponse (22-28)
backend/app/main.py (1)
frontend/src/services/api.js (1)
  • api (5-10)
backend/app/api/endpoints/practice_problems.py (6)
backend/app/database.py (1)
  • get_db (12-18)
backend/app/models/practice_problem.py (1)
  • PracticeProblem (7-22)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/models/progress.py (1)
  • Progress (7-23)
backend/app/schemas/practice_problem.py (2)
  • PracticeProblemCreate (14-18)
  • PracticeProblemResponse (21-29)
backend/app/services/ai_service.py (1)
  • generate_practice_problems (55-91)
backend/app/api/endpoints/study_sessions.py (5)
backend/app/database.py (1)
  • get_db (12-18)
backend/app/models/study_session.py (1)
  • StudySession (7-18)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/models/progress.py (1)
  • Progress (7-23)
backend/app/schemas/study_session.py (2)
  • StudySessionCreate (6-10)
  • StudySessionResponse (13-18)
backend/alembic/env.py (6)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/models/flashcard.py (1)
  • Flashcard (7-27)
backend/app/models/quiz.py (2)
  • Quiz (7-21)
  • QuizQuestion (24-37)
backend/app/models/practice_problem.py (1)
  • PracticeProblem (7-22)
backend/app/models/study_session.py (1)
  • StudySession (7-18)
backend/app/models/progress.py (1)
  • Progress (7-23)
frontend/src/pages/PracticeProblems.jsx (1)
frontend/src/services/api.js (6)
  • getPracticeProblems (39-40)
  • getPracticeProblems (39-40)
  • generatePracticeProblems (41-42)
  • generatePracticeProblems (41-42)
  • markProblemSolved (43-44)
  • markProblemSolved (43-44)
backend/app/api/endpoints/progress.py (4)
backend/app/database.py (1)
  • get_db (12-18)
backend/app/models/progress.py (1)
  • Progress (7-23)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/schemas/progress.py (1)
  • ProgressResponse (6-20)
backend/app/schemas/study_session.py (5)
backend/app/schemas/flashcard.py (1)
  • Config (38-39)
backend/app/schemas/practice_problem.py (1)
  • Config (28-29)
backend/app/schemas/progress.py (1)
  • Config (19-20)
backend/app/schemas/quiz.py (2)
  • Config (18-19)
  • Config (44-45)
backend/app/schemas/subject.py (1)
  • Config (27-28)
backend/app/models/__init__.py (6)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/models/flashcard.py (1)
  • Flashcard (7-27)
backend/app/models/quiz.py (2)
  • Quiz (7-21)
  • QuizQuestion (24-37)
backend/app/models/practice_problem.py (1)
  • PracticeProblem (7-22)
backend/app/models/study_session.py (1)
  • StudySession (7-18)
backend/app/models/progress.py (1)
  • Progress (7-23)
frontend/src/pages/Dashboard.jsx (1)
frontend/src/services/api.js (6)
  • getSubjects (13-13)
  • getSubjects (13-13)
  • createSubject (15-15)
  • createSubject (15-15)
  • deleteSubject (17-17)
  • deleteSubject (17-17)
frontend/src/services/api.js (1)
frontend/src/pages/QuizTake.jsx (1)
  • answers (10-10)
backend/app/schemas/practice_problem.py (5)
backend/app/schemas/flashcard.py (1)
  • Config (38-39)
backend/app/schemas/progress.py (1)
  • Config (19-20)
backend/app/schemas/quiz.py (2)
  • Config (18-19)
  • Config (44-45)
backend/app/schemas/study_session.py (1)
  • Config (17-18)
backend/app/schemas/subject.py (1)
  • Config (27-28)
backend/app/schemas/__init__.py (6)
backend/app/schemas/subject.py (3)
  • SubjectCreate (12-13)
  • SubjectUpdate (16-19)
  • SubjectResponse (22-28)
backend/app/schemas/flashcard.py (4)
  • FlashcardCreate (13-14)
  • FlashcardUpdate (17-21)
  • FlashcardResponse (28-39)
  • FlashcardReviewRequest (24-25)
backend/app/schemas/quiz.py (4)
  • QuizCreate (22-26)
  • QuizResponse (33-45)
  • QuizQuestionResponse (13-19)
  • QuizSubmitRequest (29-30)
backend/app/schemas/practice_problem.py (2)
  • PracticeProblemCreate (14-18)
  • PracticeProblemResponse (21-29)
backend/app/schemas/study_session.py (2)
  • StudySessionCreate (6-10)
  • StudySessionResponse (13-18)
backend/app/schemas/progress.py (1)
  • ProgressResponse (6-20)
backend/app/api/endpoints/quizzes.py (6)
backend/app/database.py (1)
  • get_db (12-18)
backend/app/models/quiz.py (2)
  • Quiz (7-21)
  • QuizQuestion (24-37)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/models/progress.py (1)
  • Progress (7-23)
backend/app/schemas/quiz.py (3)
  • QuizCreate (22-26)
  • QuizResponse (33-45)
  • QuizSubmitRequest (29-30)
backend/app/services/ai_service.py (1)
  • generate_quiz_questions (93-135)
backend/app/schemas/subject.py (5)
backend/app/schemas/flashcard.py (1)
  • Config (38-39)
backend/app/schemas/practice_problem.py (1)
  • Config (28-29)
backend/app/schemas/progress.py (1)
  • Config (19-20)
backend/app/schemas/quiz.py (2)
  • Config (18-19)
  • Config (44-45)
backend/app/schemas/study_session.py (1)
  • Config (17-18)
backend/app/models/subject.py (4)
frontend/src/pages/Flashcards.jsx (1)
  • flashcards (8-8)
frontend/src/pages/Quiz.jsx (1)
  • quizzes (9-9)
frontend/src/pages/Progress.jsx (1)
  • progress (7-7)
frontend/src/pages/SubjectDetails.jsx (1)
  • progress (9-9)
backend/app/api/endpoints/flashcards.py (7)
backend/app/database.py (1)
  • get_db (12-18)
backend/app/models/flashcard.py (1)
  • Flashcard (7-27)
backend/app/models/subject.py (1)
  • Subject (7-22)
backend/app/models/progress.py (1)
  • Progress (7-23)
backend/app/schemas/flashcard.py (4)
  • FlashcardCreate (13-14)
  • FlashcardUpdate (17-21)
  • FlashcardResponse (28-39)
  • FlashcardReviewRequest (24-25)
backend/app/services/ai_service.py (1)
  • generate_flashcards (14-53)
backend/app/services/spaced_repetition.py (1)
  • calculate_next_review (9-48)
backend/app/services/__init__.py (2)
backend/app/services/ai_service.py (1)
  • AIService (7-141)
backend/app/services/spaced_repetition.py (1)
  • SpacedRepetitionService (5-48)
🪛 Checkov (3.2.334)
docker-compose.yml

[medium] 27-28: Basic Auth Credentials

(CKV_SECRET_4)

🪛 dotenv-linter (4.0.0)
backend/.env.example

[warning] 14-14: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 18-18: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🪛 markdownlint-cli2 (0.18.1)
API_DOCUMENTATION.md

364-364: Bare URL used

(MD034, no-bare-urls)


366-366: Bare URL used

(MD034, no-bare-urls)

README.md

62-62: Bare URL used

(MD034, no-bare-urls)


158-158: Bare URL used

(MD034, no-bare-urls)


159-159: Bare URL used

(MD034, no-bare-urls)


160-160: Bare URL used

(MD034, no-bare-urls)


182-182: Bare URL used

(MD034, no-bare-urls)


226-226: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


256-256: Bare URL used

(MD034, no-bare-urls)


338-338: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


350-350: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


360-360: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


374-374: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


384-384: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


391-391: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


523-523: Bare URL used

(MD034, no-bare-urls)

🪛 OSV Scanner (2.2.4)
backend/requirements.txt

[CRITICAL] 1-1: python-jose 3.3.0: undefined

(PYSEC-2024-232)


[CRITICAL] 1-1: python-jose 3.3.0: undefined

(PYSEC-2024-233)


[CRITICAL] 1-1: python-jose 3.3.0: python-jose algorithm confusion with OpenSSH ECDSA keys

(GHSA-6c5p-j8vq-pqhj)


[CRITICAL] 1-1: python-jose 3.3.0: python-jose denial of service via compressed JWE content

(GHSA-cjwg-qfpm-7377)


[HIGH] 1-1: python-multipart 0.0.9: Denial of service (DoS) via deformation multipart/form-data boundary

(GHSA-59g5-xgcq-4qw3)

🪛 Ruff (0.14.3)
backend/app/services/ai_service.py

51-51: Do not catch blind exception: Exception

(BLE001)


89-89: Do not catch blind exception: Exception

(BLE001)


133-133: Do not catch blind exception: Exception

(BLE001)


137-137: Unused method argument: content

(ARG002)


137-137: Unused method argument: num_cards

(ARG002)

backend/app/api/endpoints/subjects.py

14-14: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


21-21: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


30-30: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


49-49: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


66-66: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

backend/app/api/endpoints/practice_problems.py

18-18: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


19-19: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


20-20: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


33-33: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


44-44: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


88-88: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


112-112: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

backend/app/config.py

16-16: Possible binding to all interfaces

(S104)

backend/app/api/endpoints/study_sessions.py

15-15: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


15-15: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


25-25: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


34-34: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


55-55: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

backend/app/api/endpoints/progress.py

14-14: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


21-21: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

backend/app/models/__init__.py

8-16: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

backend/app/schemas/__init__.py

8-25: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

backend/app/api/endpoints/quizzes.py

17-17: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


17-17: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


27-27: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


36-36: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


85-85: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


128-128: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

backend/app/api/endpoints/flashcards.py

18-18: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


18-18: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


28-28: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


28-28: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


38-38: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


51-51: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


85-85: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


102-102: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


139-139: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


156-156: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (13)
.gitignore (1)

1-45: LGTM! Comprehensive ignore patterns.

The .gitignore appropriately covers Python artifacts, environment files, build outputs, dependencies, and platform-specific files for this full-stack application.

frontend/postcss.config.js (1)

1-6: LGTM! Standard PostCSS configuration for Tailwind.

The configuration properly integrates Tailwind CSS and Autoprefixer with their default settings, which works well with the Tailwind setup.

frontend/vite.config.js (1)

1-15: LGTM! Proper Vite development configuration.

The proxy setup correctly forwards /api requests to the backend at port 8000 during development, enabling seamless full-stack development workflow.

backend/requirements.txt (1)

1-13: I'll verify whether these package versions are actually outdated by checking the latest available versions:

Update packages to more recent stable versions. No known vulnerabilities were found for the pinned versions, but several packages are significantly outdated. FastAPI's latest version is 0.121.0 released November 3, 2025, compared to your pinned 0.109.2 from early 2024. Similarly, SQLAlchemy's latest version was released October 10, 2025, and Pydantic's latest version was released November 5, 2025. All packages are actively maintained with recent updates. Consider updating to their latest stable versions within the current major release series (e.g., FastAPI 0.121.0, SQLAlchemy 2.0.44, Pydantic 2.12.4) to benefit from bug fixes, performance improvements, and security enhancements.

frontend/src/main.jsx (1)

1-10: Bootstrapper looks good.
Nothing to fix here—the Vite/React entrypoint is wired correctly.

frontend/src/index.css (1)

12-41: No action required.

The Tailwind primary color palette is properly configured with all required scales (500, 600, 700) used in the CSS classes. The configuration is complete and will build successfully.

docker-compose.yml (1)

32-34: LGTM! Proper health check dependency.

Using condition: service_healthy ensures the backend waits for PostgreSQL to be ready before starting, preventing connection errors during startup.

API_DOCUMENTATION.md (1)

354-360: LGTM! Security considerations properly documented.

The documentation correctly identifies missing authentication and rate limiting as production deployment concerns. This transparency helps users understand the current limitations.

backend/app/services/__init__.py (1)

1-4: LGTM! Clean module exports.

The service module properly exports AIService and SpacedRepetitionService with an explicit __all__ list, following Python best practices for public API definition.

backend/alembic.ini (1)

15-28: LGTM! Appropriate logging configuration.

The logging setup properly configures root, SQLAlchemy, and Alembic loggers with appropriate levels (WARN for root and SQLAlchemy, INFO for Alembic), which helps with debugging migrations without excessive output.

frontend/tailwind.config.js (1)

1-26: LGTM! Clean Tailwind configuration.

The Tailwind config is properly structured with:

  • Appropriate content paths for Vite + React
  • Well-defined primary color palette (blue theme, shades 50-900)
  • Proper TypeScript type annotation

The configuration aligns with the frontend build setup and provides a consistent color system used throughout the Layout component and other UI elements.

backend/app/api/__init__.py (1)

1-11: LGTM! Clean API router composition.

The central API router properly aggregates all endpoint routers with:

  • Consistent prefix patterns (/subjects, /flashcards, etc.)
  • Matching tags for OpenAPI documentation organization
  • Clean import structure

This composition pattern makes it easy to mount all API routes under /api in the main application and maintains good separation of concerns.

backend/app/database.py (1)

12-18: LGTM! Proper database session dependency.

The get_db() dependency correctly:

  • Yields a session for request handling
  • Ensures cleanup in a finally block
  • Follows FastAPI's recommended pattern for database session management

Comment thread API_DOCUMENTATION.md
Comment on lines +380 to +392
## Filtering and Sorting

**Filtering:**
```http
GET /flashcards?subject_id=1&difficulty=medium&topic=Calculus
```

**Sorting:**
```http
GET /quizzes?subject_id=1&sort=created_at&order=desc
```

(Note: Some filtering/sorting options may need to be implemented)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify which filtering and sorting features are implemented.

The note "(Note: Some filtering/sorting options may need to be implemented)" is vague and could confuse users trying to use these features.

Consider either:

  1. Explicitly documenting which filters/sorts are currently supported
  2. Removing the examples for unimplemented features and noting them as "planned"
  3. Adding a status indicator (✅ Implemented, 🚧 Planned) next to each feature

This helps users understand what they can use today versus what's planned.

🤖 Prompt for AI Agents
In API_DOCUMENTATION.md around lines 380 to 392, the "Filtering and Sorting"
section contains vague note about unimplemented options; update the doc to
clearly indicate which query parameters are supported today versus planned: list
each filter/sort option used in the examples with a status indicator (e.g., ✅
Implemented or 🚧 Planned), or remove any example that references an
unimplemented feature and mark it as Planned; ensure every query parameter shown
has a short one-line status and, for implemented ones, include expected behavior
and accepted values.

Comment thread backend/.env.example
DEBUG=True

# CORS Configuration
CORS_ORIGINS=["http://localhost:5173", "http://localhost:3000"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix CORS_ORIGINS format for proper parsing.

The JSON array value should be wrapped in quotes for environment variable parsing. Without quotes, this may cause parsing issues with pydantic-settings.

Apply this diff:

-CORS_ORIGINS=["http://localhost:5173", "http://localhost:3000"]
+CORS_ORIGINS='["http://localhost:5173", "http://localhost:3000"]'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CORS_ORIGINS=["http://localhost:5173", "http://localhost:3000"]
CORS_ORIGINS='["http://localhost:5173", "http://localhost:3000"]'
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 14-14: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In backend/.env.example around line 14, the CORS_ORIGINS value is currently an
unquoted JSON array which pydantic-settings may not parse; replace it with a
quoted JSON string (e.g., wrap the entire array in single quotes so the
environment variable is a string: CORS_ORIGINS='[\"http://localhost:5173\",
\"http://localhost:3000\"]') so parsers read it as a single string containing a
JSON array.

Comment thread backend/.env.example

# Application Settings
APP_NAME="AI Study Assistant"
APP_VERSION="1.0.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant quotes from APP_VERSION.

The value already contains quotes which will be parsed as part of the string literal (e.g., "1.0.0" instead of 1.0.0).

Apply this diff:

-APP_VERSION="1.0.0"
+APP_VERSION=1.0.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
APP_VERSION="1.0.0"
APP_VERSION=1.0.0
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 18-18: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
In backend/.env.example around line 18, APP_VERSION is set with redundant
surrounding quotes which will be parsed into the literal value; remove the
surrounding double quotes so the environment variable is stored without quotes
(e.g., APP_VERSION=1.0.0) to ensure loaders read the raw version string.

Comment thread backend/alembic.ini
[alembic]
script_location = alembic
prepend_sys_path = .
sqlalchemy.url = postgresql://studyuser:studypass@localhost:5432/studyassistant

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace hardcoded database URL with placeholder.

The hardcoded database credentials pose a security risk if this file is committed. According to the AI summary, backend/alembic/env.py overrides this value from settings at runtime, making this hardcoded URL both redundant and misleading.

Apply this diff to use a placeholder that makes the override behavior clear:

-sqlalchemy.url = postgresql://studyuser:studypass@localhost:5432/studyassistant
+sqlalchemy.url = driver://user:pass@localhost/dbname

This follows Alembic best practices where the actual URL is provided by the environment script, and the ini file contains only a placeholder for documentation purposes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sqlalchemy.url = postgresql://studyuser:studypass@localhost:5432/studyassistant
sqlalchemy.url = driver://user:pass@localhost/dbname
🤖 Prompt for AI Agents
In backend/alembic.ini around line 4, the file contains a hardcoded SQLAlchemy
URL which is insecure and redundant because backend/alembic/env.py supplies the
real URL at runtime; replace the hardcoded credentials with a clear placeholder
(e.g., sqlalchemy.url = driver://user:pass@localhost/dbname or a comment) that
documents the value is provided by the env.py or environment, ensuring no real
credentials remain in the committed ini file.

Comment on lines +24 to +33
if not progress:
# Create progress if it doesn't exist
subject = db.query(Subject).filter(Subject.id == subject_id).first()
if not subject:
raise HTTPException(status_code=404, detail="Subject not found")

progress = Progress(subject_id=subject_id)
db.add(progress)
db.commit()
db.refresh(progress)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep GET /progress/{id} side-effect free.

This GET endpoint creates rows when none exist, breaching HTTP safety semantics and causing hidden writes on cache retries or monitoring pings. Please refuse the request (404) or move initialization to a dedicated create flow instead of mutating inside GET.

-    progress = db.query(Progress).filter(Progress.subject_id == subject_id).first()
-    if not progress:
-        # Create progress if it doesn't exist
-        subject = db.query(Subject).filter(Subject.id == subject_id).first()
-        if not subject:
-            raise HTTPException(status_code=404, detail="Subject not found")
-
-        progress = Progress(subject_id=subject_id)
-        db.add(progress)
-        db.commit()
-        db.refresh(progress)
-
-    return progress
+    subject = db.query(Subject).filter(Subject.id == subject_id).first()
+    if not subject:
+        raise HTTPException(status_code=404, detail="Subject not found")
+
+    progress = db.query(Progress).filter(Progress.subject_id == subject_id).first()
+    if not progress:
+        raise HTTPException(status_code=404, detail="Progress not found")
+
+    return progress

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/app/api/endpoints/progress.py around lines 24 to 33, the GET handler
currently creates a Progress row when none exists which introduces a
side-effectful write; remove the creation logic so the GET remains read-only and
instead raise HTTPException(status_code=404, detail="Progress not found") when
no progress exists; if initialization is required, move that code to a dedicated
POST/PUT create endpoint or an initialization service and ensure the GET only
performs queries (no db.add/db.commit/db.refresh).

Comment on lines +18 to +38
const loadQuiz = async () => {
try {
const response = await getQuiz(quizId);
setQuiz(response.data);

// Initialize answers for completed quiz
if (response.data.completed_at) {
const existingAnswers = {};
response.data.questions.forEach((q) => {
if (q.user_answer) {
existingAnswers[q.id] = q.user_answer;
}
});
setAnswers(existingAnswers);
}
} catch (error) {
console.error('Error loading quiz:', error);
} finally {
setLoading(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset answers when loading a different quiz.

When you switch from one quiz to another, the old answers map sticks around because we never clear it. That leaves stale question IDs in state, so the UI shows an inflated answered count and the submission payload contains answers for unrelated questions (the backend can’t match those IDs). Please reset answers before optionally seeding completed quizzes.

       const response = await getQuiz(quizId);
       setQuiz(response.data);
+      setAnswers({});
 
       // Initialize answers for completed quiz
       if (response.data.completed_at) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const loadQuiz = async () => {
try {
const response = await getQuiz(quizId);
setQuiz(response.data);
// Initialize answers for completed quiz
if (response.data.completed_at) {
const existingAnswers = {};
response.data.questions.forEach((q) => {
if (q.user_answer) {
existingAnswers[q.id] = q.user_answer;
}
});
setAnswers(existingAnswers);
}
} catch (error) {
console.error('Error loading quiz:', error);
} finally {
setLoading(false);
}
};
const loadQuiz = async () => {
try {
const response = await getQuiz(quizId);
setQuiz(response.data);
setAnswers({});
// Initialize answers for completed quiz
if (response.data.completed_at) {
const existingAnswers = {};
response.data.questions.forEach((q) => {
if (q.user_answer) {
existingAnswers[q.id] = q.user_answer;
}
});
setAnswers(existingAnswers);
}
} catch (error) {
console.error('Error loading quiz:', error);
} finally {
setLoading(false);
}
};
🤖 Prompt for AI Agents
In frontend/src/pages/QuizTake.jsx around lines 18 to 38, the answers state is
not cleared when loading a new quiz so stale answers persist across quizzes;
before optionally seeding answers for a completed quiz, call the setter to reset
answers to an empty object (e.g., setAnswers({})) so you clear old question IDs,
then only populate existingAnswers from response.data.questions when
response.data.completed_at is set; ensure this happens inside the try block
before seeding and does not rely on prior state.

Comment on lines +12 to +29
useEffect(() => {
loadData();
}, [id]);

const loadData = async () => {
try {
const [subjectRes, progressRes] = await Promise.all([
getSubject(id),
getSubjectProgress(id)
]);
setSubject(subjectRes.data);
setProgress(progressRes.data);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add loadData to useEffect dependencies or move it inside useEffect.

The useEffect hook calls loadData() but doesn't include it in the dependency array. This violates React Hooks rules and the react-hooks/exhaustive-deps ESLint rule, potentially causing stale closure bugs.

Preferred solution: Define loadData inside the useEffect:

   useEffect(() => {
-    loadData();
-  }, [id]);
-
-  const loadData = async () => {
-    try {
-      const [subjectRes, progressRes] = await Promise.all([
-        getSubject(id),
-        getSubjectProgress(id)
-      ]);
-      setSubject(subjectRes.data);
-      setProgress(progressRes.data);
-    } catch (error) {
-      console.error('Error loading data:', error);
-    } finally {
-      setLoading(false);
-    }
-  };
+    const loadData = async () => {
+      try {
+        const [subjectRes, progressRes] = await Promise.all([
+          getSubject(id),
+          getSubjectProgress(id)
+        ]);
+        setSubject(subjectRes.data);
+        setProgress(progressRes.data);
+      } catch (error) {
+        console.error('Error loading data:', error);
+      } finally {
+        setLoading(false);
+      }
+    };
+    
+    loadData();
+  }, [id]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
loadData();
}, [id]);
const loadData = async () => {
try {
const [subjectRes, progressRes] = await Promise.all([
getSubject(id),
getSubjectProgress(id)
]);
setSubject(subjectRes.data);
setProgress(progressRes.data);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
const loadData = async () => {
try {
const [subjectRes, progressRes] = await Promise.all([
getSubject(id),
getSubjectProgress(id)
]);
setSubject(subjectRes.data);
setProgress(progressRes.data);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};
loadData();
}, [id]);
🤖 Prompt for AI Agents
In frontend/src/pages/SubjectDetails.jsx around lines 12 to 29, the useEffect
calls loadData but doesn't include it in the dependency array which violates
React Hooks rules; define the async loadData function inside the useEffect (or
alternatively wrap it with useCallback and include it in deps) and then call it
immediately, ensuring you keep [id] in the dependency array and remove the
external loadData reference so ESLint no longer flags
react-hooks/exhaustive-deps.

Comment on lines +3 to +10
const API_BASE_URL = 'http://localhost:8000/api';

const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Externalize the API base URL
Hard-coding http://localhost:8000/api means any deployed build (Docker, staging, production, even a different dev port) will fail to reach the backend. Please read the base URL from configuration (e.g., import.meta.env.VITE_API_URL) and fall back to localhost only when the env var is absent.

-const API_BASE_URL = 'http://localhost:8000/api';
+const API_BASE_URL =
+  import.meta.env.VITE_API_URL?.replace(/\/$/, '') ?? 'http://localhost:8000/api';

Then initialize Axios with baseURL: API_BASE_URL.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const API_BASE_URL = 'http://localhost:8000/api';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
const API_BASE_URL =
import.meta.env.VITE_API_URL?.replace(/\/$/, '') ?? 'http://localhost:8000/api';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
🤖 Prompt for AI Agents
In frontend/src/services/api.js around lines 3 to 10, the API base URL is
hard-coded to 'http://localhost:8000/api'; change this to read from
configuration by setting API_BASE_URL = import.meta.env.VITE_API_URL ||
'http://localhost:8000/api' (or equivalent env access pattern), then pass that
API_BASE_URL into axios.create via baseURL: API_BASE_URL so deployments can
override the backend URL via VITE_API_URL while falling back to localhost.

Comment on lines +24 to +25
export const generateFlashcards = (subjectId, topic, numCards = 5) =>
api.post(`/flashcards/generate?subject_id=${subjectId}&topic=${topic}&num_cards=${numCards}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Encode generation query params
Interpolating topic directly into the query string breaks when the topic contains &, ?, or = (e.g., “Signals & Systems”), so the backend receives truncated or incorrect values. Use Axios’ params option to handle encoding.

-export const generateFlashcards = (subjectId, topic, numCards = 5) =>
-  api.post(`/flashcards/generate?subject_id=${subjectId}&topic=${topic}&num_cards=${numCards}`);
+export const generateFlashcards = (subjectId, topic, numCards = 5) =>
+  api.post('/flashcards/generate', null, {
+    params: { subject_id: subjectId, topic, num_cards: numCards },
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const generateFlashcards = (subjectId, topic, numCards = 5) =>
api.post(`/flashcards/generate?subject_id=${subjectId}&topic=${topic}&num_cards=${numCards}`);
export const generateFlashcards = (subjectId, topic, numCards = 5) =>
api.post('/flashcards/generate', null, {
params: { subject_id: subjectId, topic, num_cards: numCards },
});
🤖 Prompt for AI Agents
In frontend/src/services/api.js around lines 24-25, the function builds the
query string by interpolating topic (and other params) directly which breaks
when topic contains characters like '&', '?' or '='; change the API call to use
Axios' params option (pass an object with subject_id, topic and num_cards)
instead of manual string concatenation so Axios URL-encodes values correctly and
avoids truncated/incorrect backend values.

Comment thread README.md
Comment on lines +78 to +81
#### 1. Clone the Repository
```bash
cd /home/user/study
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix the clone instructions.

Step “Clone the Repository” never actually clones anything—cd /home/user/study assumes a path that doesn’t exist yet. Anyone following the README will fail right here. Please add the missing git clone … command (and then cd into the freshly cloned directory).

-```bash
-cd /home/user/study
-```
+```bash
+git clone https://github.com/qwertystars/study.git
+cd study
+```
🤖 Prompt for AI Agents
In README.md around lines 78 to 81, the "Clone the Repository" step only does
`cd /home/user/study` and never actually clones the repo; update the step to
first run a git clone command for the repository URL (e.g., git clone
https://github.com/qwertystars/study.git) and then cd into the newly created
directory (cd study) so the instructions create and enter the project folder
correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants