Skip to content

Build AI-Powered Flashcard Study App#3

Open
qwertystars wants to merge 1 commit into
mainfrom
claude/build-flashcard-app-011CUvJ921douvkTxvh3hHAz
Open

Build AI-Powered Flashcard Study App#3
qwertystars wants to merge 1 commit into
mainfrom
claude/build-flashcard-app-011CUvJ921douvkTxvh3hHAz

Conversation

@qwertystars

@qwertystars qwertystars commented Nov 8, 2025

Copy link
Copy Markdown
Owner

Core Features:
✅ AI-powered flashcard generation from multiple sources ✅ Spaced repetition system (SM-2 simplified)
✅ User authentication with JWT
✅ Full deck management (CRUD)
✅ Interactive practice mode with flip animations
✅ Support for PDF, PPTX, text, and YouTube

Tech Stack:
Backend:

  • FastAPI + PostgreSQL + SQLAlchemy
  • AI integration (OpenAI & Anthropic APIs)
  • File parsing (PyPDF2, python-pptx, yt-dlp)
  • JWT authentication
  • Spaced repetition algorithm

Frontend:

  • React 18 + TypeScript + Vite
  • TailwindCSS + shadcn/ui components
  • React Router for navigation
  • Axios for API calls
  • Responsive design

Infrastructure:

  • Docker Compose setup
  • PostgreSQL database
  • Nginx reverse proxy
  • Environment configuration

Project Structure:

  • backend/: FastAPI application with models, routes, services
  • frontend/: React app with pages, components, hooks
  • docker-compose.yml: Complete deployment setup
  • README.md: Comprehensive documentation

See README.md for setup and usage instructions.

Summary by CodeRabbit

  • New Features

    • Complete flashcard learning application with AI-powered card generation from multiple content sources (files, text, YouTube).
    • Spaced repetition study system to optimize retention.
    • User authentication and deck management.
    • Practice mode with review tracking and progress indicators.
    • Support for uploading PDFs, PowerPoints, and text materials.
  • Documentation

    • Comprehensive README with setup, usage, API reference, and troubleshooting guides.
  • Chores

    • Project initialization with backend (FastAPI, PostgreSQL) and frontend (React, TypeScript) infrastructure.
    • Docker and Docker Compose configuration for containerized deployment.

Core Features:
✅ AI-powered flashcard generation from multiple sources
✅ Spaced repetition system (SM-2 simplified)
✅ User authentication with JWT
✅ Full deck management (CRUD)
✅ Interactive practice mode with flip animations
✅ Support for PDF, PPTX, text, and YouTube

Tech Stack:
Backend:
- FastAPI + PostgreSQL + SQLAlchemy
- AI integration (OpenAI & Anthropic APIs)
- File parsing (PyPDF2, python-pptx, yt-dlp)
- JWT authentication
- Spaced repetition algorithm

Frontend:
- React 18 + TypeScript + Vite
- TailwindCSS + shadcn/ui components
- React Router for navigation
- Axios for API calls
- Responsive design

Infrastructure:
- Docker Compose setup
- PostgreSQL database
- Nginx reverse proxy
- Environment configuration

Project Structure:
- backend/: FastAPI application with models, routes, services
- frontend/: React app with pages, components, hooks
- docker-compose.yml: Complete deployment setup
- README.md: Comprehensive documentation

See README.md for setup and usage instructions.
@coderabbitai

coderabbitai Bot commented Nov 8, 2025

Copy link
Copy Markdown

Walkthrough

Introduces a complete AI-powered spaced repetition flashcard application. Includes a FastAPI backend with user authentication, deck/flashcard management, spaced repetition practice, and AI-driven card generation; a React 18/TypeScript frontend with dashboard, practice, and upload interfaces; Docker/Docker Compose deployment setup; and comprehensive documentation.

Changes

Cohort / File(s) Summary
Configuration & Environment
.env.example, backend/.env.example, backend/.gitignore, frontend/.gitignore
Sample environment templates for database, JWT, AI keys, and upload settings; comprehensive ignore patterns for Python artifacts, node modules, and local configs.
Backend Initialization & Configuration
backend/Dockerfile, backend/app/__init__.py, backend/app/config.py, backend/app/database.py, backend/app/main.py
FastAPI app setup with SQLAlchemy ORM configuration, database initialization on startup, CORS configuration, and router registration; Dockerfile builds Python 3.11 image with dependencies.
Backend Models
backend/app/models/__init__.py, backend/app/models/user.py, backend/app/models/deck.py, backend/app/models/flashcard.py, backend/app/models/review.py
SQLAlchemy ORM models defining User (with email and hashed password), Deck (associated with user), Flashcard (associated with deck), and Review (tracking spaced repetition intervals). Includes cascade delete relationships.
Backend API Routes
backend/app/routes/__init__.py, backend/app/routes/auth.py, backend/app/routes/decks.py, backend/app/routes/flashcards.py, backend/app/routes/practice.py, backend/app/routes/upload.py
Full CRUD endpoints for decks and flashcards; authentication endpoints (signup, login, token, me); practice endpoints for due cards and review submission; upload endpoint with file/text/YouTube support and AI flashcard generation.
Backend Schemas
backend/app/schemas/__init__.py, backend/app/schemas/user.py, backend/app/schemas/deck.py, backend/app/schemas/flashcard.py, backend/app/schemas/review.py
Pydantic request/response models for user auth, deck management, flashcard CRUD, and review tracking with ORM integration via from_attributes.
Backend Services & Utilities
backend/app/services/__init__.py, backend/app/services/ai_service.py, backend/app/utils/__init__.py, backend/app/utils/auth.py, backend/app/utils/file_parser.py, backend/app/utils/spaced_repetition.py
AIService with OpenAI/Anthropic provider support for flashcard generation and text summarization; password hashing and JWT token management; file parsing for PDF, PowerPoint, and YouTube; SM-2 spaced repetition algorithm.
Backend Runtime
backend/requirements.txt, backend/run.py
Python dependencies (FastAPI, SQLAlchemy, Pydantic, JWT, file processing, AI clients); development run script with uvicorn auto-reload.
Frontend Build & Configuration
frontend/package.json, frontend/tsconfig.json, frontend/tsconfig.node.json, frontend/vite.config.ts, frontend/tailwind.config.js, frontend/postcss.config.js
npm project definition with React 18, TypeScript, and build tools; Vite bundler config with /api proxy; Tailwind CSS theming and extend config; PostCSS with Tailwind and Autoprefixer.
Frontend Entry & Styling
frontend/index.html, frontend/src/main.tsx, frontend/src/index.css
HTML root with favicon and entry script; React app bootstrap in StrictMode; Tailwind base styles with CSS custom properties for light/dark mode theming.
Frontend Routing & Auth
frontend/src/App.tsx, frontend/src/contexts/AuthContext.tsx
React Router setup with PrivateRoute/PublicRoute guards; AuthContext providing login, signup, logout, and current user state with localStorage persistence.
Frontend UI Components
frontend/src/components/ui/button.tsx, frontend/src/components/ui/card.tsx, frontend/src/components/ui/input.tsx, frontend/src/components/ui/label.tsx, frontend/src/components/ui/toast.tsx, frontend/src/components/ui/toaster.tsx
CVA-based Button with variants; Card sub-components (Header, Title, Description, Content, Footer); styled Input and Label; Toast system with Radix UI primitives and lifecycle animations; Toaster consumer component.
Frontend Hooks & Utilities
frontend/src/hooks/use-toast.ts, frontend/src/lib/api.ts, frontend/src/lib/utils.ts, frontend/src/types/index.ts
Custom toast hook with reducer-based state and pub-sub; Axios API client with Bearer token injection and grouped endpoint wrappers; formatting utilities (date, DateTime, overdue check); TypeScript interfaces for User, Deck, Flashcard, Review, Auth.
Frontend Pages
frontend/src/pages/Login.tsx, frontend/src/pages/Signup.tsx, frontend/src/pages/Dashboard.tsx, frontend/src/pages/DeckEditor.tsx, frontend/src/pages/Practice.tsx, frontend/src/pages/Upload.tsx
Login and signup forms with validation and error handling; Dashboard listing user's decks with today's due count and quick actions; DeckEditor for inline flashcard editing/deletion; Practice page with spaced repetition flow and review submission; Upload page with file/text/YouTube tabs and AI card generation.
Infrastructure & Deployment
docker-compose.yml, frontend/Dockerfile, frontend/nginx.conf
Docker Compose orchestration with PostgreSQL 15 (healthcheck), FastAPI backend, and React frontend services; multi-stage React Dockerfile with Vite build and Nginx static serving; Nginx config with gzip, API reverse proxy, and static asset caching.
Documentation
README.md
Comprehensive project documentation covering features, tech stack, project structure, prerequisites, quick start, local development, usage guide, API endpoints, configuration, spaced repetition details, troubleshooting, and production deployment guidance.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as React Frontend
    participant Backend as FastAPI Backend
    participant DB as PostgreSQL
    participant AI as OpenAI/Anthropic

    Note over User,AI: User Authentication & Deck Creation Flow

    User->>Frontend: Sign up with email/password
    Frontend->>Backend: POST /api/auth/signup
    Backend->>DB: Create User (hashed password)
    Backend->>Frontend: Return User + Token
    Frontend->>Frontend: Store token in localStorage
    
    Note over User,AI: Upload Content & Generate Flashcards

    User->>Frontend: Upload file/text/YouTube URL
    Frontend->>Backend: POST /api/upload/process (multipart/form-data)
    Backend->>Backend: Parse content (FileParser)
    Backend->>AI: Generate flashcards from content
    AI-->>Backend: Return list of Q&A pairs
    Backend->>DB: Create Deck + Flashcards
    Backend->>Frontend: Return Deck with card_count
    Frontend->>Frontend: Navigate to deck editor

    Note over User,AI: Spaced Repetition Practice Flow

    User->>Frontend: Click "Practice" on deck
    Frontend->>Backend: GET /api/practice/deck/{id}/due
    Backend->>DB: Query Flashcards filtered by next_review <= now
    Backend->>Frontend: Return due Flashcards + Review metadata
    Frontend->>Frontend: Display flashcard (question side)
    
    User->>Frontend: Click "Remembered" or "Forgot"
    Frontend->>Backend: POST /api/practice/review (remembered: bool)
    Backend->>Backend: Calculate next_review via SM-2 algorithm
    Backend->>DB: Insert Review record
    Backend->>Frontend: Return Review with updated interval
    Frontend->>Frontend: Show next flashcard or completion
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Specific areas requiring extra attention:
    • AI service fallback logic and JSON parsing error handling across OpenAI and Anthropic providers
    • Spaced repetition algorithm correctness (calculate_next_review doubling logic and interval calculations)
    • JWT token security: expiration handling, Bearer token injection in API interceptor, secret key management
    • File upload safety: temporary file cleanup, path traversal prevention, error handling in FileParser
    • Practice endpoint ownership validation and due-card query logic (left join with Review for latest interval/next_review)
    • Frontend auth context token refresh and logout flow consistency
    • Frontend API error handling and edge cases (network failures, malformed responses)
    • Docker Compose health checks and service dependencies (backend awaiting db readiness)

Poem

🐰 Hoppy hops through files so bright,
A flashcard kingdom, organized right!
With decks and reviews dancing in line,
AI learns your pace (SM-2 divine)—
Now practice your way, one card at a time! 📚✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.33% 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 'Build AI-Powered Flashcard Study App' accurately and clearly summarizes the main objective of the changeset: constructing a complete AI-powered flashcard application.
✨ 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/build-flashcard-app-011CUvJ921douvkTxvh3hHAz

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: 20

🧹 Nitpick comments (12)
README.md (2)

38-38: Add language specifier to code fence.

The project structure code block needs a language identifier for proper markdown formatting.

-```
+```plaintext
 flashcard-app/
 ├── backend/

105-107: Wrap bare URLs in markdown link syntax.

Five bare URLs should be wrapped in markdown brackets for consistent formatting per markdownlint standards (MD034).

-   - Frontend: http://localhost:3000
-   - Backend API: http://localhost:8000
-   - API Documentation: http://localhost:8000/docs
+   - Frontend: [`http://localhost:3000`](http://localhost:3000)
+   - Backend API: [`http://localhost:8000`](http://localhost:8000)
+   - API Documentation: [`http://localhost:8000/docs`](http://localhost:8000/docs)
-   The API will be available at http://localhost:8000
+   The API will be available at [`http://localhost:8000`](http://localhost:8000)
-   The app will be available at http://localhost:3000
+   The app will be available at [`http://localhost:3000`](http://localhost:3000)

Also applies to: 160-160, 179-179

backend/Dockerfile (1)

1-25: Consider running the application as a non-root user.

The Dockerfile runs the application as root (default user in the container), which is a security risk if the container is compromised. Industry best practice is to create and switch to a non-root user.

Apply this diff to add a non-root user:

 # Create uploads directory
 RUN mkdir -p uploads
 
+# Create non-root user
+RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
+USER appuser
+
 # Expose port
 EXPOSE 8000
frontend/Dockerfile (1)

18-28: Consider adding a non-root user for nginx.

Running nginx as root in production is a security concern. While nginx itself drops privileges, the container process starts as root.

Add a non-root user before the CMD:

 FROM nginx:alpine
 
 # Copy built assets from build stage
 COPY --from=build /app/dist /usr/share/nginx/html
 
 # Copy nginx configuration
 COPY nginx.conf /etc/nginx/conf.d/default.conf
 
+# Run as non-root user
+RUN chown -R nginx:nginx /usr/share/nginx/html
+USER nginx
+
 EXPOSE 80
 
 CMD ["nginx", "-g", "daemon off;"]

Note: This may require adjusting nginx.conf to bind to port 8080 instead of 80, and updating EXPOSE accordingly.

backend/app/database.py (1)

8-8: Consider adding connection pool settings for production.

The engine is created with minimal configuration. For production deployments, consider adding resilience and pool management settings.

Apply this diff to add production-grade settings:

-engine = create_engine(settings.DATABASE_URL)
+engine = create_engine(
+    settings.DATABASE_URL,
+    pool_pre_ping=True,  # Verify connections before using
+    pool_size=5,
+    max_overflow=10,
+    pool_recycle=3600,  # Recycle connections after 1 hour
+)
  • pool_pre_ping=True detects and discards stale connections
  • Pool sizing prevents connection exhaustion
  • pool_recycle helps with databases that close idle connections
frontend/src/App.tsx (1)

74-89: Consider consolidating the Practice routes.

Both /practice and /practice/:deckId render the same Practice component. While this works, you could simplify by using a single route with an optional parameter: /practice/:deckId?.

Apply this diff to consolidate:

-      <Route
-        path="/practice"
-        element={
-          <PrivateRoute>
-            <Practice />
-          </PrivateRoute>
-        }
-      />
-      <Route
-        path="/practice/:deckId"
-        element={
-          <PrivateRoute>
-            <Practice />
-          </PrivateRoute>
-        }
-      />
+      <Route
+        path="/practice/:deckId?"
+        element={
+          <PrivateRoute>
+            <Practice />
+          </PrivateRoute>
+        }
+      />
backend/app/schemas/__init__.py (1)

7-20: Consider sorting __all__ for consistency.

The static analysis tool suggests applying isort-style sorting to the __all__ list. This is a minor style improvement that enhances consistency.

Apply this diff:

 __all__ = [
-    "UserCreate",
-    "UserLogin",
-    "UserResponse",
-    "Token",
     "DeckCreate",
+    "DeckResponse",
     "DeckUpdate",
-    "DeckResponse",
     "FlashcardCreate",
+    "FlashcardResponse",
     "FlashcardUpdate",
-    "FlashcardResponse",
     "ReviewCreate",
     "ReviewResponse",
+    "Token",
+    "UserCreate",
+    "UserLogin",
+    "UserResponse",
 ]
frontend/src/pages/Signup.tsx (1)

22-29: Consider adding password strength validation.

Currently, only password matching is validated client-side. Adding minimum length or strength requirements would improve user experience by providing immediate feedback before the backend rejects weak passwords.

Example addition:

     if (password !== confirmPassword) {
       toast({
         title: 'Error',
         description: 'Passwords do not match',
         variant: 'destructive',
       });
       return;
     }
+
+    if (password.length < 8) {
+      toast({
+        title: 'Error',
+        description: 'Password must be at least 8 characters',
+        variant: 'destructive',
+      });
+      return;
+    }
frontend/src/pages/DeckEditor.tsx (2)

108-108: Consider using a custom confirmation dialog.

Native confirm() dialogs work but don't match the app's design system and can't be styled. Consider using a custom dialog component from your UI library for better UX consistency.

Also applies to: 128-128


50-75: Consider adding loading state to the add card form.

The form submission doesn't disable the submit button or show a loading state during the API call. This could allow users to double-submit the form. Consider adding a loading state.

Example implementation:

 export default function DeckEditor() {
   // ... existing state ...
+  const [addingCard, setAddingCard] = useState(false);

   const handleAddCard = async (e: React.FormEvent) => {
     e.preventDefault();
     if (!deckId || !newQuestion || !newAnswer) return;
+    setAddingCard(true);

     try {
       const newCard = await flashcardsAPI.create({
         question: newQuestion,
         answer: newAnswer,
         deck_id: parseInt(deckId),
       });

       setFlashcards([...flashcards, newCard]);
       setNewQuestion('');
       setNewAnswer('');
       toast({
         title: 'Success',
         description: 'Flashcard added',
       });
     } catch (error) {
       toast({
         title: 'Error',
         description: 'Failed to add flashcard',
         variant: 'destructive',
       });
+    } finally {
+      setAddingCard(false);
     }
   };

Then update the button:

-              <Button type="submit">
+              <Button type="submit" disabled={addingCard}>
                 <Plus className="h-4 w-4 mr-2" />
-                Add Flashcard
+                {addingCard ? 'Adding...' : 'Add Flashcard'}
               </Button>
backend/app/utils/auth.py (1)

32-42: Consider using timezone-aware datetime for future compatibility.

Lines 36 and 38 use datetime.utcnow(), which is deprecated in Python 3.12+ in favor of datetime.now(timezone.utc). While not critical, updating would ensure future compatibility.

Apply this diff to use timezone-aware datetime:

 def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
     """Create a JWT access token"""
     to_encode = data.copy()
     if expires_delta:
-        expire = datetime.utcnow() + expires_delta
+        expire = datetime.now(timezone.utc) + expires_delta
     else:
-        expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+        expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
 
     to_encode.update({"exp": expire})
     encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
     return encoded_jwt

Note: You'll need to add from datetime import timezone to the imports.

backend/app/utils/file_parser.py (1)

14-47: Add timeout to prevent hanging on network operations.

While the PDF, PPTX, and text parsers don't have the same security issues as the YouTube parser, consider adding timeouts if any of these libraries make network calls internally. For the current implementation, the exception handling could be improved by chaining exceptions and being more specific.

Example improvement for exception handling:

     def parse_pdf(file_path: str) -> str:
         """Extract text from a PDF file"""
         try:
             reader = PdfReader(file_path)
             text = ""
             for page in reader.pages:
                 text += page.extract_text() + "\n"
             return text.strip()
-        except Exception as e:
-            raise ValueError(f"Error parsing PDF: {str(e)}")
+        except Exception as e:
+            raise ValueError(f"Error parsing PDF: {str(e)}") from e
📜 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 7767d67.

📒 Files selected for processing (64)
  • .env.example (1 hunks)
  • README.md (1 hunks)
  • backend/.env.example (1 hunks)
  • backend/.gitignore (1 hunks)
  • backend/Dockerfile (1 hunks)
  • backend/app/__init__.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/deck.py (1 hunks)
  • backend/app/models/flashcard.py (1 hunks)
  • backend/app/models/review.py (1 hunks)
  • backend/app/models/user.py (1 hunks)
  • backend/app/routes/__init__.py (1 hunks)
  • backend/app/routes/auth.py (1 hunks)
  • backend/app/routes/decks.py (1 hunks)
  • backend/app/routes/flashcards.py (1 hunks)
  • backend/app/routes/practice.py (1 hunks)
  • backend/app/routes/upload.py (1 hunks)
  • backend/app/schemas/__init__.py (1 hunks)
  • backend/app/schemas/deck.py (1 hunks)
  • backend/app/schemas/flashcard.py (1 hunks)
  • backend/app/schemas/review.py (1 hunks)
  • backend/app/schemas/user.py (1 hunks)
  • backend/app/services/__init__.py (1 hunks)
  • backend/app/services/ai_service.py (1 hunks)
  • backend/app/utils/__init__.py (1 hunks)
  • backend/app/utils/auth.py (1 hunks)
  • backend/app/utils/file_parser.py (1 hunks)
  • backend/app/utils/spaced_repetition.py (1 hunks)
  • backend/requirements.txt (1 hunks)
  • backend/run.py (1 hunks)
  • docker-compose.yml (1 hunks)
  • frontend/.gitignore (1 hunks)
  • frontend/Dockerfile (1 hunks)
  • frontend/index.html (1 hunks)
  • frontend/nginx.conf (1 hunks)
  • frontend/package.json (1 hunks)
  • frontend/postcss.config.js (1 hunks)
  • frontend/src/App.tsx (1 hunks)
  • frontend/src/components/ui/button.tsx (1 hunks)
  • frontend/src/components/ui/card.tsx (1 hunks)
  • frontend/src/components/ui/input.tsx (1 hunks)
  • frontend/src/components/ui/label.tsx (1 hunks)
  • frontend/src/components/ui/toast.tsx (1 hunks)
  • frontend/src/components/ui/toaster.tsx (1 hunks)
  • frontend/src/contexts/AuthContext.tsx (1 hunks)
  • frontend/src/hooks/use-toast.ts (1 hunks)
  • frontend/src/index.css (1 hunks)
  • frontend/src/lib/api.ts (1 hunks)
  • frontend/src/lib/utils.ts (1 hunks)
  • frontend/src/main.tsx (1 hunks)
  • frontend/src/pages/Dashboard.tsx (1 hunks)
  • frontend/src/pages/DeckEditor.tsx (1 hunks)
  • frontend/src/pages/Login.tsx (1 hunks)
  • frontend/src/pages/Practice.tsx (1 hunks)
  • frontend/src/pages/Signup.tsx (1 hunks)
  • frontend/src/pages/Upload.tsx (1 hunks)
  • frontend/src/types/index.ts (1 hunks)
  • frontend/tailwind.config.js (1 hunks)
  • frontend/tsconfig.json (1 hunks)
  • frontend/tsconfig.node.json (1 hunks)
  • frontend/vite.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (33)
frontend/src/pages/Login.tsx (2)
frontend/src/contexts/AuthContext.tsx (1)
  • useAuth (74-80)
backend/app/routes/auth.py (1)
  • login (44-63)
frontend/src/pages/DeckEditor.tsx (2)
frontend/src/types/index.ts (2)
  • Deck (7-16)
  • Flashcard (18-27)
frontend/src/lib/api.ts (2)
  • decksAPI (47-71)
  • flashcardsAPI (74-105)
frontend/src/components/ui/label.tsx (1)
frontend/src/lib/utils.ts (1)
  • cn (4-6)
backend/app/schemas/__init__.py (4)
backend/app/schemas/user.py (4)
  • UserCreate (7-11)
  • UserLogin (14-18)
  • UserResponse (21-29)
  • Token (32-36)
backend/app/schemas/deck.py (3)
  • DeckCreate (7-11)
  • DeckUpdate (14-18)
  • DeckResponse (21-34)
backend/app/schemas/flashcard.py (3)
  • FlashcardCreate (7-12)
  • FlashcardUpdate (15-19)
  • FlashcardResponse (22-35)
backend/app/schemas/review.py (2)
  • ReviewCreate (6-10)
  • ReviewResponse (13-25)
frontend/src/contexts/AuthContext.tsx (4)
backend/app/models/user.py (1)
  • User (8-20)
frontend/src/types/index.ts (3)
  • User (1-5)
  • LoginCredentials (44-47)
  • SignupCredentials (49-52)
frontend/src/lib/api.ts (1)
  • authAPI (29-44)
backend/app/routes/auth.py (2)
  • login (44-63)
  • signup (22-40)
frontend/src/pages/Practice.tsx (2)
frontend/src/types/index.ts (2)
  • Deck (7-16)
  • Flashcard (18-27)
frontend/src/lib/api.ts (2)
  • decksAPI (47-71)
  • practiceAPI (108-123)
frontend/src/components/ui/input.tsx (1)
frontend/src/lib/utils.ts (1)
  • cn (4-6)
frontend/src/pages/Upload.tsx (1)
frontend/src/lib/api.ts (1)
  • uploadAPI (126-144)
backend/app/schemas/deck.py (3)
backend/app/schemas/flashcard.py (1)
  • Config (34-35)
backend/app/schemas/review.py (1)
  • Config (24-25)
backend/app/schemas/user.py (1)
  • Config (28-29)
backend/app/schemas/review.py (3)
backend/app/schemas/deck.py (1)
  • Config (33-34)
backend/app/schemas/flashcard.py (1)
  • Config (34-35)
backend/app/schemas/user.py (1)
  • Config (28-29)
backend/app/utils/auth.py (3)
backend/app/database.py (1)
  • get_db (17-23)
backend/app/models/user.py (1)
  • User (8-20)
backend/app/schemas/user.py (1)
  • TokenData (39-42)
backend/app/models/flashcard.py (1)
frontend/src/types/index.ts (1)
  • Flashcard (18-27)
frontend/src/components/ui/toaster.tsx (2)
frontend/src/hooks/use-toast.ts (1)
  • useToast (186-186)
frontend/src/components/ui/toast.tsx (6)
  • ToastProvider (120-120)
  • Toast (122-122)
  • ToastTitle (123-123)
  • ToastDescription (124-124)
  • ToastClose (125-125)
  • ToastViewport (121-121)
frontend/src/App.tsx (2)
frontend/src/contexts/AuthContext.tsx (2)
  • useAuth (74-80)
  • AuthProvider (16-72)
frontend/src/components/ui/toaster.tsx (1)
  • Toaster (11-33)
frontend/src/pages/Signup.tsx (2)
frontend/src/contexts/AuthContext.tsx (1)
  • useAuth (74-80)
backend/app/routes/auth.py (1)
  • signup (22-40)
frontend/src/components/ui/toast.tsx (1)
frontend/src/lib/utils.ts (1)
  • cn (4-6)
frontend/src/components/ui/button.tsx (1)
frontend/src/lib/utils.ts (1)
  • cn (4-6)
backend/app/schemas/flashcard.py (3)
backend/app/schemas/deck.py (1)
  • Config (33-34)
backend/app/schemas/review.py (1)
  • Config (24-25)
backend/app/schemas/user.py (1)
  • Config (28-29)
frontend/src/hooks/use-toast.ts (1)
frontend/src/components/ui/toast.tsx (3)
  • ToastProps (118-118)
  • ToastActionElement (119-119)
  • Toast (122-122)
frontend/src/types/index.ts (4)
backend/app/models/user.py (1)
  • User (8-20)
backend/app/models/deck.py (1)
  • Deck (8-22)
backend/app/models/flashcard.py (1)
  • Flashcard (8-22)
backend/app/models/review.py (1)
  • Review (8-25)
backend/app/routes/decks.py (7)
backend/app/database.py (1)
  • get_db (17-23)
backend/app/models/user.py (1)
  • User (8-20)
backend/app/models/deck.py (1)
  • Deck (8-22)
backend/app/models/flashcard.py (1)
  • Flashcard (8-22)
backend/app/models/review.py (1)
  • Review (8-25)
backend/app/schemas/deck.py (3)
  • DeckCreate (7-11)
  • DeckUpdate (14-18)
  • DeckResponse (21-34)
backend/app/utils/auth.py (1)
  • get_current_user (45-68)
frontend/src/components/ui/card.tsx (1)
frontend/src/lib/utils.ts (1)
  • cn (4-6)
backend/app/routes/practice.py (9)
backend/app/database.py (1)
  • get_db (17-23)
backend/app/models/user.py (1)
  • User (8-20)
backend/app/models/deck.py (1)
  • Deck (8-22)
backend/app/models/flashcard.py (1)
  • Flashcard (8-22)
backend/app/models/review.py (1)
  • Review (8-25)
backend/app/schemas/flashcard.py (1)
  • FlashcardResponse (22-35)
backend/app/schemas/review.py (2)
  • ReviewCreate (6-10)
  • ReviewResponse (13-25)
backend/app/utils/auth.py (1)
  • get_current_user (45-68)
backend/app/utils/spaced_repetition.py (2)
  • SpacedRepetition (6-45)
  • calculate_next_review (10-33)
frontend/src/pages/Dashboard.tsx (3)
frontend/src/types/index.ts (2)
  • Deck (7-16)
  • Flashcard (18-27)
frontend/src/contexts/AuthContext.tsx (1)
  • useAuth (74-80)
frontend/src/lib/api.ts (2)
  • decksAPI (47-71)
  • practiceAPI (108-123)
backend/app/models/deck.py (1)
frontend/src/types/index.ts (1)
  • Deck (7-16)
backend/app/models/review.py (1)
frontend/src/types/index.ts (1)
  • Review (29-37)
backend/app/models/user.py (1)
frontend/src/types/index.ts (1)
  • User (1-5)
backend/app/routes/upload.py (9)
backend/app/database.py (1)
  • get_db (17-23)
backend/app/models/user.py (1)
  • User (8-20)
backend/app/models/deck.py (1)
  • Deck (8-22)
backend/app/models/flashcard.py (1)
  • Flashcard (8-22)
backend/app/schemas/deck.py (1)
  • DeckResponse (21-34)
backend/app/schemas/flashcard.py (1)
  • FlashcardResponse (22-35)
backend/app/utils/auth.py (1)
  • get_current_user (45-68)
backend/app/utils/file_parser.py (3)
  • FileParser (11-114)
  • parse_file (81-114)
  • parse_youtube (50-78)
backend/app/services/ai_service.py (2)
  • AIService (9-199)
  • generate_flashcards (16-45)
backend/app/routes/flashcards.py (7)
backend/app/database.py (1)
  • get_db (17-23)
backend/app/models/user.py (1)
  • User (8-20)
backend/app/models/deck.py (1)
  • Deck (8-22)
backend/app/models/flashcard.py (1)
  • Flashcard (8-22)
backend/app/models/review.py (1)
  • Review (8-25)
backend/app/schemas/flashcard.py (3)
  • FlashcardCreate (7-12)
  • FlashcardUpdate (15-19)
  • FlashcardResponse (22-35)
backend/app/utils/auth.py (1)
  • get_current_user (45-68)
backend/app/routes/auth.py (4)
backend/app/database.py (1)
  • get_db (17-23)
backend/app/models/user.py (1)
  • User (8-20)
backend/app/schemas/user.py (4)
  • UserCreate (7-11)
  • UserLogin (14-18)
  • UserResponse (21-29)
  • Token (32-36)
backend/app/utils/auth.py (4)
  • verify_password (22-24)
  • get_password_hash (27-29)
  • create_access_token (32-42)
  • get_current_user (45-68)
backend/app/models/__init__.py (5)
backend/app/models/user.py (1)
  • User (8-20)
frontend/src/types/index.ts (4)
  • User (1-5)
  • Deck (7-16)
  • Flashcard (18-27)
  • Review (29-37)
backend/app/models/deck.py (1)
  • Deck (8-22)
backend/app/models/flashcard.py (1)
  • Flashcard (8-22)
backend/app/models/review.py (1)
  • Review (8-25)
backend/app/schemas/user.py (3)
backend/app/schemas/deck.py (1)
  • Config (33-34)
backend/app/schemas/flashcard.py (1)
  • Config (34-35)
backend/app/schemas/review.py (1)
  • Config (24-25)
frontend/src/lib/api.ts (1)
frontend/src/types/index.ts (7)
  • SignupCredentials (49-52)
  • User (1-5)
  • LoginCredentials (44-47)
  • AuthTokens (39-42)
  • Deck (7-16)
  • Flashcard (18-27)
  • Review (29-37)
🪛 ast-grep (0.39.7)
backend/app/utils/file_parser.py

[warning] 57-57: The function mktemp is deprecated. When using this function, it is possible for an attacker to modify the created file before the filename is returned. Use NamedTemporaryFile() instead and pass it the delete=False parameter.
Context: tempfile.mktemp()
Note: [CWE-377]: Insecure Temporary File [OWASP A01:2021]: Broken Access Control [REFERENCES]
https://docs.python.org/3/library/tempfile.html#tempfile.mktemp
https://owasp.org/Top10/A01_2021-Broken_Access_Control

(avoid-mktemp-python)

🪛 Checkov (3.2.334)
docker-compose.yml

[medium] 29-30: Basic Auth Credentials

(CKV_SECRET_4)

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

[warning] 6-6: [UnorderedKey] The ALGORITHM key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 7-7: [UnorderedKey] The ACCESS_TOKEN_EXPIRE_MINUTES key should go before the ALGORITHM key

(UnorderedKey)


[warning] 11-11: [UnorderedKey] The ANTHROPIC_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The MAX_UPLOAD_SIZE key should go before the UPLOAD_DIR key

(UnorderedKey)


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

(ValueWithoutQuotes)

.env.example

[warning] 9-9: [UnorderedKey] The ANTHROPIC_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 13-13: [UnorderedKey] The MAX_UPLOAD_SIZE key should go before the UPLOAD_DIR key

(UnorderedKey)

🪛 markdownlint-cli2 (0.18.1)
README.md

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

(MD040, fenced-code-language)


105-105: Bare URL used

(MD034, no-bare-urls)


106-106: Bare URL used

(MD034, no-bare-urls)


107-107: Bare URL used

(MD034, no-bare-urls)


160-160: Bare URL used

(MD034, no-bare-urls)


179-179: Bare URL used

(MD034, no-bare-urls)

🪛 OSV Scanner (2.2.4)
backend/requirements.txt

[HIGH] 1-1: fastapi 0.109.0: undefined

(PYSEC-2024-38)


[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.6: python-multipart vulnerable to Content-Type Header ReDoS

(GHSA-2jv5-9r88-3w3p)


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

(GHSA-59g5-xgcq-4qw3)


[HIGH] 1-1: yt-dlp 2024.1.4: yt-dlp has dependency on potentially malicious third-party code in Douyu extractors

(GHSA-3v33-3wmw-3785)


[HIGH] 1-1: yt-dlp 2024.1.4: yt-dlp File system modification and RCE through improper file-extension sanitization

(GHSA-79w7-vh3h-8g4j)


[HIGH] 1-1: yt-dlp 2024.1.4: yt-dlp: --exec command injection when using %q in yt-dlp on Windows (Bypass of CVE-2023-40581)

(GHSA-hjq6-52gw-2g7p)

🪛 Ruff (0.14.3)
backend/app/schemas/__init__.py

7-20: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

backend/app/utils/auth.py

46-46: 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)


62-62: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

backend/app/services/ai_service.py

37-37: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Avoid specifying long messages outside the exception class

(TRY003)


50-50: Avoid specifying long messages outside the exception class

(TRY003)


89-89: Prefer TypeError exception for invalid type

(TRY004)


89-89: Abstract raise to an inner function

(TRY301)


89-89: Avoid specifying long messages outside the exception class

(TRY003)


93-93: Abstract raise to an inner function

(TRY301)


93-93: Avoid specifying long messages outside the exception class

(TRY003)


98-98: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


98-98: Avoid specifying long messages outside the exception class

(TRY003)


98-98: Use explicit conversion flag

Replace with conversion flag

(RUF010)


99-99: Do not catch blind exception: Exception

(BLE001)


100-100: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


100-100: Avoid specifying long messages outside the exception class

(TRY003)


100-100: Use explicit conversion flag

Replace with conversion flag

(RUF010)


105-105: Avoid specifying long messages outside the exception class

(TRY003)


144-144: Prefer TypeError exception for invalid type

(TRY004)


144-144: Abstract raise to an inner function

(TRY301)


144-144: Avoid specifying long messages outside the exception class

(TRY003)


148-148: Abstract raise to an inner function

(TRY301)


148-148: Avoid specifying long messages outside the exception class

(TRY003)


153-153: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


153-153: Avoid specifying long messages outside the exception class

(TRY003)


153-153: Use explicit conversion flag

Replace with conversion flag

(RUF010)


154-154: Do not catch blind exception: Exception

(BLE001)


155-155: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


155-155: Avoid specifying long messages outside the exception class

(TRY003)


155-155: Use explicit conversion flag

Replace with conversion flag

(RUF010)


175-175: Avoid specifying long messages outside the exception class

(TRY003)

backend/run.py

7-7: Possible binding to all interfaces

(S104)

backend/app/main.py

54-54: Possible binding to all interfaces

(S104)

backend/app/routes/decks.py

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)


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)


37-37: Comparison to None should be cond is None

Replace with cond is None

(E711)


61-61: 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)


62-62: 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)


80-80: Comparison to None should be cond is None

Replace with cond is None

(E711)


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)


103-103: 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)


132-132: 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)


133-133: 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)


159-159: Comparison to None should be cond is None

Replace with cond is None

(E711)


179-179: 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)


180-180: 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

13-13: Possible hardcoded password assigned to: "SECRET_KEY"

(S105)

backend/app/utils/file_parser.py

23-23: Do not catch blind exception: Exception

(BLE001)


24-24: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


24-24: Avoid specifying long messages outside the exception class

(TRY003)


24-24: Use explicit conversion flag

Replace with conversion flag

(RUF010)


37-37: Do not catch blind exception: Exception

(BLE001)


38-38: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


38-38: Avoid specifying long messages outside the exception class

(TRY003)


38-38: Use explicit conversion flag

Replace with conversion flag

(RUF010)


46-46: Do not catch blind exception: Exception

(BLE001)


47-47: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


47-47: Avoid specifying long messages outside the exception class

(TRY003)


47-47: Use explicit conversion flag

Replace with conversion flag

(RUF010)


58-58: Use of insecure and deprecated function (mktemp)

(S306)


67-67: Probable use of requests call without timeout

(S113)


71-71: Probable use of requests call without timeout

(S113)


77-77: Do not catch blind exception: Exception

(BLE001)


78-78: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


78-78: Avoid specifying long messages outside the exception class

(TRY003)


78-78: Use explicit conversion flag

Replace with conversion flag

(RUF010)

backend/app/routes/practice.py

24-24: 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)


74-74: 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)


75-75: 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)


126-126: 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)


127-127: 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/routes/upload.py

24-24: Do not perform function call File 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)


31-31: 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)


71-74: Abstract raise to an inner function

(TRY301)


77-80: Abstract raise to an inner function

(TRY301)


121-124: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


125-125: Do not catch blind exception: Exception

(BLE001)


126-129: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


128-128: Use explicit conversion flag

Replace with conversion flag

(RUF010)


137-137: 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)


138-138: 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)


179-179: Consider moving this statement to an else block

(TRY300)


182-185: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


186-186: Do not catch blind exception: Exception

(BLE001)


187-190: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


189-189: Use explicit conversion flag

Replace with conversion flag

(RUF010)

backend/app/routes/flashcards.py

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)


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)


68-68: 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)


69-69: 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)


110-110: 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)


111-111: 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)


148-148: 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)


149-149: 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)


199-199: 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)


200-200: 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/routes/auth.py

22-22: 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)


68-68: 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)


68-68: 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)


89-89: 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

7-7: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

🔇 Additional comments (24)
backend/app/__init__.py (1)

1-1: Module initializer looks good.

A minimal __init__.py with a docstring is a reasonable approach. If your package needs to expose a public API (models, services, configuration), consider adding strategic imports/exports here as the backend grows.

README.md (1)

1-397: Comprehensive documentation with clear setup and usage guidance.

The README provides excellent coverage of features, tech stack, project structure, setup procedures (both Docker and local), API endpoints, configuration, and troubleshooting. The spaced repetition algorithm explanation is helpful, and the production deployment section adds operational value. Clear structure makes it accessible to both new contributors and users.

frontend/.gitignore (1)

1-24: LGTM!

Comprehensive gitignore patterns covering logs, dependencies, build artifacts, and editor files appropriate for a Vite/React project.

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

1-1: LGTM!

Standard package initializer.

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

1-1: LGTM!

Standard package initializer.

backend/.env.example (1)

1-15: LGTM!

Appropriate environment variable templates for backend configuration. The static analysis warnings about key ordering are stylistic preferences that don't affect functionality. The ValueWithoutQuotes warning for MAX_UPLOAD_SIZE is a false positive—numeric values don't require quotes in .env files.

Note: Configuration duplication with root .env.example was already flagged in the root file review.

frontend/src/components/ui/label.tsx (1)

1-24: LGTM!

Clean implementation following shadcn/ui patterns. Proper use of forwardRef, TypeScript typing, and integration with Radix UI primitives. The peer-disabled styling ensures good accessibility.

backend/.gitignore (1)

1-47: LGTM!

Comprehensive gitignore covering Python artifacts, virtual environments, sensitive configs, uploads, and database files. Well-organized and appropriate for the backend service.

backend/run.py (1)

4-10: Binding to 0.0.0.0 is acceptable for development.

Static analysis flags the host="0.0.0.0" binding as a potential security risk. However, this is a development script (as indicated by the docstring and reload=True), and binding to all interfaces is standard practice to allow access when running in Docker containers.

Ensure that production deployments use a more restrictive host binding or rely on proper network configuration.

frontend/src/main.tsx (1)

1-10: LGTM!

Standard React 18 bootstrap pattern with StrictMode enabled. The non-null assertion on line 6 is safe given that the root element is guaranteed to exist in index.html.

frontend/vite.config.ts (1)

1-22: LGTM! Standard Vite configuration.

The configuration correctly sets up React, path aliases, and development server with API proxying to the backend.

frontend/src/index.css (1)

1-59: LGTM! Standard Tailwind and shadcn/ui theming setup.

The CSS properly integrates Tailwind directives and defines comprehensive theme variables for light and dark modes.

frontend/src/components/ui/toaster.tsx (1)

11-33: LGTM! Clean toast notification implementation.

The component correctly integrates with the toast system, handles optional fields, and follows React best practices.

backend/app/database.py (1)

17-23: LGTM! Proper session lifecycle management.

The dependency correctly yields a session and ensures cleanup in the finally block.

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

1-7: LGTM! Model exports are correctly defined.

The module properly re-exports the core ORM models. The current order (User, then domain entities) is logical, though the linter suggests alphabetical sorting which you can optionally apply.

backend/app/config.py (1)

17-23: LGTM! AI and upload configuration looks reasonable.

Optional API keys and explicit upload limits are well-structured. The 50MB upload limit is clearly expressed.

docker-compose.yml (1)

30-30: Ensure SECRET_KEY is documented as required for production.

The default SECRET_KEY value includes a warning, but relying on defaults for security-critical values is risky. Ensure your README or deployment documentation emphasizes that SECRET_KEY must be set to a strong, random value in production.

frontend/src/contexts/AuthContext.tsx (1)

1-80: LGTM! Clean authentication context implementation.

The authentication flow is well-structured with proper loading state management and token persistence. The auto-login after signup (lines 47-51) provides good UX, and error handling is appropriately delegated to consuming components.

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

1-35: LGTM! Well-structured Pydantic schemas.

The flashcard schemas are properly defined with appropriate field types and ORM integration via from_attributes = True.

backend/app/models/review.py (1)

1-25: LGTM! Appropriate model for spaced repetition tracking.

The Review model correctly captures the essential spaced repetition fields. The required next_review field (line 20) ensures explicit scheduling of reviews, which is appropriate for the SM-2 algorithm.

backend/app/utils/auth.py (1)

45-68: LGTM! Dependency injection is correctly implemented.

The Depends() calls in argument defaults (line 46) are the correct FastAPI pattern for dependency injection. The static analysis warnings are false positives and can be safely ignored.

backend/app/routes/auth.py (1)

1-91: LGTM! Well-implemented authentication endpoints.

The auth routes follow FastAPI best practices with appropriate error handling, status codes, and response models. The duplicate login endpoints (lines 43-63 for JSON, lines 66-85 for OAuth2 form) provide good API flexibility.

Note: The static analysis warnings about Depends() in argument defaults are false positives—this is the correct FastAPI dependency injection pattern.

frontend/nginx.conf (1)

1-36: LGTM! Well-configured nginx setup with appropriate optimizations.

The configuration properly handles:

  • SPA routing with fallback to index.html (lines 14-16)
  • API proxying with necessary headers (lines 19-29)
  • Static asset caching for performance (lines 32-35)
  • Gzip compression for reduced bandwidth (lines 8-12)

Ensure the backend FastAPI app mounts routers with the /api prefix to match the proxy configuration at line 19.

frontend/src/pages/Dashboard.tsx (1)

1-174: LGTM! Well-structured dashboard with good UX patterns.

The implementation demonstrates good practices:

  • Concurrent data loading with Promise.all (lines 25-28) for better performance
  • Proper error handling with user feedback via toast (lines 31-36)
  • Loading state management (lines 37-39)
  • Event propagation handling for nested clickable elements (lines 148-149, 158-159)
  • Responsive grid layout and conditional rendering

Comment thread .env.example
Comment on lines +1 to +13
# Database
DATABASE_URL=postgresql://flashcard_user:flashcard_password@localhost:5432/flashcards_db

# JWT Secret (generate a strong random key for production)
SECRET_KEY=your-super-secret-key-change-in-production

# AI API Keys (at least one is required)
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...

# Application Settings
UPLOAD_DIR=./uploads
MAX_UPLOAD_SIZE=50000000

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

Configuration duplication with backend/.env.example.

Both root .env.example and backend/.env.example define overlapping environment variables (DATABASE_URL, SECRET_KEY, API keys, upload settings). This creates ambiguity about which file should be used and maintained.

Consider one of these approaches:

  • Option 1 (recommended): Keep only backend/.env.example since the backend is the primary consumer of these variables, and remove the root-level file.
  • Option 2: If multiple services need separate configs, document clearly in README which file each service uses and ensure backend/app/config.py explicitly references the correct path.
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 9-9: [UnorderedKey] The ANTHROPIC_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 13-13: [UnorderedKey] The MAX_UPLOAD_SIZE key should go before the UPLOAD_DIR key

(UnorderedKey)

🤖 Prompt for AI Agents
In .env.example around lines 1-13, these environment variables duplicate
backend/.env.example causing ambiguity; choose the recommended Option 1: remove
the root-level .env.example file and ensure backend/.env.example contains the
canonical DATABASE_URL, SECRET_KEY, API keys and upload settings; update any
project scripts/CI/docs that referenced the root .env to point to backend/.env
and run a quick grep to replace references to the root file so only
backend/.env.example is maintained.

Comment thread backend/app/config.py
"""Application settings"""

# Database
DATABASE_URL: str = "postgresql://user:password@localhost:5432/flashcards_db"

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 | 🔴 Critical

Remove hardcoded credentials from default DATABASE_URL.

The default contains user:password, which is a security risk if the environment variable is not set. This could lead to accidental exposure or use in development/staging.

Apply this diff:

-    DATABASE_URL: str = "postgresql://user:password@localhost:5432/flashcards_db"
+    DATABASE_URL: str

This forces the environment variable to be set explicitly. Update the README or provide a .env.example to document the expected format.

📝 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
DATABASE_URL: str = "postgresql://user:password@localhost:5432/flashcards_db"
DATABASE_URL: str
🤖 Prompt for AI Agents
In backend/app/config.py around line 10, remove the hardcoded DATABASE_URL value
containing "user:password" and instead require the DATABASE_URL to come from the
environment; if the env var is missing or empty, raise an explicit error (or
fail fast) so the app cannot start with default credentials. Update the code to
read the value from the environment and validate presence, and then document the
required variable format in README or add a .env.example as suggested.

Comment thread backend/app/config.py
Comment on lines +13 to +15
SECRET_KEY: str = "your-secret-key-change-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

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 | 🔴 Critical

Remove insecure default SECRET_KEY and enforce explicit configuration.

The default "your-secret-key-change-in-production" is a critical security vulnerability. If .env is missing or misconfigured, JWT tokens will be signed with this weak, publicly known key, compromising all authentication.

Apply this diff:

-    SECRET_KEY: str = "your-secret-key-change-in-production"
+    SECRET_KEY: str
     ALGORITHM: str = "HS256"
     ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

This forces explicit configuration. Document in README that SECRET_KEY must be a cryptographically random string (e.g., generated via openssl rand -hex 32).

📝 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
SECRET_KEY: str = "your-secret-key-change-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
🧰 Tools
🪛 Ruff (0.14.3)

13-13: Possible hardcoded password assigned to: "SECRET_KEY"

(S105)

🤖 Prompt for AI Agents
In backend/app/config.py around lines 13 to 15, remove the insecure hardcoded
default SECRET_KEY and require an explicit environment value; change the code to
read SECRET_KEY from the environment (no fallback) and raise a clear startup
error if it's missing or too short/weak (e.g., enforce minimum byte/hex length),
and update README to document that SECRET_KEY must be a cryptographically random
string (example: openssl rand -hex 32) and how to set it in .env or env vars.

Comment on lines +32 to +41
due_count = (
db.query(Flashcard)
.outerjoin(Review)
.filter(Flashcard.deck_id == deck.id)
.filter(
(Review.next_review <= datetime.utcnow()) | (Review.next_review == None)
)
.distinct()
.count()
)

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

Scope due_count to the current user’s latest review

The due_count query joins every review row, so it (a) includes other users’ reviews and (b) marks a card as due if any historical review is overdue—even when the current user’s latest review is scheduled in the future. That makes the dashboard show inflated due counts. Reuse the same “latest review per user” logic you apply in the practice routes and check due-ness against that single row.

Apply this diff (replicate the pattern in each due_count block):

-        due_count = (
-            db.query(Flashcard)
-            .outerjoin(Review)
-            .filter(Flashcard.deck_id == deck.id)
-            .filter(
-                (Review.next_review <= datetime.utcnow()) | (Review.next_review == None)
-            )
-            .distinct()
-            .count()
-        )
+        due_count = 0
+        cards = db.query(Flashcard).filter(Flashcard.deck_id == deck.id).all()
+        for card in cards:
+            latest_review = (
+                db.query(Review)
+                .filter(
+                    Review.flashcard_id == card.id,
+                    Review.user_id == current_user.id,
+                )
+                .order_by(Review.reviewed_at.desc())
+                .first()
+            )
+            if not latest_review or SpacedRepetition.is_due(latest_review.next_review):
+                due_count += 1

Remember to import SpacedRepetition at the top and apply the same fix in get_deck and update_deck.

Also applies to: 77-83, 156-162

🧰 Tools
🪛 Ruff (0.14.3)

37-37: Comparison to None should be cond is None

Replace with cond is None

(E711)

🤖 Prompt for AI Agents
In backend/app/routes/decks.py around lines 32-41, the due_count query currently
joins all Review rows causing counts to include other users' reviews and
historical reviews; replace it with the "latest review per user" pattern used in
practice routes: join/Subquery to select only the latest Review per
(flashcard_id, user_id) (using SpacedRepetition.latest_review logic), then join
that subquery to Flashcard and filter due-ness against that single
latest_review.next_review (next_review <= datetime.utcnow() or next_review is
NULL). Import SpacedRepetition at the top of the file and apply the same
replacement for the other due_count blocks at lines 77-83 and 156-162, and
likewise update get_deck and update_deck to use the latest-review-scoped query
when computing due_count.

Comment on lines +55 to +65
if not latest_review or latest_review.next_review <= datetime.utcnow():
card_dict = {
"id": card.id,
"deck_id": card.deck_id,
"question": card.question,
"answer": card.answer,
"created_at": card.created_at,
"updated_at": card.updated_at,
"next_review": latest_review.next_review if latest_review else None,
"interval_days": latest_review.interval_days if latest_review else None,
}

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 | 🔴 Critical

Fix timezone-aware due checks

Review.next_review is stored with timezone information (SQLAlchemy DateTime(timezone=True)), so the ORM returns offset-aware datetimes. Comparing them with datetime.utcnow() creates a naïve-versus-aware comparison, which raises a TypeError: can't compare offset-naive and offset-aware datetimes as soon as a reviewed card is fetched. This breaks both /practice/deck/{deck_id}/due and /practice/today. Switch to a timezone-aware “now” before comparing.

Apply this diff:

-from datetime import datetime
+from datetime import datetime, timezone
@@
-        if not latest_review or latest_review.next_review <= datetime.utcnow():
+        if not latest_review or latest_review.next_review <= datetime.now(timezone.utc):
@@
-        if not latest_review or latest_review.next_review <= datetime.utcnow():
+        if not latest_review or latest_review.next_review <= datetime.now(timezone.utc):

Also applies to: 154-164

🤖 Prompt for AI Agents
In backend/app/routes/practice.py around lines 55-65 (and also apply the same
change at lines 154-164), you are comparing offset-aware Review.next_review
datetimes to a naïve datetime.utcnow(), causing a TypeError; update the code to
use a timezone-aware "now" (for example datetime.now(timezone.utc) or
datetime.utcnow().replace(tzinfo=timezone.utc)) wherever datetime.utcnow() is
used for these comparisons so the left and right operands are both offset-aware;
replace each datetime.utcnow() in these blocks with datetime.now(timezone.utc)
(and add "from datetime import timezone" if not already imported).

Comment thread frontend/Dockerfile
COPY package*.json ./

# Install dependencies
RUN npm install

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

Use npm ci for reproducible builds.

Using npm install without enforcing the lockfile can lead to inconsistent builds across environments. This could introduce subtle bugs or dependency conflicts.

Apply this diff:

-RUN npm install
+RUN npm ci

This ensures the exact dependency tree from package-lock.json is installed.

📝 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
RUN npm install
RUN npm ci
🤖 Prompt for AI Agents
In frontend/Dockerfile around line 9, the Dockerfile uses "RUN npm install"
which can produce non-reproducible installs; replace it with "RUN npm ci" so the
image installs exact versions from package-lock.json, ensuring reproducible
builds and failing if lockfile is out of sync.

Comment thread frontend/package.json
Comment on lines +12 to +27
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.1",
"axios": "^1.6.5",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0",
"lucide-react": "^0.309.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 | 🔴 Critical

🧩 Analysis chain

Verify dependency versions for security updates.

The specified versions are from late 2023/early 2024. Given the current date (November 2025), these packages may have security patches or important updates available.

Run the following script to check for outdated packages and security advisories:


🏁 Script executed:

#!/bin/bash
# Check for latest versions and security advisories for key frontend dependencies

echo "=== Checking React ==="
curl -s https://registry.npmjs.org/react | jq -r '.["dist-tags"].latest'

echo "=== Checking Axios ==="
curl -s https://registry.npmjs.org/axios | jq -r '.["dist-tags"].latest'

echo "=== Checking React Router ==="
curl -s https://registry.npmjs.org/react-router-dom | jq -r '.["dist-tags"].latest'

echo "=== Security advisories for axios ==="
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NPM, package: "axios") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

Length of output: 1396


Update axios to resolve critical security vulnerabilities.

The specified version (^1.6.5) includes axios 1.6.5, which has HIGH-severity vulnerabilities:

  • Denial-of-service attack through lack of data size check (patched in 1.12.0)
  • SSRF and credential leakage via absolute URL (patched in 1.8.2)

Upgrade to at least ^1.12.0 or higher (latest: 1.13.2) to resolve these issues.

🤖 Prompt for AI Agents
In frontend/package.json around lines 12 to 27 the axios dependency is pinned to
^1.6.5 which contains known high-severity vulnerabilities; update the axios
entry to at least ^1.12.0 (preferably the latest 1.13.2), then run your package
manager to reinstall and update the lockfile (npm install / yarn install / pnpm
install), run the test suite and any vulnerability audits (npm audit / yarn
audit) and commit the updated package.json and lockfile so the fixed version is
pinned in CI.

Comment on lines +32 to +34
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>

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 | 🔴 Critical

Fix the ref type mismatch.

The ref is typed as HTMLParagraphElement but the component renders an h3 element, which is HTMLHeadingElement. This type inconsistency could cause type errors when consumers try to access heading-specific properties on the ref.

Apply this diff to fix the type:

 const CardTitle = React.forwardRef<
-  HTMLParagraphElement,
+  HTMLHeadingElement,
   React.HTMLAttributes<HTMLHeadingElement>
 >(({ className, ...props }, ref) => (
📝 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 CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
const CardTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
🤖 Prompt for AI Agents
In frontend/src/components/ui/card.tsx around lines 32 to 34, the forwardRef
generics use HTMLParagraphElement for the ref while the component renders an h3
(an HTMLHeadingElement); change the first generic type from HTMLParagraphElement
to HTMLHeadingElement so the ref type matches the rendered element, keeping the
existing React.HTMLAttributes<HTMLHeadingElement> for props.

Comment on lines +31 to +37
setDeck(deckData);
setCards(cardsData);
} else {
// Practice all due cards
const cardsData = await practiceAPI.getTodaysReviews();
setCards(cardsData);
}

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 | 🔴 Critical

Reset practice state when reloading cards

Switching from /practice/:deckId to another deck reuses this component, so currentIndex and showAnswer retain their previous values. If the previous deck left you at, say, index 5 but the new deck only returns one due card, cards[currentIndex] is undefined and the render attempts to read .question, throwing a runtime TypeError. Reset the practice state each time a fresh card list arrives.

@@
         const [deckData, cardsData] = await Promise.all([
           decksAPI.getById(parseInt(deckId)),
           practiceAPI.getDueCards(parseInt(deckId)),
         ]);
         setDeck(deckData);
         setCards(cardsData);
+        setCurrentIndex(0);
+        setShowAnswer(false);
       } else {
         // Practice all due cards
         const cardsData = await practiceAPI.getTodaysReviews();
         setCards(cardsData);
+        setCurrentIndex(0);
+        setShowAnswer(false);
       }

Also applies to: 35-37

🤖 Prompt for AI Agents
In frontend/src/pages/Practice.tsx around lines 31 to 37, when replacing the
card list (either by setting deck/cards for a specific deck or loading today's
reviews) the component reuses prior state so currentIndex and showAnswer can
point past the new list and cause cards[currentIndex] to be undefined; reset the
practice state whenever you set new cards (and when setting deck) by setting
currentIndex back to 0 (or -1 if you prefer starting state) and
setShowAnswer(false) so renders won't access out-of-range items; apply the same
reset in both branches (lines 31-37 and 35-37) wherever cards are replaced.

Comment on lines +215 to +223
<Label htmlFor="numCards">Number of Flashcards</Label>
<Input
id="numCards"
type="number"
min="1"
max="50"
value={numCards}
onChange={(e) => setNumCards(parseInt(e.target.value))}
/>

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

Guard numCards against NaN.

parseInt on the number input emits NaN whenever the field is cleared or an intermediate invalid value is typed. React then complains about receiving NaN for a controlled input, and the form submits "NaN" to the backend, which breaks validation for upload requests. Please sanitize the value before updating state (and clamp it to the allowed range) so we never store NaN.

Apply this diff to harden the handler:

-                  <Input
+                  <Input
                     id="numCards"
                     type="number"
                     min="1"
                     max="50"
                     value={numCards}
-                    onChange={(e) => setNumCards(parseInt(e.target.value))}
+                    onChange={(e) => {
+                      const value = Number.parseInt(e.target.value, 10);
+                      if (Number.isNaN(value)) {
+                        return;
+                      }
+                      setNumCards(Math.min(Math.max(value, 1), 50));
+                    }}
                   />
📝 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
<Label htmlFor="numCards">Number of Flashcards</Label>
<Input
id="numCards"
type="number"
min="1"
max="50"
value={numCards}
onChange={(e) => setNumCards(parseInt(e.target.value))}
/>
<Label htmlFor="numCards">Number of Flashcards</Label>
<Input
id="numCards"
type="number"
min="1"
max="50"
value={numCards}
onChange={(e) => {
const value = Number.parseInt(e.target.value, 10);
if (Number.isNaN(value)) {
return;
}
setNumCards(Math.min(Math.max(value, 1), 50));
}}
/>
🤖 Prompt for AI Agents
In frontend/src/pages/Upload.tsx around lines 215 to 223, the onChange handler
for numCards uses parseInt and can set state to NaN when the input is cleared or
temporarily invalid; change the handler to parse the value, test
Number.isNaN(parsed), and if NaN set a safe fallback (e.g., 1), otherwise clamp
the parsed value into the allowed range using Math.max(1, Math.min(50, parsed))
before calling setNumCards so the state never holds NaN and always respects the
1–50 bounds.

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