# Project export: Prereq

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: TreeHacks 2026
- Tagline: Real-time knowledge graphs that make every lecture impossible to fall behind in
- Devpost: https://devpost.com/software/prereq-sg1thw
- GitHub: https://github.com/jasonyi33/prereq
- Demo: https://prereq-frontend.onrender.com/
- Video: https://www.youtube.com/embed/Xaod8It2Wko?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Zoom] Education Track)
- Team: 7 GitHub contributor(s) — shay (70 commits), Claude Opus 4.6 (66 commits), Jason Yi (46 commits), Cynthia (41 commits), Cursor (12 commits), kayceenguyenn (5 commits), kayceenguyenn (4 commits)

## Devpost submission (written by the team)

### Overview

Problem &

### Inspiration

Education is one of the most powerful drivers of opportunity, yet our classrooms still rely on guesswork to measure understanding. Professors introduce new concepts without knowing who's following along, and students often don't realize they're lost until the exam. Our team has experienced this firsthand and found that these issues are only exacerbated in online settings. Part of the problem is how most classroom tools augment learning. Stanford’s machine learning course (CS229) includes a 200-page reader filled with dense, interconnected concepts. While studying for the midterm, Cynthia manually mapped out all the major topics and their prerequisites to understand how everything fit together. This visual reinforced that learning is cumulative and interconnected. You can’t understand backpropagation without the chain rule, or speak Spanish without mastering conjugations. As a result, we leverage dynamic visual aids that provide professors with real-time feedback that twelve students understand the chain rule while eight are lost on gradient descent. This way, they can adapt their instruction accordingly and give those eight students targeted help. What It Does Prereq is a live, interactive learning copilot that turns lectures into personalized, ever-evolving knowledge graphs. Each student's graph updates in real time, showing what they've mastered, what they're struggling with, and an actionable plan to close relevant gaps. Professors are armed with a live heatmap of class-wide learning progress, pinpointing exactly which concepts and prerequisites to reinforce understanding. For students: As your professor speaks, Prereq transcribes the lecture and automatically identifies concepts being discussed (e.g., "Backpropagation"), lighting them up on your personal graph. In-class Prereq quizzes, exam performance, and conversations with Aaron, our AI tutor, all feed into your graph, so it updates continuously. After class, you can keep collaborating with Aaron, who is personalized to your weak graph nodes. Our study group feature also helps foster community! We match students who opt in based on complementarity: your weak spots are their strengths, and vice versa. One click gives them both a Zoom link at a mutually agreed-upon time and a side‑by‑side view of who can teach what, turning the class into a learning network. For professors: Instead of generic iClicker/PollEV questions, we help professors generate polls from what was just said and aim them at concepts the class is struggling with. They can see each student’s knowledge graph and sort a class mastery heatmap by the most difficult concepts. During the lecture, we also surface live reinforcement suggestions (what to re-explain, which examples to add) to help them adjust in the moment. How We Built It At the start of the semester, a professor uploads their course material as a PDF. Eliminating manual parsing, Claude Sonnet extracts every concept and its prerequisites into a knowledge graph: 35+ concepts from a 200-page textbook in seconds. Each student gets their own copy, and two students in the same lecture will have completely different graphs by Week 2. During a lecture, Zoom's RTMS SDK captures live audio and transcription with precision. Getting that integration right is one aspect we’re extremely proud of. The RTMS SDK helped us handle webhook validation, OAuth, per-teacher credentials, and joining the RTMS stream reliably (more in Challenges). Everything spoken is run through concept detection (Claude Haiku), stored in Supabase via our Flask API, and pushed to student knowledge graphs and the professor heatmap in real time over Socket.IO. We also update graphs based on in-class poll performances. Our study-group feature pairs students by opposite strengths and weaknesses and sends out a Zoom link based on their schedules so they can start a peer session in one click. After the lecture, Claude Sonnet powers Aaron, our Socratic tutoring agent, that adapts to each student's conversation, and we use Perplexity’s Sonar API to surface targeted learning resources (articles, videos, exercises) that complement the course material. Our frontend is built with Next.js, React, Tailwind CSS, and react-force-graph-2d for the interactive knowledge graph. The backend runs Express with a custom Socket.IO server alongside Flask for graph CRUD and mastery logic, all backed by Supabase (PostgreSQL). The app is fully deployed on Render. Render gave us a production-ready API with scaling and load balancing in five minutes with no DevOps overhead. Challenges & Accomplishments Building Prereq was an ambitious goal, and implementing RMTS was technically complex. Beyond passing Zoom’s webhook validation (HMAC-signed challenge) and implementing OAuth for the right streams, we spent hours debugging why RMTS did not work across multiple devices for our Render deployment. This required us to methodically build and test endpoints to inspect active lecture streams. Keeping the live Prereq experience fast and accurate was another focus. To reduce latency for API calls, we built out caching for our AI agents and have a Redis layer. Our backend fully works and is optimized for scalability. Creating a comprehensive knowledge graph from 200+ page course documents was also a challenge. To support more users, we designed a pipeline to extract concepts and prerequisites at scale reliably. Finally, we are proud of how intuitive our UI/UX is to those of all learning styles. We help facilitate social connections through our study group matching platform, encouraging collaboration between students of varying levels. Distinguishing between passive vs. active mastery was an interesting design challenge. What We Learned On the technical side, we applied our knowledge of networking and webhooks to debug APIs. AI tutoring is super popular in EdTech, but we learned that a general, abstract agent was not as useful as an agent recognizing a student in context to their academic history. On the product side, we learned that personalization and context are everything. AI tutoring only works when it understands timestamps the student didn't understand, past mistakes, and missing prerequisites. Also, even a 500ms delay in poll feedback broke the live experience, so we added caching layers, used time-sensitive calls carefully, and designed around a Socket.IO push instead of polling to keep interactions instant. What’s Next We plan on integrating directly with the tools students already live in, such as Canvas, Blackboard, Edpuzzle, and Kahoot. Potential sources like homework accuracy, reading completion, quiz attempts, lecture polls, exam performance could all flow into one shared knowledge graph within our system. Long term, concepts mastered in one class should strengthen performance in the next. Over time, the system becomes a living map of a student’s strengths, weaknesses, and growth.

## README (from the GitHub repository)

# Prereq

**Your personal knowledge graph in every lecture.**

A live learning copilot that turns lectures into personalized knowledge graphs. Students see their understanding update in real time as concepts are discussed and polls are answered; professors get a heatmap of class mastery and polls tied to the lecture.

## Features

- **PDF → knowledge graph** — Upload course material; Claude extracts concepts and prerequisites (e.g. 35+ nodes from a 200-page textbook). Each student has their own graph; mastery is a confidence score (0–1) driven by polls, tutoring, and attendance.
- **Live concept detection** — Zoom RTMS captures lecture audio; transcripts are run through concept detection (Claude Haiku) and pushed to student graphs and professor heatmap in real time via Socket.IO.
- **Contextual polling** — AI generates poll questions from what was just said and targets concepts the class is struggling with. Responses update mastery and graph colors (red → yellow → green) instantly.
- **Aaron (AI tutor)** — Post-lecture Socratic tutor (Claude Sonnet) personalized to each student’s weak nodes; Perplexity Sonar surfaces learning resources (articles, videos) for specific gaps.
- **Study groups** — Match students by complementary strengths/weaknesses; one-click Zoom link and view of who can teach what.
- **Professor dashboard** — Live heatmap by concept, per-student graphs, and in-lecture reinforcement suggestions (what to re-explain, which examples to add).

## Tech stack

| Component | Technology |
|-----------|------------|
| Frontend | Next.js, React, Tailwind CSS, react-force-graph-2d, Socket.IO |
| Backend | Flask (Python), Supabase (PostgreSQL) |
| AI | Claude Sonnet 4.5 (extraction, questions, tutoring), Claude Haiku 4.5 (detection, grading) |
| Live | Zoom RTMS (audio/transcription), Perplexity Sonar (resources) |
| Deploy | Render (frontend + API + Redis) |

## Prerequisites

- Python 3.10+
- Node.js 18+
- [Supabase](https://supabase.com) project
- API keys: Anthropic (Claude), Perplexity; optionally Zoom, Deepgram

## Installation

```bash
git clone https://github.com/jasonyi33/prereq.git
cd prereq
cp .env.example .env
```

Edit `.env` with `SUPABASE_URL`, `SUPABASE_KEY`, `ANTHROPIC_API_KEY`, `PERPLEXITY_API_KEY`, and `FLASK_API_URL` (your Flask API base URL). Zoom keys for live RTMS with their SDK.

Create the database tables in Supabase (SQL Editor).

## Running locally

**Terminal 1 — Flask API**

```bash
cd api
pip install -r requirements.txt
python main.py
```

Then seed demo data (CS229-style course, ~35 concepts, 4 students):

```bash
python scripts/seed_demo.py
```

**Terminal 2 — Next.js frontend**

```bash
cd frontend
npm install
npm run dev
```

## Project structure

```
prereq/
├── api/                 # Flask backend
│   ├── main.py
│   ├── requirements.txt
│   └── ...
├── frontend/            # Next.js + Socket.IO
│   ├── server/          # Express + Socket.IO
│   ├── src/app/         # Pages and API routes
│   ├── src/components/
│   └── ...
├── scripts/             # seed_demo.py, etc.
├── CLAUDE.md            # Dev guide and full schema
├── render.yaml          # Render config
└── .env.example
```

## License

Made at TreeHacks 2026! All rights reserved.


## Detected evidence (automated analysis)

Indexed codebase: 122 recognized source files, 799 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Flask (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 139)

```
.claude/settings.local.json
.env.example
.github/workflows/deploy-api.yml
.gitignore
api/__init__.py
api/.claude/settings.local.json
api/API_DOCUMENTATION.md
api/app.py
api/Dockerfile
api/requirements.txt
api/result.txt
api/scripts/clear_sam.py
api/scripts/seed_study_pool.py
api/src/__init__.py
api/src/cache.py
api/src/db.py
api/src/middleware/__init__.py
api/src/middleware/auth.py
api/src/routes/auth.py
api/src/routes/concepts.py
api/src/routes/courses.py
api/src/routes/create.py
api/src/routes/graph.py
api/src/routes/heatmap.py
api/src/routes/lectures.py
api/src/routes/pages.py
api/src/routes/polls.py
api/src/routes/students.py
api/src/routes/study_groups.py
api/src/routes/transcripts.py
api/src/routes/tutoring.py
api/src/services/create_kg.py
api/src/services/generate_content.py
CLAUDE.md
frontend/.claude/settings.local.json
frontend/.dockerignore
frontend/.gitignore
frontend/components.json
frontend/Dockerfile
frontend/eslint.config.mjs
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/scripts/install-rtms.mjs
frontend/server/auto-responder.ts
frontend/server/chat-route.ts
frontend/server/db.ts
frontend/server/index.ts
frontend/server/intervention-route.ts
frontend/server/poll-activate-route.ts
frontend/server/poll-close-route.ts
frontend/server/poll-generate-route.ts
frontend/server/poll-respond-route.ts
frontend/server/rtms.ts
frontend/server/simulator.ts
frontend/server/socket-helpers.ts
frontend/server/socket.ts
frontend/server/transcript-route.ts
frontend/src/app/api/health/route.ts
frontend/src/app/api/lectures/[id]/interventions/route.ts
frontend/src/app/api/lectures/[id]/summary/route.ts
frontend/src/app/api/lectures/[id]/transcript/route.ts
frontend/src/app/api/lectures/route.ts
frontend/src/app/api/perplexity/ask/route.ts
frontend/src/app/api/polls/[pollId]/results/route.ts
frontend/src/app/api/resources/search/route.ts
frontend/src/app/api/study-groups/chat/route.ts
frontend/src/app/api/tutoring/sessions/[id]/messages/route.ts
frontend/src/app/api/tutoring/sessions/route.ts
frontend/src/app/globals.css
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/app/professor/dashboard/page.tsx
frontend/src/app/professor/upload/page.tsx
frontend/src/app/providers.tsx
frontend/src/app/student/[studentId]/page.tsx
frontend/src/app/student/[studentId]/study-group/page.tsx
frontend/src/app/student/[studentId]/summaries/page.tsx
frontend/src/app/student/[studentId]/tutor/page.tsx
frontend/src/components/dashboard/ConceptHeatmap.tsx
frontend/src/components/dashboard/ConceptTimeline.tsx
frontend/src/components/dashboard/InterventionPanel.tsx
frontend/src/components/dashboard/PollControls.tsx
frontend/src/components/dashboard/StudentList.tsx
frontend/src/components/dashboard/TranscriptFeed.tsx
frontend/src/components/dashboard/ZoomSettingsDialog.tsx
frontend/src/components/graph/KnowledgeGraph.tsx
frontend/src/components/graph/NodeDetailPanel.tsx
frontend/src/components/student/ConceptLearning.tsx
frontend/src/components/student/LectureSummaryPanel.tsx
frontend/src/components/student/PerplexityDialog.tsx
frontend/src/components/student/PollCard.tsx
frontend/src/components/student/SidePanel.tsx
frontend/src/components/study-group/ConceptSelector.tsx
frontend/src/components/study-group/MatchedCard.tsx
frontend/src/components/study-group/WaitingCard.tsx
frontend/src/components/tutor/ChatInterface.tsx
frontend/src/components/tutor/WeakConceptsSidebar.tsx
frontend/src/components/ui/badge.tsx
frontend/src/components/ui/button.tsx
frontend/src/components/ui/card.tsx
frontend/src/components/ui/dialog.tsx
frontend/src/components/ui/dropdown-menu.tsx
frontend/src/components/ui/input.tsx
frontend/src/components/ui/select.tsx
frontend/src/components/ui/StarsBackground.tsx
frontend/src/components/ui/textarea.tsx
frontend/src/lib/api.ts
frontend/src/lib/auth-context.tsx
frontend/src/lib/colors.test.ts
frontend/src/lib/colors.ts
frontend/src/lib/concept-cache.ts
frontend/src/lib/flask.ts
frontend/src/lib/graph.ts
frontend/src/lib/prompts/__tests__/parsing.test.ts
frontend/src/lib/prompts/concept-detection.ts
frontend/src/lib/prompts/concept-extraction.ts
frontend/src/lib/prompts/intervention.ts
frontend/src/lib/prompts/lecture-summary.ts
[19 more files omitted for size]
```

### Dependencies

- api/requirements.txt: anthropic, flask, flask-cors, gunicorn, pyjwt[crypto], pypdf2, python-dotenv, redis, requests, supabase
- frontend/package.json: @anthropic-ai/sdk@^0.74.0, @supabase/supabase-js@^2.95.3, @tailwindcss/postcss@^4, @types/d3-force@^3.0.10, @types/express@^5.0.6, @types/node@^20, @types/react@^19, @types/react-dom@^19, @zoom/rtms@^1.0.2, class-variance-authority@^0.7.1, clsx@^2.1.1, d3-force@^3.0.0, dotenv@^17.3.1, eslint@^9, eslint-config-next@16.1.6, express@^5.2.1, ioredis@^5.9.3, jsrsasign@^11.1.0, katex@^0.16.28, lucide-react@^0.564.0, motion@^12.34.0, next@16.1.6, radix-ui@^1.4.3, react@19.2.3, react-dom@19.2.3, react-force-graph-2d@^1.29.1, react-markdown@^10.1.0, react-use-measure@^2.1.7, recharts@^3.7.0, rehype-katex@^7.0.1, remark-gfm@^4.0.1, remark-math@^6.0.0, shadcn@^3.8.4, socket.io@^4.8.3, socket.io-client@^4.8.3, tailwind-merge@^3.4.0, tailwindcss@^4, tsx@^4.21.0, tw-animate-css@^1.4.0, typescript@^5, vitest@^4.0.18

### Recent commits (newest first)

- chore: clean up repo — remove IDE config, temp files, and legacy scripts
- Update license information in README
- Update README to simplify database table creation instructions
- Update README: tagline, live demo placeholder, env wording
- feat: add script to fix missing student mastery data
- Brand tutor as Aaron - AI Tutor, align tutoring and learn UI with landing style
- fix: stop heatmap replay on re-render and use mock teaching suggestions
- fix: demo upload returns early without DB writes to preserve seeded data
- fix: stop heatmap replaying animation on unrelated re-renders
- perf: switch teaching suggestions to Haiku for speed, work without concepts
- feat: add simple teaching suggestions based on recent transcript
- fix(study-group): add fallback partnerProfile so availability always renders
- fix: reset ConceptLearning state when switching concepts
- fix: correct table names in GET endpoints for learning pages and quizzes
- fix: use concept_quiz_questions table instead of quiz_questions
- fix(study-group): use real Zoom link + fix availability visibility
- fix: correct table name in query to use concept_learning_pages
- feat: update script to call individual concept endpoints
- fix: use concept_learning_pages table instead of learning_pages
- fix(dashboard): fall through to all courses when teacher has none + lecture numbering + upload delay

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

### CLAUDE.md

```markdown
# Prereq — Shared Development Guide

## Project Overview

Prereq is a live Zoom classroom companion with personalized knowledge graphs per student. Two services, one shared Postgres database, deployed on Render.

**PRD:** `tasks/prd-prereq.md` — read this first for full context on goals, user stories, and functional requirements.

---

## Development Principles

**KISS — Keep It Simple.** This is a 48-hour hackathon. Every line of code should serve the demo. No abstractions "for later," no config systems, no plugin architectures. If a hardcoded string works, use a hardcoded string. If a 10-line function works, don't refactor it into 3 files. Simple code that works beats elegant code that's half-finished.

**YAGNI — You Aren't Gonna Need It.** Don't build anything not in the PRD. No pagination, no error boundaries for edge cases that won't happen in the demo, no user settings, no admin panels. If it's in the Non-Goals section of the PRD, don't build it. If you're thinking "this might be useful," stop — it won't be, not in 48 hours.

**TDD — Write Tests First for Critical Paths.** You don't need 100% coverage, but write tests *before* implementation for:
- Flask mastery endpoints (confidence rules are the most bug-prone logic)
- Confidence-to-color derivation (both Python and TypeScript)
- Claude prompt output parsing (eval_result mapping, concept detection label parsing)
- The attendance-boost capping logic

Skip tests for: UI components, Socket.IO event wiring, Claude prompt text (test the parsing, not the prose). Use `pytest` for Flask, `vitest` for Next.js.

**Prompting — Ask Until You're 100% Sure.** Before writing or modifying any Claude prompt (concept extraction, concept detection, question generation, response evaluation, tutoring agent, understanding check), ask clarifying questions until you have zero ambiguity about: (1) the exact input format, (2) the exact output JSON schema, (3) edge cases (what if Claude returns unexpected keys? what if no concepts are detected?), and (4) which model it runs on. Don't guess — ask. A bad prompt wastes more time than a 2-minute conversation.

---

## Architecture

```
┌─────────────────────────┐     ┌─────────────────────────┐
│  frontend/              │     │  api/                   │
│  Next.js + Socket.IO    │────→│  Flask (Python)         │
│  Port 3000              │HTTP │  Port 5000              │
│                         │     │                         │
│  - All frontend pages   │     │  - Knowledge graph CRUD │
│  - Socket.IO server     │     │  - PDF upload + extract │
│  - Zoom RTMS listener   │     │  - Mastery updates      │
│  - Claude AI calls:     │     │  - Graph queries        │
│    - concept detection  │     │  - Heatmap aggregation  │
│    - question generation│     │  - Concept extraction   │
│    - response evaluation│     │    via Claude            │
│    - tutoring agent     │     │                         │
│  - Perplexity calls     │     │                         │
└───────
[truncated — 27527 more characters]
```

### api/API_DOCUMENTATION.md

```markdown
# API Endpoint Documentation

**Base URL:** `http://localhost:5000` (development) or `FLASK_API_URL` (production)
**All endpoints return JSON**
**Error responses:** `{ "error": "Error message" }` with appropriate HTTP status codes

---

## Table of Contents

1. [Courses](#courses)
2. [Concepts](#concepts)
3. [Students & Mastery](#students--mastery)
4. [Graph](#graph)
5. [Heatmap](#heatmap)
6. [Lectures](#lectures)
7. [Transcripts](#transcripts)
8. [Polls](#polls)
9. [Tutoring](#tutoring)
10. [Pages & Quizzes](#pages--quizzes)
11. [Create/Upload](#createupload)

---

## Courses

### GET /api/courses
**Type:** CRUD
**Purpose:** Retrieve all courses
**Auth:** None
**Request:** None
**Response:**
```json
[
  {
    "id": "uuid",
    "name": "CS229 Machine Learning",
    "description": "Introduction to machine learning",
    "created_at": "2025-01-15T10:30:00Z"
  }
]
```

### POST /api/courses
**Type:** CRUD
**Purpose:** Create a new course
**Auth:** None
**Request Body:**
```json
{
  "name": "CS229 Machine Learning",
  "description": "Introduction to machine learning"
}
```
**Response:** `201 Created`
```json
{
  "id": "uuid",
  "name": "CS229 Machine Learning",
  "description": "Introduction to machine learning",
  "created_at": "2025-01-15T10:30:00Z"
}
```

### GET /api/courses/{course_id}
**Type:** CRUD
**Purpose:** Retrieve a single course by ID
**Auth:** None
**Path Parameters:**
- `course_id` (string, uuid): Course identifier

**Response:** `200 OK`
```json
{
  "id": "uuid",
  "name": "CS229 Machine Learning",
  "description": "Introduction to machine learning",
  "created_at": "2025-01-15T10:30:00Z"
}
```
**Error:** `404 Not Found` if course doesn't exist

### POST /api/courses/{course_id}/upload
**Type:** NON-CRUD (Knowledge Graph Generation)
**Purpose:** Upload a PDF and generate knowledge graph for a course
**Auth:** None
**Path Parameters:**
- `course_id` (string, uuid): Course identifier

**Request:**
- Content-Type: `multipart/form-data`
- Body: `file` (PDF file)

**Process:**
1. Calculates MD5 hash of uploaded file
2. Checks `pdf_cache` table for existing results
3. If cached: Returns cached graph data
4. If not cached:
   - Saves PDF to `/tmp/{hash}_{filename}`
   - Calls `create_kg()` to extract knowledge graph using Claude API
   - Parses knowledge graph markdown into structured format
   - Calculates importance scores for nodes
   - Deletes temporary file
   - Caches result in `pdf_cache` table
5. Updates course with `pdf_cache_hash`
6. Inserts concept nodes into `concept_nodes` table
7. Inserts edges into `concept_edges` table

**Response:** `200 OK`
```json
{
  "graph": {
    "nodes": {
      "Backpropagation": "Algorithm for computing gradients...",
      "Chain Rule": "Calculus rule for derivatives..."
    },
    "edges": [
      ["Chain Rule", "Backpropagation"],
      ["Gradient Descent", "Backpropagation"]
    ]
  },
  "importance": {
    "Backpropagation": 0.85,
    "Chain Rule": 0.72
  }
}
```

**Notes:**
- Uses MD5 hash-bas
[truncated — 39314 more characters]
```

### api/requirements.txt

```
flask
flask-cors
supabase
anthropic
pypdf2
gunicorn
python-dotenv
requests
pyjwt[crypto]
redis
```

### api/Dockerfile

```
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .
COPY src/ ./src/

EXPOSE 8080
ENTRYPOINT ["sh", "-c", "gunicorn app:app --bind 0.0.0.0:$PORT"]
```

### frontend/Dockerfile

```
FROM node:20-slim AS base

# Install libstdc++6 from Debian testing (trixie) — the @zoom/rtms native
# binary needs GLIBCXX_3.4.32 which requires GCC 13+, not available in Bookworm
RUN echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list && \
    apt-get update && \
    apt-get install -y -t trixie libstdc++6 && \
    rm -f /etc/apt/sources.list.d/trixie.list && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Create logs directory for @zoom/rtms SDK internal logging
RUN mkdir -p /app/logs

# Install dependencies
COPY package.json package-lock.json* ./
RUN npm ci

# Copy source
COPY . .

# Build Next.js (NEXT_PUBLIC_* vars must be set at build time via Render build args)
ARG NEXT_PUBLIC_FLASK_API_URL
ENV NEXT_PUBLIC_FLASK_API_URL=$NEXT_PUBLIC_FLASK_API_URL
ENV NODE_ENV=production
RUN npm run build

EXPOSE 3000

CMD ["npx", "tsx", "server/index.ts"]

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "tsx server/index.ts",
    "build": "node scripts/install-rtms.mjs; next build",
    "start": "tsx server/index.ts",
    "lint": "eslint",
    "test": "npx vitest run"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.74.0",
    "@supabase/supabase-js": "^2.95.3",
    "@zoom/rtms": "^1.0.2",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "d3-force": "^3.0.0",
    "dotenv": "^17.3.1",
    "express": "^5.2.1",
    "ioredis": "^5.9.3",
    "jsrsasign": "^11.1.0",
    "katex": "^0.16.28",
    "lucide-react": "^0.564.0",
    "motion": "^12.34.0",
    "next": "16.1.6",
    "radix-ui": "^1.4.3",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "react-force-graph-2d": "^1.29.1",
    "react-markdown": "^10.1.0",
    "react-use-measure": "^2.1.7",
    "recharts": "^3.7.0",
    "rehype-katex": "^7.0.1",
    "remark-gfm": "^4.0.1",
    "remark-math": "^6.0.0",
    "socket.io": "^4.8.3",
    "socket.io-client": "^4.8.3",
    "tailwind-merge": "^3.4.0",
    "tsx": "^4.21.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/d3-force": "^3.0.10",
    "@types/express": "^5.0.6",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "shadcn": "^3.8.4",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0",
    "typescript": "^5",
    "vitest": "^4.0.18"
  }
}

```

### api/app.py

```python
from flask import Flask, jsonify, request
from flask_cors import CORS

from src.routes.pages import pages
from src.routes.courses import courses
from src.routes.create import create
from src.routes.graph import graph
from src.routes.students import students
from src.routes.heatmap import heatmap
from src.routes.lectures import lectures
from src.routes.transcripts import transcripts
from src.routes.concepts import concepts
from src.routes.polls import polls
from src.routes.tutoring import tutoring
from src.routes.study_groups import study_groups
from src.routes.auth import auth

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024  # 50MB upload limit
CORS(app)

app.register_blueprint(auth)
app.register_blueprint(create)
app.register_blueprint(courses)
app.register_blueprint(students)
app.register_blueprint(graph)
app.register_blueprint(heatmap)
app.register_blueprint(lectures)
app.register_blueprint(transcripts)
app.register_blueprint(concepts)
app.register_blueprint(polls)
app.register_blueprint(tutoring)
app.register_blueprint(study_groups)
app.register_blueprint(pages)



@app.after_request
def add_cache_headers(response):
    if request.method == 'GET' and response.status_code == 200:
        response.headers['Cache-Control'] = 'public, max-age=5'
    return response


@app.route('/api/health', methods=['GET'])
def health():
    return jsonify({'status': 'ok'}), 200


if __name__ == '__main__':
    import os
    port = int(os.getenv("PORT", 8080))
    app.run(host="0.0.0.0", port=port, debug=True)

```

### frontend/server/index.ts

```typescript
import path from "path";
import dotenv from "dotenv";

// Load .env from project root BEFORE anything else
dotenv.config({ path: path.resolve(__dirname, "../../.env") });

import express from "express";
import { createServer } from "http";
import next from "next";
import { Server } from "socket.io";
import { setupSocket } from "./socket";
import transcriptRoute from "./transcript-route";
import chatRoute from "./chat-route";
import pollGenerateRoute from "./poll-generate-route";
import pollActivateRoute from "./poll-activate-route";
import pollRespondRoute from "./poll-respond-route";
import pollCloseRoute from "./poll-close-route";
import interventionRoute from "./intervention-route";
const dev = process.env.NODE_ENV !== "production";
const port = parseInt(process.env.PORT || "3000", 10);

const app = express();
const server = createServer(app);

const io = new Server(server, {
  cors: { origin: "*" },
});

setupSocket(io);

// Initialize Zoom RTMS — always attempt to load so per-teacher webhooks work
// even without global ZOOM_CLIENT_ID env var
app.use("/webhook", express.json());
app.use("/webhook/:teacherId", express.json());

let clearCredentialCacheFn: ((teacherId: string) => void) | null = null;

// Start RTMS import early, but don't block — we'll await it before registering the catch-all
const rtmsReady = import("./rtms").then(({ setupRTMS, clearCredentialCache }) => {
  setupRTMS(app);
  clearCredentialCacheFn = clearCredentialCache;
}).catch((err) => {
  console.warn("[RTMS] Zoom RTMS module unavailable:", err.message);
});

// Cache-clearing endpoint called by ZoomSettingsDialog after saving credentials
app.post("/api/zoom/clear-cache", express.json(), (req, res) => {
  const teacherId = req.query.teacherId as string;
  if (teacherId && clearCredentialCacheFn) {
    clearCredentialCacheFn(teacherId);
  }
  res.json({ ok: true });
});

const nextApp = next({ dev });
const nextHandler = nextApp.getRequestHandler();

nextApp.prepare().then(async () => {
  // Wait for RTMS routes to register BEFORE the Next.js catch-all
  await rtmsReady;

  // Transcript route runs in Express context so Socket.IO emit works
  app.use(transcriptRoute);

  // Chat route runs in Express context to avoid body parsing issues
  app.use(chatRoute);

  // Poll routes run in Express so Socket.IO emits work
  app.use(pollGenerateRoute);
  app.use(pollActivateRoute);
  app.use(pollRespondRoute);
  app.use(pollCloseRoute);

  // Intervention route runs in Express to ensure env vars work
  app.use(interventionRoute);

  // Pass all other requests to Next.js
  app.use((req, res) => {
    return nextHandler(req, res);
  });

  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
    console.log(`> Socket.IO server attached`);
  });
});

```

### frontend/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono, Instrument_Serif, Comfortaa } from "next/font/google";
import "./globals.css";
import Providers from "./providers";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

const comfortaa = Comfortaa({
  variable: "--font-comfortaa",
  subsets: ["latin"],
});

const instrumentSerif = Instrument_Serif({
  variable: "--font-instrument-serif",
  subsets: ["latin"],
  weight: "400",
  style: ["normal", "italic"],
});

export const metadata: Metadata = {
  title: "Prereq",
  description: "Live classroom companion with personalized knowledge graphs",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} ${instrumentSerif.variable} ${comfortaa.variable} antialiased`}
      >
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

```

### frontend/src/app/page.tsx

```typescript
"use client";

import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/lib/auth-context";
import { flaskApi } from "@/lib/api";
import StarsBackground from "@/components/ui/StarsBackground";

export default function LandingPage() {
  const router = useRouter();
  const { user, role, profile, courses, enrollments, loading, signIn, signUp, signOut } = useAuth();

  const [showAuth, setShowAuth] = useState(false);
  const [mode, setMode] = useState<"login" | "signup">("login");
  const [roleToggle, setRoleToggle] = useState<"student" | "teacher">("student");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");
  const [joinCode, setJoinCode] = useState("");
  const [error, setError] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [enrolling, setEnrolling] = useState(false);

  // Auto-redirect authenticated users
  useEffect(() => {
    if (loading || !user) return;
    setShowAuth(true);
    if (role === "teacher" && courses.length > 0) {
      localStorage.setItem("courseId", courses[0].id);
      router.push("/professor/dashboard");
    }
  }, [loading, user, role, courses, router]);

  if (loading) {
    return (
      <div className="relative min-h-screen flex items-center justify-center overflow-hidden">
        <StarsBackground />
        <div className="relative z-10 text-gray-400 text-sm font-light tracking-wide">Loading...</div>
      </div>
    );
  }

  async function handleAuth(e: React.FormEvent) {
    e.preventDefault();
    setError("");
    setSubmitting(true);
    try {
      if (mode === "login") {
        const result = await signIn(email, password);
        if (result.error) setError(result.error);
      } else {
        if (!name.trim()) { setError("Name is required"); setSubmitting(false); return; }
        const result = await signUp(email, password, name, roleToggle);
        if (result.error) setError(result.error);
      }
    } finally {
      setSubmitting(false);
    }
  }

  async function handleEnroll(e: React.FormEvent) {
    e.preventDefault();
    if (!joinCode.trim()) return;
    setError("");
    setEnrolling(true);
    try {
      const result: { student_id: string; course_id: string; course_name: string } = await flaskApi.post("/api/courses/enroll", { join_code: joinCode });
      localStorage.setItem("courseId", result.course_id);
      localStorage.setItem("studentId", result.student_id);
      document.cookie = `studentId=${result.student_id};path=/`;
      router.push(`/student/${result.student_id}`);
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : "Enrollment failed");
    } finally {
      setEnrolling(false);
    }
  }

  // --- Authenticated: Teacher with no courses ---
  if (user && role === "teacher" && courses.length === 0) {
    return (
      <div className="relative min-h-screen flex items-center justify-center overflow-hidden">
        <StarsBackground />
        <div className="relative z-10 w-full max-w-md px-6">
          <Header />
          <GlassCard>
            <div className="flex items-center justify-between mb-6">
              <h2 className="text-lg font-medium text-gray-800 tracking-tight">Create Your Course</h2>
              <button onClick={signOut} className="text-xs text-gray-400 hover:text-gray-600 transition-colors">Sign out</button>
            </div>
            <p className="text-sm text-gray-500 mb-4">Welcome, {profile?.name || "Professor"}! Create a course to get started.</p>
            <CreateCourseForm onCreated={() => router.push("/professor/upload")} />
          </GlassCard>
        </div>
      </div>
    );
  }

  // --- Authenticated: Student ---
  if (user && role === "student") {
    return (
      <div className="relative min-h-screen flex items-center justify-center overflow-hidden">
        <StarsBackground />
        <div className="relative z-10 w-full max-w-md px-6">
          <Header />
          <GlassCard>
            <div className="flex items-center justify-between mb-6">
              <h2 className="text-lg font-medium text-gray-800 tracking-tight">
                {enrollments.length > 0 ? "Your Courses" : "Join a Course"}
              </h2>
              <button onClick={signOut} className="text-xs text-gray-400 hover:text-gray-600 transition-colors">Sign out</button>
            </div>

            {enrollments.length > 0 && (
              <div className="space-y-3 mb-6">
                {enrollments.map((e) => (
                  <button
                    key={e.student_id}
                    onClick={() => {
                      localStorage.setItem("courseId", e.course_id);
                      localStorage.setItem("studentId", e.student_id);
                      document.cookie = `studentId=${e.student_id};path=/`;
                      router.push(`/student/${e.student_id}`);
                    }}
                    className="w-full text-left p-4 rounded-xl border border-gray-200 hover:border-gray-300 bg-white/60 hover:bg-white transition-all group"
                  >
                    <div className="font-medium text-gray-700 group-hover:text-gray-900 transition-colors">{e.course_name}</div>
                    <div className="text-xs text-gray-400 mt-1">Click to join lecture</div>
                  </button>
                ))}
              </div>
            )}

            <form onSubmit={handleEnroll} className="space-y-4">
              <div>
                <label className="block text-sm font-medium text-gray-500 mb-1.5">
                  {enrollments.length > 0 ? "Join another course" : "Enter join code"}
                </label>
                <input
                  type="text"
                  value={joinCode}
                  onChange={(e) => setJoinCode(e.target.value.toUpperCase())}
                  placeholder="e.g. CS229M"
                  classN
[truncated — 12127 more characters]
```

[110 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]