An intelligent academic assistant that lets students upload course materials and interact with them through Q&A, auto-generated quizzes, and chapter summaries — all powered by Retrieval-Augmented Generation (RAG).
- Overview
- Features
- Tech Stack & Tool Choices
- Project Structure
- How It Works — RAG Pipeline
- API Endpoints
- Setup & Installation
- Environment Variables
- Running the App
JustClick AI is a full-stack study tool built for university students. Students upload their lecture slides, PDFs, or documents organised by semester and subject. They can then:
- Ask questions and get answers grounded strictly in their own uploaded content
- Generate 10-question multiple-choice quizzes from any chapter or the entire subject
- Get structured chapter summaries with key concepts and takeaways
- Chat naturally — the assistant remembers recent conversation history
The system never uses outside knowledge. Every answer, quiz, and summary is produced only from what the student uploaded.
| Feature | Description |
|---|---|
| 📁 File Upload | Supports PDF, PPTX, DOCX, and plain TXT files |
| 🔍 Semantic Q&A | Vector similarity search finds the most relevant chunks before answering |
| 📝 Quiz Generation | Auto-generates 10 MCQs with a separated answer key (---ANSWER KEY---) |
| 📄 Chapter Summary | Structured summaries with overview, key concepts, and takeaways |
| 🗂️ Semester & Subject Scoping | Answers are strictly scoped to the selected semester and subject |
| 💬 Chat History | Last 6 messages are included for follow-up question context |
| 🔄 Cross-Subject Hints | If a topic isn't in the current subject, the bot hints which subject covers it |
| ♻️ Re-index Utility | Admin tool to re-extract and re-embed files after extraction logic updates |
Why Flask over Django / FastAPI? Flask was chosen for its minimal footprint and flexibility. The project doesn't need Django's admin panel or ORM migrations by default, and Flask integrates more cleanly with SQLAlchemy as a standalone extension. FastAPI would have been a valid choice for async performance, but since DeepSeek calls are synchronous and the workload is not high-concurrency, Flask's simplicity won out.
Why SQLAlchemy?
Used to track uploaded materials (Material table) and chat messages (ChatMessage table). SQLAlchemy gives clean ORM models without raw SQL. SQLite is used in development because there is no need for a separate database server — it lives in a single file. In production this can be swapped for PostgreSQL by changing one DATABASE_URL environment variable.
Why ChromaDB over Pinecone / Weaviate / pgvector?
ChromaDB runs entirely locally with zero infrastructure — no API key, no cloud account, no Docker container required. It persists vector data to a local chroma_db/ folder automatically. For a university project where the priority is simplicity and offline capability, ChromaDB is the best fit. Pinecone would be better for large-scale production with millions of vectors, but introduces cost and network dependency. pgvector would require a PostgreSQL setup. ChromaDB hits the sweet spot.
Why this embedder over OpenAI embeddings?
sentence-transformers runs 100% locally and for free. OpenAI's text-embedding-ada-002 would cost money per token and require a network call for every chunk at index time and every query at search time. For a student tool indexing hundreds of lecture slides, local embeddings are far more practical. all-MiniLM-L6-v2 is small (80MB), fast, and well-tested for semantic similarity on English academic text.
Why DeepSeek over GPT-4 / Claude / Gemini?
DeepSeek offers GPT-4-class reasoning at a fraction of the cost — often 10–20x cheaper per million tokens. For a study tool making many LLM calls (one per question, one per quiz, one per summary), cost matters. The OpenAI-compatible API means the integration is identical to using OpenAI — just a different base_url and api_key. Switching to GPT-4 or Claude requires changing only two lines in rag.py.
Why pypdf over pdfminer / pdfplumber?
pypdf is lightweight, actively maintained, and handles the standard PDF text extraction use case well. pdfplumber is more powerful for table extraction, but adds unnecessary complexity. pdfminer is lower-level and requires more boilerplate. For lecture slides and notes, pypdf's page.extract_text() is sufficient.
Why python-pptx?
It is the only mature, maintained library for reading .pptx files in Python. The project extracts text shape-by-shape and paragraph-by-paragraph, preserving bullet point hierarchy — a deliberate choice to maintain slide structure in the vector index.
Why python-docx?
Same rationale — the de facto standard for .docx reading in Python. Paragraphs are extracted in order and joined with newlines.
Why Next.js over plain React / Vite?
Next.js gives file-based routing, server components, and a built-in API proxy layer out of the box. The App Router (app/ directory) provides layouts that wrap every page, making it trivial to apply a consistent shell (sidebar + main panel) across all routes. Plain React with Vite would require manually setting up routing (React Router), SSR, and build optimisation.
Why Tailwind over CSS Modules / styled-components? Tailwind's utility classes mean zero context-switching between JS and CSS files. Chat bubble colours, sidebar widths, and panel layouts are all defined inline. For a project where one developer is building both logic and UI, Tailwind dramatically speeds up iteration.
| Component | Role |
|---|---|
ChatInput.js |
Text box + Send button; fires POST /api/chat on submit |
MainPanel.js |
Centre panel; holds MessageList and ChatInput |
MessageList.js |
Scrollable list of user/AI bubbles; renders quiz cards interactively |
Sidebar.js |
Left sidebar; semester picker, subject tabs, file upload |
project-root/
│
├── backend/ # Flask application
│ ├── app.py # App factory: registers blueprints, creates DB tables
│ ├── config.py # Env vars: DEEPSEEK_API_KEY, TOP_K, DB path, etc.
│ ├── extensions.py # Shared singletons: ChromaDB client, sentence embedder
│ ├── models.py # SQLAlchemy models: Material, ChatMessage
│ ├── routes.py # All API endpoints
│ ├── rag.py # RAG core: indexing, Q&A, quiz, summary logic
│ ├── manage_db.py # CLI helpers: init DB, drop tables, seed data
│ ├── seed.py # Optional: pre-load sample materials for testing
│ ├── requirements.txt # Python dependencies
│ ├── .env # Secret keys (never commit this file)
│ └── chroma_db/ # ChromaDB vector store data (auto-created on first run)
│
└── app/ # Next.js frontend (App Router)
├── globals.css # Global styles, Tailwind base, chat bubble colours
├── layout.js # Root layout: wraps every page, sets <html> meta tags
├── page.js # Home page: renders <MainPanel> inside the app shell
│
└── components/
├── submit/
│ ├── ChatInput.js # Text box + Send button; fires POST /api/chat on submit
│ └── MainPanel.js # Centre panel: holds <MessageList> + <ChatInput>
├── cards/
│ └── MessageList.js # Scrollable list of user/AI bubbles; renders quiz cards
└── Sidebar.js # Left sidebar: semester picker, subject tabs, file upload
Upload file
↓
Compute SHA-256 hash → skip if already indexed
↓
Extract text (PDF / PPTX / DOCX / TXT)
↓
Split into overlapping word chunks (400 words, 80-word overlap)
↓
Filter out junk chunks (raw XML, ZIP manifests)
↓
Embed each chunk with sentence-transformers (all-MiniLM-L6-v2)
↓
Store in ChromaDB with metadata: { semester, subject, source, chapter, chunk_index }
↓
Save Material record to SQLite
Chapter detection happens automatically:
- First checks if the first 3 lines of a chunk contain
Chapter N/Section N - Falls back to the filename (e.g.
chapter_2.pptx→Chapter 2) - Falls back to
"General Content"if neither matches
Junk filtering removes chunks that are raw XML internal to PPTX/DOCX zip containers, detected by looking for patterns like <a:t>, .rels, ppt/slides/slide1.xml, etc.
Every incoming question passes through three regex checks before the vector search runs:
Question received
↓
_RE_QUIZ matched? → _build_quiz()
↓ no
_RE_SUMMARY matched? → _build_summary()
↓ no
Normal RAG Q&A
Quiz triggers (case-insensitive): quiz, quizze, make quiz, create quiz, test me, quiz me, mcq, practice test, generate questions, questions and answers, make questions and answers, q&a, and common misspellings.
Summary triggers: summary, summarize, overview, recap, brief, explain chapter, chapter summary.
Chapter detection (_detect_chapter) extracts chapter N / section N / ch. N from the question using a regex that also handles word-form numbers (one, two … ten).
Level 1: Search current subject + semester
→ found relevant chunks? → answer with DeepSeek
↓ not found
Level 2: Search all subjects in current semester
→ found? → tell user which subject covers this topic
↓ not found
Level 3: Search all semesters
→ found? → tell user which semester/subject covers this
↓ not found
Level 4: "I don't have any information about this topic."
Relevance is determined by cosine distance threshold = 1.3 (0 = identical vectors, 2 = opposite). Chunks with distance ≥ 1.3 are discarded as irrelevant.
Chapter specified in question?
├── YES → fetch chunks for that chapter only
└── NO → fetch chunks from the entire subject (all chapters combined)
Build context from up to 15 chunks
↓
Send to DeepSeek with strict format prompt
↓
Returns: 10 MCQs + ---ANSWER KEY--- separator + Q1: B / Q2: A ... format
↓
Frontend splits on ---ANSWER KEY--- to render interactive quiz cards
Chapter specified?
├── YES → fetch that chapter's chunks
└── NO → ask user to pick a chapter (shows available list)
Build context from up to 15 chunks
↓
Send to DeepSeek with structured summary prompt
↓
Returns: ## Overview / ## Key Concepts / ## Key Takeaways
The last 6 messages from the ChatMessage table (filtered by session_id) are appended to every Q&A prompt as CHAT HISTORY. This lets users ask follow-up questions like "explain the third point more" without repeating context. Quiz and summary requests do not include chat history — they use only the document content.
All endpoints are defined in routes.py.
| Method | Path | Description |
|---|---|---|
POST |
/api/chat |
Main chat endpoint. Body: { session_id, semester, subject, message } |
POST |
/api/upload |
Upload a file. Form data: file, semester, subject |
GET |
/api/materials |
List all indexed materials. Query: ?semester=&subject= |
DELETE |
/api/materials/<id> |
Delete a material record and its ChromaDB chunks |
GET |
/api/subjects |
List subjects for a given semester. Query: ?semester= |
- Python 3.11+
- Node.js 18+
- A DeepSeek API key → platform.deepseek.com
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Initialise the database
python manage_db.py init
# Start the Flask server
flask run --port 5000cd app
npm install
npm run devOpen http://localhost:3000.
Create a .env file inside backend/:
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx
TOP_K=20
DATABASE_URL=sqlite:///justclick.db
CHROMA_PATH=./chroma_db
SECRET_KEY=your-flask-secret-key| Variable | Description | Default |
|---|---|---|
DEEPSEEK_API_KEY |
DeepSeek API key (required) | — |
TOP_K |
Number of chunks retrieved per vector query | 20 |
DATABASE_URL |
SQLAlchemy DB URL | sqlite:///justclick.db |
CHROMA_PATH |
Path for ChromaDB persistence | ./chroma_db |
SECRET_KEY |
Flask session secret | — |
# Terminal 1 — Backend
cd backend && flask run --port 5000
# Terminal 2 — Frontend
cd app && npm run devThen open http://localhost:3000, pick a semester, select a subject, upload your lecture slides, and start chatting.
If you update the text extraction logic and need to re-embed existing files:
# In Flask shell: flask shell
from rag import reindex_subject
result = reindex_subject("sem 1", "Data Structures")
print(result)
# → { "reindexed": ["chapter1.pptx", ...], "skipped": [], "deleted_chunks": 142 }This deletes all ChromaDB vectors for the subject and re-extracts/re-embeds from the original files recorded in the SQL database.
| Decision | Choice | Why |
|---|---|---|
| Vector DB | ChromaDB | Zero infrastructure, local, free |
| Embedder | all-MiniLM-L6-v2 | Local, free, fast, good quality |
| LLM | DeepSeek | GPT-4 quality at 10–20x lower cost |
| LLM SDK | OpenAI Python SDK | DeepSeek is OpenAI-API-compatible |
| Backend | Flask | Lightweight, flexible, easy SQLAlchemy integration |
| ORM | SQLAlchemy | Clean models, easy to swap DB backend |
| Frontend | Next.js App Router | Built-in routing, layouts, SSR |
| Styling | Tailwind CSS | Fast utility-first development |
| PDF reading | pypdf | Lightweight, sufficient for lecture slides |
| PPTX reading | python-pptx | Only mature option; shape-level extraction |
| DOCX reading | python-docx | Industry standard |