# Project export: EasyQuizzes

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: EasyQuizzes is a smart way to study for your test, Just upload your PDFs, Choose the topics you need help with and BOOM FlashCards for all your courses. It even has OCR (overkill).
- Devpost: https://devpost.com/software/easyquizzes
- GitHub: https://github.com/Atharva2099/EasyQuizzes
- Video: https://www.youtube.com/embed/QGkC77hOxns?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Atharva2099 (87 commits)

## Devpost submission (written by the team)

### Inspiration

Having used Quizzlet for most of my tests the hardest task for me is first reading through all my notes and then making every Flashcard myself (hoping I grasp the right idea and my answer on the other side is right). I used to waste lot of my time preparing to prepare for the test than actually studying. With all the new development in AI and with Open Source models catching up with the State-of-the-Art models. Its easier than ever to create study notes

### What it does

EasyQuizzes automates the creation of study flashcards from your notes. You upload a PDF, choose a topic, and set the number of flashcards you want. The app then creates knowledge chunks from these PDFs stores them to ChromaDB's vector database. When the user asks for N flashcards it pulls the K(preset value) most relevant chunks and the llama3.2 model uses them to generate your flashcards.

### How we built it

We used ChromaDB as our vector database for storing chunk embeddings. FastAPI was utilized for API handling and backend testing. Meta’s LLaMA 3.2-3B-preview powers the app, with deployment made easier through Groq. The backend is written in Python, while the frontend uses VanillaJS, HTML, and CSS

### Challenges we ran into

As a non CS grad, I had some programming background with LLMs and RAG , but deploying these projects was really hard the entire first day was spent on resolving issues with colliding packages in JS and Python. We almost entirely gave up on the tech stack.

### Accomplishments we're proud of

e're proud the app works as intended, especially under a tight 3-day deadline. The successful implementation of OCR was also a big win.

### What we learned

We learned the value of teamwork, leveraging each other's strengths and past experiences. Delegating tasks effectively based on expertise helped us push through challenges.

### What's next

Post-competition, we aim to integrate VLLMs like LLaMA 3.2-11B-vision or smaller models like LLAVA or Moondream to add features like converting videos and audios into flashcards. We also plan to incorporate a NoSQL database to store flashcards and eventually develop EasyQuizzes into a full-fledged app. The current implementation uses groq hosted models which lack the finetuning needed for such specific tasks. So, we hope to deploy our own LoRA models for better output or understanding OCR content better.

## README (from the GitHub repository)

# ByteBuilder Flashcard Generator - Cal Hacks 11.0

ByteBuilder presents a Flashcard Generator, developed for Cal Hacks 11.0. This web application allows users to upload PDF documents and generate AI-powered flashcards, creating an excellent tool for study and revision.

## Table of Contents
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Running the Application](#running-the-application)
- [Usage](#usage)
- [Working](#Working)
- [Project Structure](#project-structure)
- [Technologies Used](#technologies-used)
- [Troubleshooting](#troubleshooting)
- [Future Improvements](#future-improvements)


# Flashcard Generator

Flashcard Generator is a web application that allows users to upload PDF documents and generate flashcards based on the content. It uses AI to create multiple-choice questions and answers, making it an excellent tool for study and revision.

## Features

- PDF upload and processing
- AI-powered flashcard generation
- Multiple-choice question format
- Topic-specific flashcard creation
- Interactive flashcard interface

## Prerequisites

- Python 3.8 or higher
- Groq API key (sign up at https://www.groq.com)

## Installation

1. Clone the repository:
   
2. Create a virtual environment (for PiP):

         python -m venv venv
         source venv/bin/activate  # On Windows: venv\Scripts\activate

   
4. Install dependencies using PiP(requirements.txt):

        pip install -r requirements.txt

   
2. Install dependencies using Conda (environment.yml):
     
        conda env create -f environment.yml
        conda activate flashcard-env
   
4. Set up environment variables:

- Open `.env` and replace `your_api_key_here` with your actual Groq API key

## Running the Application

1. Start the FastAPI server:
   
         python -m uvicorn backend.app.main:app --reload

2. Open a web browser and navigate to `http://Localhost:8000` or whichever is provided by CLI

## Usage

1. Upload a PDF file using the "Upload PDF" button.
2. Enter a topic and the number of flashcards you want to generate.
3. Click "Generate Flashcards" to create your flashcards.
4. Navigate through the flashcards using the "Previous" and "Next" buttons.
5. Click on a flashcard to reveal the answer.

## Working


Crash course in Python Book Link:[ https://ehmatthes.github.io/pcc/](https://khwarizmi.org/wp-content/uploads/2021/04/Eric_Matthes_Python_Crash_Course_A_Hands.pdf) <br /><br />

[[<img width="856" alt="image" src="https://github.com/user-attachments/assets/1dff4ab9-1c72-453a-a162-aeb68c7ddedc">]](https://www.youtube.com/watch?v=QGkC77hOxns)



## Project Structure

This structure represents the main directories and files in the project:
- `backend/`: Contains the backend logic.
  - `app/`: Contains Python scripts for backend functionality.
    - `__init__.py`: Initializes the Python package.
    - `main.py`: The entry point of the backend application.
    - `models.py`: Defines database or data models.
    - `ocr.py`: Script for Optical Character Recognition (OCR).
    - `VectorDB.py`: Vector database management.
    - `llm.py`: Logic for Large Language Model (LLM) interactions.
- `frontend/`: Contains frontend files.
  - `index.html`: The main HTML file.
  - `styles.css`: The CSS file for styling.
  - `script.js`: The JavaScript file for frontend functionality.
- `.env.example`: Example of environment variables.
- `.gitignore`: Files and directories to be ignored by Git.
- `requirements.txt`: List of dependencies for the backend.
- `README.md`: Project documentation.


## Technologies Used

- Backend: FastAPI, ChromaDB, Groq API
- Frontend: HTML, CSS, JavaScript
- PDF Processing: PyPDF2
- Environment Management: python-dotenv

## Troubleshooting

If you encounter any issues:
- Ensure you're using Python 3.8 or higher
- Verify that all dependencies are correctly installed
- Check that your `.env` file contains the correct Groq API key
- Make sure you have an active internet connection for API calls

## Future Improvements

- Implement user accounts for saving and managing flashcard sets
- Add support for more file formats beyond PDF
- Enhance the AI model for even more accurate and diverse question generation
- Develop a mobile app version for on-the-go studying



## Detected evidence (automated analysis)

Indexed codebase: 12 recognized source files, 27 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (14 of 14)

```
.gitignore
backend/__init__.py
backend/app/__init__.py
backend/app/llm.py
backend/app/main.py
backend/app/models.py
backend/app/ocr.py
backend/app/VectorDB.py
environment.yml
frontend/index.html
frontend/script.js
frontend/styles.css
README.md
requirements.txt
```

### Dependencies

- requirements.txt: chromadb@==0.3.23, fastapi@==0.96.1, groq@==0.11.0, pydantic@==1.10.12, PyPDF2@==3.0.1, python-dotenv@==1.0.0, python-multipart@==0.0.5, uvicorn@==0.20.0

### Recent commits (newest first)

- Update README.md
- Rename .vercelignore to .gitignore
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update index.html
- Update main.py
- Update main.py
- Update main.py
- Delete backend/requirements.txt
- Create requirements.txt
- Update main.py
- Delete backend/app/vercel_app.py
- Delete vercel_requirements.txt
- Delete vercel.json
- Update requirements.txt
- Update vercel_app.py
- Create vercel_requirements.txt

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
fastapi==0.96.1
uvicorn==0.20.0
python-multipart==0.0.5
pydantic==1.10.12
chromadb==0.3.23
groq==0.11.0
python-dotenv==1.0.0
PyPDF2==3.0.1

```

### backend/app/main.py

```python
from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel
from typing import List, Optional
import os
import logging
import asyncio
import traceback

# Assuming these modules exist in your project
# If they don't, you'll need to implement or mock them
from .models import Flashcard
from .VectorDB import store_text, retrieve_diverse_contexts
from .llm import generate_qa_pair
from .ocr import extract_text_from_pdf

app = FastAPI()

# Enable CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Get the directory of the current file
current_dir = os.path.dirname(os.path.realpath(__file__))
# Go up two levels to reach the project root
project_root = os.path.dirname(os.path.dirname(current_dir))
# Path to the frontend directory
frontend_dir = os.path.join(project_root, "frontend")

# Mount the frontend directory
app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")

# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

# Progress tracking
progress = {}

@app.get("/")
async def read_root():
    return FileResponse(os.path.join(frontend_dir, "index.html"))

@app.post("/api/upload")
async def upload_file(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
    logger.info(f"Received file upload request: {file.filename}")
    try:
        # Instead of saving the file, let's just read its content
        content = await file.read()
        file_size = len(content)
        logger.info(f"File size: {file_size} bytes")

        if file_size > 4 * 1024 * 1024:  # 4MB limit
            raise ValueError("File size exceeds 4MB limit")

        # Generate a unique file ID without actually saving the file
        file_id = f"{file.filename}_{os.urandom(8).hex()}"
        progress[file_id] = 0

        # Add background task without actually processing the file for now
        background_tasks.add_task(dummy_process, file_id)
        logger.info(f"Background task added for file: {file_id}")
        
        response_data = {"message": "File upload started. Processing in background.", "file_id": file_id}
        logger.info(f"Sending response: {response_data}")
        return JSONResponse(content=response_data)
    except ValueError as ve:
        logger.error(f"Value error: {str(ve)}")
        return JSONResponse(status_code=400, content={"error": str(ve)})
    except Exception as e:
        logger.error(f"Error during file upload: {str(e)}")
        logger.error(traceback.format_exc())
        return JSONResponse(status_code=500, content={"error": f"An error occurred during file upload: {str(e)}"})

async def dummy_process(file_id: str):
    # Simulate processing without actually doing anything
    for i in range(10):
        progress[file_id] = i * 10
        await asyncio.sleep(1)
    progress[file_id] = 100

@app.get("/api/progress/{file_id}")
async def get_progress(file_id: str):
    if file_id not in progress:
        raise HTTPException(status_code=404, detail="File not found")
    return {"progress": progress[file_id]}

class FlashcardRequest(BaseModel):
    topic: str
    num_cards: int = 5
    page: Optional[int] = 1

@app.post("/api/generate_flashcards", response_model=dict)
async def generate_flashcards(request: FlashcardRequest):
    logger.debug(f"Received request: {request}")
    try:
        contexts = retrieve_diverse_contexts(request.topic, request.num_cards * 2)
        logger.debug(f"Retrieved {len(contexts)} contexts")
        
        if not contexts:
            logger.warning("No contexts found for the given topic")
            raise HTTPException(status_code=404, detail="No relevant content found for the given topic.")

        flashcards = []
        used_questions = set()
        cards_per_page = 10
        start_index = (request.page - 1) * cards_per_page
        end_index = start_index + cards_per_page

        for context in contexts[start_index:end_index]:
            qa_pair = generate_qa_pair(context)
            logger.debug(f"Generated QA pair: {qa_pair}")
            if qa_pair is not None:
                question, answer = qa_pair
                if question and answer and question not in used_questions:
                    flashcards.append(Flashcard(question=question, answer=answer))
                    used_questions.add(question)
            
            if len(flashcards) >= request.num_cards:
                break

        if not flashcards:
            logger.warning("Failed to generate any flashcards")
            raise HTTPException(status_code=500, detail="Failed to generate any flashcards. The AI model might be having difficulties. Please try again with a different topic or upload more diverse content.")

        total_pages = -(-len(contexts) // cards_per_page)  # Ceiling division
        logger.info(f"Generated {len(flashcards)} flashcards")
        return {
            "flashcards": flashcards,
            "current_page": request.page,
            "total_pages": total_pages
        }

    except Exception as e:
        logger.error(f"An error occurred: {str(e)}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"An error occurred while generating flashcards: {str(e)}")

@app.get("/api/test")
async def test_endpoint():
    return {"message": "API is working"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 8000)))

```

### environment.yml

```yaml
name: flashcard-env
channels:
  - conda-forge
  - defaults
dependencies:
  - python=3.10
  - fastapi=0.96.1
  - uvicorn=0.20.0
  - python-multipart=0.0.5
  - pydantic=1.10.12
  - python-dotenv=1.0.0
  - pip
  - pip:
    - chromadb==0.3.23
    - groq==0.11.0
    - PyPDF2==3.0.1
```

### frontend/index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flashcard Generator</title>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="/styles.css">
</head>
<body>
    <div class="container">
        <h1>Flashcard Generator</h1>
        <div id="upload-section">
            <div class="file-upload-container">
                <input type="file" id="file-upload" accept=".pdf" style="display: none;">
                <label for="file-upload" class="file-upload-label">Choose PDF</label>
                <div id="file-name"></div>
            </div>
            <button id="upload-btn">Upload PDF</button>
        </div>
        <div id="progress-bar" class="progress" style="display: none;">
            <div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
        </div>
        <div id="generate-section" style="display: none;">
            <form id="flashcard-form">
                <input type="text" id="topics" placeholder="Enter topic" required>
                <input type="number" id="num-cards" placeholder="Number of cards" min="1" required>
                <button type="submit">Generate Flashcards</button>
            </form>
        </div>
        <div id="flashcards-container" style="display: none;">
            <div id="flashcard-display"></div>
            <div id="navigation">
                <button id="prev-btn">Previous</button>
                <span id="card-index"></span>
                <button id="next-btn">Next</button>
            </div>
        </div>
    </div>
    <script src="/script.js"></script>
</body>
</html>

```

### frontend/styles.css

```css
body {
    font-family: 'Roboto', sans-serif;
    line-height: 1.6;
    margin: 0;
    padding: 20px;
    background-color: #121212;
    color: #e0e0e0;
}

.container {
    max-width: 800px;
    margin: auto;
    padding: 20px;
    background-color: #1e1e1e;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(255,255,255,0.1);
}

h1 {
    text-align: center;
    color: #bb86fc;
}

button, .file-upload-label {
    background-color: #bb86fc;
    color: #121212;
    padding: 10px 15px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s ease;
}

button:hover, .file-upload-label:hover {
    background-color: #9a67ea;
}

input[type="file"] {
    display: none;
}

.file-upload-label {
    display: inline-block;
    margin-right: 10px;
}

input[type="text"], input[type="number"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #bb86fc;
    border-radius: 5px;
    background-color: #2d2d2d;
    color: #e0e0e0;
}

#upload-section {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 20px;
}

.file-upload-container {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}

#file-name {
    margin-top: 5px;
    font-size: 0.9em;
    color: #bb86fc;
}

#flashcards-container {
    margin-top: 20px;
}

.flashcard {
    background-color: transparent;
    width: 100%;
    height: 400px;
    perspective: 1000px;
    margin-bottom: 20px;
}

.flashcard-inner {
    position: relative;
    width: 100%;
    height: 100%;
    text-align: center;
    transition: transform 0.6s;
    transform-style: preserve-3d;
}

.flashcard.flipped .flashcard-inner {
    transform: rotateY(180deg);
}

.flashcard-front, .flashcard-back {
    position: absolute;
    width: 100%;
    height: 100%;
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    background-color: #2d2d2d;
    border-radius: 10px;
    padding: 20px;
    box-sizing: border-box;
    overflow-y: auto;
    color: #ffffff;
}

.flashcard-back {
    transform: rotateY(180deg);
    background-color: #3d3d3d;
}

.flashcard h3 {
    margin-bottom: 15px;
    width: 100%;
    text-align: center;
}

.flashcard p {
    margin-bottom: 10px;
    width: 100%;
    text-align: left;
}

#navigation {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 20px;
}

#card-index {
    font-size: 18px;
    font-weight: bold;
    color: #bb86fc;
}

.progress {
    height: 20px;
    background-color: #2d2d2d;
    border-radius: 10px;
    margin: 20px 0;
    overflow: hidden;
}

.progress-bar {
    height: 100%;
    background-color: #bb86fc;
    border-radius: 10px;
    transition: width 0.5s ease;
    text-align: center;
    line-height: 20px;
    color: #121212;
}

#generate-section, #flashcards-container {
    display: none;
}
```

### frontend/script.js

```javascript
let allFlashcards = [];
let currentCardIndex = 0;

document.addEventListener('DOMContentLoaded', () => {
    const uploadBtn = document.getElementById('upload-btn');
    const flashcardForm = document.getElementById('flashcard-form');
    const prevBtn = document.getElementById('prev-btn');
    const nextBtn = document.getElementById('next-btn');
    const fileUpload = document.getElementById('file-upload');
    
    uploadBtn.addEventListener('click', uploadFile);
    flashcardForm.addEventListener('submit', handleGenerateFlashcards);
    prevBtn.addEventListener('click', showPreviousCard);
    nextBtn.addEventListener('click', showNextCard);
    fileUpload.addEventListener('change', updateFileName);
});

function updateFileName() {
    const fileUpload = document.getElementById('file-upload');
    const fileNameDisplay = document.getElementById('file-name');
    if (fileUpload.files.length > 0) {
        fileNameDisplay.textContent = `Selected file: ${fileUpload.files[0].name}`;
    } else {
        fileNameDisplay.textContent = '';
    }
}

async function uploadFile() {
    console.log('Upload function called');
    const fileUpload = document.getElementById('file-upload');
    const file = fileUpload.files[0];
    if (!file) {
        console.log('No file selected');
        alert('Please select a file first.');
        return;
    }

    console.log('File selected:', file.name);

    const formData = new FormData();
    formData.append('file', file);

    try {
        console.log('Sending request to /api/upload');
        const progressBar = document.querySelector('.progress-bar');
        document.querySelector('.progress').style.display = 'block';
        
        const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData
        });
        console.log('Response status:', response.status);
        console.log('Response headers:', response.headers);
        
        const responseText = await response.text();
        console.log('Raw response:', responseText);
        
        let data;
        try {
            data = JSON.parse(responseText);
        } catch (parseError) {
            console.error('Error parsing JSON:', parseError);
            throw new Error(`Invalid JSON response from server: ${responseText}`);
        }
        
        console.log('Parsed response data:', data);
        
        if (response.ok) {
            if (data.message && data.message.includes("File upload started")) {
                console.log('File upload successful');
                alert('File uploaded successfully and processing started!');
                document.getElementById('generate-section').style.display = 'block';
                checkProgress(data.file_id);
            } else {
                throw new Error(data.error || 'Unknown server response');
            }
        } else {
            throw new Error(data.error || `HTTP error! status: ${response.status}`);
        }
    } catch (error) {
        console.error('Error during file upload:', error);
        alert('An error occurred while uploading the file: ' + error.message);
    } finally {
        document.querySelector('.progress').style.display = 'none';
    }
}

// Add a test function to check if the API is responding
async function testAPI() {
    try {
        const response = await fetch('/api/test');
        const data = await response.json();
        console.log('API test response:', data);
    } catch (error) {
        console.error('API test error:', error);
    }
}

// Call testAPI when the page loads
document.addEventListener('DOMContentLoaded', testAPI);

async function checkProgress(fileId) {
    const progressBar = document.querySelector('.progress-bar');
    document.querySelector('.progress').style.display = 'block';

    try {
        while (true) {
            const response = await fetch(`/api/progress/${fileId}`);
            const data = await response.json();
            console.log('Progress:', data.progress);
            
            progressBar.style.width = `${data.progress}%`;
            progressBar.textContent = `${Math.round(data.progress)}%`;
            
            if (data.progress >= 100) {
                setTimeout(() => {
                    document.querySelector('.progress').style.display = 'none';
                }, 1000);
                break;
            }
            
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    } catch (error) {
        console.error('Error checking progress:', error);
        document.querySelector('.progress').style.display = 'none';
    }
}

async function handleGenerateFlashcards(event) {
    event.preventDefault();
    
    const topic = document.getElementById('topics').value.trim();
    const numCards = parseInt(document.getElementById('num-cards').value);
    
    if (!topic || isNaN(numCards) || numCards <= 0) {
        alert('Please enter a valid topic and number of cards.');
        return;
    }
    
    try {
        allFlashcards = await generateFlashcards(topic, numCards);
        
        if (allFlashcards.length > 0) {
            currentCardIndex = 0;
            displayFlashcard();
            document.getElementById('flashcards-container').style.display = 'block';
        } else {
            alert('No flashcards were generated. Please try again.');
        }
    } catch (error) {
        console.error('Error generating flashcards:', error);
        alert('An error occurred while generating flashcards. Please try again.');
    }
}

async function generateFlashcards(topic, numCards) {
    console.log(`Generating ${numCards} flashcards for topic: ${topic}`);
    const response = await fetch('/api/generate_flashcards', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ topic, num_cards: numCards })
    });
    
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${r
[truncated — 1724 more characters]
```

### backend/app/models.py

```python
# backend/app/models.py
from pydantic import BaseModel

class Flashcard(BaseModel):
    question: str
    answer: str
```

### backend/app/ocr.py

```python
from PyPDF2 import PdfReader
import logging

logger = logging.getLogger(__name__)

def extract_text_from_pdf(file_path: str, chunk_size: int = 5) -> list[str]:
    chunks = []
    try:
        with open(file_path, 'rb') as file:
            reader = PdfReader(file)
            total_pages = len(reader.pages)
            
            for i in range(0, total_pages, chunk_size):
                chunk = ""
                for j in range(i, min(i + chunk_size, total_pages)):
                    chunk += reader.pages[j].extract_text()
                chunks.append(chunk)
        
        return chunks
    except Exception as e:
        logger.error(f"Error extracting text from PDF: {str(e)}", exc_info=True)
        return []
```

### backend/app/VectorDB.py

```python
# backend/app/vector_db.py
import chromadb
from chromadb.config import Settings
import uuid
import logging
from chromadb.errors import NotEnoughElementsException

logger = logging.getLogger(__name__)

# Use a persistent storage for ChromaDB
client = chromadb.Client(Settings(
    chroma_db_impl="duckdb+parquet",
    persist_directory="./chroma_db"
))

# Create the collection if it doesn't exist
collection = client.get_or_create_collection("flashcards")

def store_text(text: str, metadata: dict = None):
    try:
        collection.add(
            documents=[text],
            metadatas=[metadata] if metadata else None,
            ids=[str(uuid.uuid4())]
        )
        client.persist()
    except Exception as e:
        logger.error(f"Error storing text in VectorDB: {str(e)}", exc_info=True)

def retrieve_context(query: str, n_results: int = 1):
    try:
        results = collection.query(
            query_texts=[query],
            n_results=n_results
        )
        return results['documents'][0] if results['documents'] else []
    except NotEnoughElementsException:
        logger.warning(f"Not enough elements for query: {query}. Returning all available.")
        return collection.get()['documents']

def retrieve_diverse_contexts(topic: str, n_contexts: int = 5):
    logger.debug(f"Retrieving contexts for topic: {topic}, n_contexts: {n_contexts}")
    try:
        results = collection.query(
            query_texts=[topic],
            n_results=n_contexts
        )
        contexts = results['documents'][0] if results['documents'] else []
    except NotEnoughElementsException:
        logger.warning(f"Not enough contexts available. Retrieving all available contexts.")
        contexts = collection.get()['documents']
    
    logger.debug(f"Retrieved {len(contexts)} contexts")
    return contexts

def get_total_documents():
    return collection.count()
```

### backend/app/llm.py

```python
from groq import Groq
import os
from dotenv import load_dotenv
import logging

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Load environment variables
load_dotenv()

# Initialize Groq client
client = Groq(api_key=os.getenv("GROQ_API_KEY"))

def generate_qa_pair(context: str):
    prompt = f"""Given the following context, generate a unique and specific multiple-choice question-answer pair suitable for a flashcard. The question should test understanding of key concepts.

Context: {context}

Generate a question-answer pair in the following format:
Question: [Your specific, unique multiple-choice question with 4 options labeled A, B, C, and D]
A) [Option A]
B) [Option B]
C) [Option C]
D) [Option D]
Answer: [The correct option letter followed by a brief explanation]

Ensure the question is not generic and is specifically related to the given context."""

    try:
        chat_completion = client.chat.completions.create(
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                }
            ],
            model="llama-3.2-3b-preview",
            max_tokens=1000,  # Increased to accommodate longer response
            temperature=0.7,
        )

        response = chat_completion.choices[0].message.content
        logger.info(f"Generated response: {response}")

        # Split the response into question and answer
        parts = response.split("Answer:")
        if len(parts) != 2:
            logger.error("Response format is incorrect")
            return None

        question_part = parts[0].strip()
        answer_part = parts[1].strip()

        # Further process the question to include options
        question_lines = question_part.split("\n")
        question = question_lines[0].replace("Question:", "").strip()
        options = "\n".join(question_lines[1:])

        return f"{question}\n{options}", answer_part

    except Exception as e:
        logger.error(f"Error generating QA pair: {e}")
        return None
```