# Project export: pigeonhole

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: Pigeonhole is an AI TA designed for instruction, not answer delivery. It follows course rules, prompts student reasoning, and produces reviewable summaries that help instructors diagnose confusion.
- Devpost: https://devpost.com/software/pigeonhole
- GitHub: https://github.com/danielmargento/course-ta-bot.git
- Video: https://player.vimeo.com/video/1165160143?byline=0&portrait=0&title=0#t=
- Team: 2 GitHub contributor(s) — danielmargento (16 commits), annie33lii (3 commits)

## Devpost submission (written by the team)

### Inspiration

Generative AI is now a default study tool. The issue is not use vs. no use, but alignment: general-purpose chatbots do not know a course’s syllabus, policies, or pedagogical intent, and they often optimize for answer completion rather than conceptual understanding. We built pigeonhole to make AI support instruction by design, with instructor control and learning-first interaction.

### What it does

pigeonhole is a course-aligned AI TA that provides scaffolded help rather than direct solutions. For students, it guides problem solving through questions, hints, and stepwise decomposition. After a session, it produces a structured recap (what was confusing, how understanding changed, what to review) and enables quick review through a lightweight quiz. For instructors, it offers configurable guardrails (what help is allowed, what topics are off-limits) and aggregated insight into common misconceptions and friction points.

### How we built it

We implemented a web app with distinct student and instructor workflows: Instructor configuration: course context, policy constraints, and approved hint scaffolds. Student chat: a guided dialogue loop that prioritizes questioning, partial hints, and checking student work before progressing. Learning artifacts: automatic session summaries and exportable PDFs for later study or forum posting. Analytics: aggregation of anonymized interaction signals to surface high-level confusion patterns.

### Challenges we ran into

Defining “helpful but not too helpful” in a way that is consistent across topics and student behaviors. Preventing accidental answer leakage while still allowing students to make progress under time pressure. Translating open-ended chat transcripts into concise, accurate recaps without introducing new errors. Designing instructor controls that are powerful but simple enough to set up quickly.

### Accomplishments we're proud of

Built an end-to-end experience that serves both students and instructors, not just a chatbot. Implemented policy-aware scaffolding that reliably pushes for student work and reasoning before hints escalate. Produced reusable learning artifacts (recaps + PDF export) that turn chat into study material.

### What we learned

Guardrails are most effective when they are explicit, instructor-configurable, and enforced at the interaction level, not just as a disclaimer. Students respond well to structured guidance when it reduces cognitive load (first step, plan, checkpoints). Instructor trust depends on transparency, control, and clear evidence of learning alignment.

### What's next

Richer instructor tooling: assignment-specific rubrics, granular hint templates, and per-topic constraints. Stronger analytics: confusion clustering by concept, time-to-first-progress, and pre/post understanding signals. Personalized study support: adaptive review plans before exams, spaced repetition prompts, and targeted practice sets. Multimodal outputs: short personalized explanation videos based on what a student struggled with. Deeper integration: LMS and course content connections to make setup and compliance seamless.

## README (from the GitHub repository)

# 🎓 Pascal — AI Teaching Assistant

AI isn't going anywhere. Students are already using ChatGPT, Claude, and Copilot to do their homework — often getting complete solutions with zero learning. Banning AI tools doesn't work. The answer isn't prohibition; it's giving teachers control.

**Pascal** is a course-aware AI TA that puts instructors in the loop. Teachers configure exactly how much help the bot gives, what it can and can't reveal, and which materials it draws from. Students get real-time scaffolded help that builds understanding — not an answer machine.

---

## 💡 Why Pascal?

Most AI tools treat education as an afterthought. Students paste a homework problem into ChatGPT and get a full solution. They learn nothing. Teachers have no visibility or control.

Pascal flips this:

- 🧑‍🏫 **Teacher in the loop.** Instructors set the rules — help level, allowed artifacts, topic restrictions, staff notes — and the bot follows them. The teacher decides whether students get Socratic questioning, guided hints, or full tutoring. Not the AI.
- 🔒 **Integrity first.** The default is restrictive. No answers, no full code, attempt required first. Teachers opt *in* to more permissive modes when appropriate. The bot enforces academic integrity by design, not as an afterthought.
- 📄 **Scoped to real course materials.** Pascal only knows what the teacher gives it — syllabi, lecture notes, problem sets. It doesn't hallucinate references to materials that don't exist. When general chat is enabled, it's limited to teacher-selected documents.
- 📌 **Assignment-aware.** Each assignment can have its own help level, anchor document, staff notes, and annotated hints. When a student asks about "question 4," the bot knows exactly which document to look in.

---

## ✨ Features

### 🛠 For Instructors

- **Course creation** with join codes for student enrollment
- **Three help levels** per assignment: Strict (confirmation only), Guided (Socratic hints), Full Support (complete tutoring after effort shown)
- **Policy guardrails** — toggle final answers, full code, require-attempt-first, allowed/disallowed artifacts
- **Topic gating** — mark topics as "not yet taught," "allowed," or "warn" to prevent the bot from spoiling upcoming material
- **Staff notes** — private per-assignment instructions the bot follows but never reveals to students
- **Anchor documents** — designate one material as THE assignment doc so the bot knows where to look for question numbers
- **PDF annotations** — highlight specific passages in uploaded PDFs with private hints for the bot
- **General chat control** — disabled by default; teachers enable it and select exactly which materials (e.g. syllabus) the bot can reference
- **Course materials** — upload PDFs, text files, and markdown; extracted text is chunked and embedded for RAG retrieval
- **Usage insights** — see which assignments get the most questions, top topics, common misconceptions (LLM-summarized)
- **Announcements** — post messages visible to enrolled students
- **Roster management** — view enrolled students
- **Student view preview** — see exactly what students see

### 🎒 For Students

- **Assignment-aware chat** — select an assignment and get help scoped to that specific problem set and its materials
- **Concept check quizzes** — inline multiple-choice questions after explanations to reinforce learning (toggleable)
- **Clickable source citations** — bot responses link back to specific pages/sections of course materials
- **Save and bookmark** helpful messages during a session
- **Export to PDF** — save threads or snippets for offline review or forum sharing
- **Session history** — resume past conversations per assignment
- **Feedback** — rate responses as helpful, not helpful, or too revealing
- **LaTeX and Markdown** rendering for math-heavy courses

### ⚙️ Under the Hood

- **RAG retrieval** — course materials are chunked, embedded, and searched via vector similarity so the bot references real content, not hallucinations
- **Anchor boosting** — chunks from the designated assignment document get a similarity boost and a guaranteed secondary search fallback
- **Anti-hallucination prompting** — the bot is explicitly forbidden from inventing lecture names, chapter titles, or any references not present in its provided materials
- **Per-assignment policy overrides** — each assignment can override the course-level help policy
- **Streaming responses** via SSE for real-time chat
- **Question hint matching** — the bot detects when students reference specific questions and applies instructor-provided hints

---

## 🧱 Tech Stack

- **Next.js** (App Router) + TypeScript
- **TailwindCSS**
- **Supabase** (Postgres + Auth + Storage)
- **OpenAI API** (GPT-4o-mini for chat, text-embedding-3-small for RAG)

---

## 🚀 Getting Started

1. Clone the repo
2. Copy `.env.example` to `.env.local` and fill in Supabase + OpenAI keys
3. Run migrations: `supabase db push`
4. Install dependencies: `npm install`
5. Start dev server: `npm run dev`

---

## 📁 Project Structure

```
src/
  app/
    admin/          # Instructor dashboard + course config
    student/        # Student dashboard + chat
    api/            # API routes (chat, assignments, bot-config, etc.)
  components/
    admin/          # AssignmentEditor, MaterialsPanel, PdfAnnotator, etc.
    assignments/    # AssignmentSelect
    chat/           # ChatWindow, ChatMessage, ChatComposer
    courses/        # CourseCard
    layout/         # AppShell (nav)
    pdf/            # ExportButton
  lib/
    prompt.ts       # System prompt builders (full-text + RAG)
    policy.ts       # Guardrail logic, topic gating
    embeddings.ts   # Vector embedding generation
    types.ts        # Shared TypeScript types
supabase/
  migrations/       # SQL migrations
```


## Detected evidence (automated analysis)

Indexed codebase: 83 recognized source files, 290 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (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
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (89 of 89)

```
.gitignore
eslint.config.mjs
next.config.ts
package.json
postcss.config.mjs
PROJECT_STRUCTURE.md
README.md
src/app/admin/course/[id]/page.tsx
src/app/admin/courses/page.tsx
src/app/api/announcements/route.ts
src/app/api/announcements/view/route.ts
src/app/api/assignments/route.ts
src/app/api/bot-config/route.ts
src/app/api/chat/route.ts
src/app/api/concept-checks/route.ts
src/app/api/courses/route.ts
src/app/api/debug-materials/route.ts
src/app/api/enroll/route.ts
src/app/api/insights/route.ts
src/app/api/instructor-chat/route.ts
src/app/api/materials/reextract/route.ts
src/app/api/materials/route.ts
src/app/api/materials/view/route.ts
src/app/api/messages/route.ts
src/app/api/profile/route.ts
src/app/api/roster/route.ts
src/app/api/sessions/route.ts
src/app/api/signup/route.ts
src/app/globals.css
src/app/layout.tsx
src/app/login/page.tsx
src/app/page.tsx
src/app/profile/page.tsx
src/app/signup/page.tsx
src/app/student/course/[id]/history/page.tsx
src/app/student/course/[id]/page.tsx
src/app/student/courses/page.tsx
src/components/admin/AssignmentEditor.tsx
src/components/admin/ContextUploader.tsx
src/components/admin/InsightsPanel.tsx
src/components/admin/InstructorChatPanel.tsx
src/components/admin/MaterialsPanel.tsx
src/components/admin/PolicyEditor.tsx
src/components/admin/PreviewPanel.tsx
src/components/admin/RosterPanel.tsx
src/components/admin/StylePresetSelect.tsx
src/components/assignments/AssignmentSelect.tsx
src/components/chat/ChatComposer.tsx
src/components/chat/ChatMessage.tsx
src/components/chat/ChatWindow.tsx
src/components/chat/ConceptCheckCard.tsx
src/components/chat/PigeonChatBubble.tsx
src/components/chat/SavedNotesPanel.tsx
src/components/chat/SaveToggle.tsx
src/components/courses/CourseCard.tsx
src/components/courses/CourseHeader.tsx
src/components/landing/PigeonHero.tsx
src/components/layout/AppShell.tsx
src/components/pdf/ExportButton.tsx
src/config/defaultPolicy.ts
src/config/stylePresets.ts
src/hooks/useUser.ts
src/lib/analytics.ts
src/lib/auth.ts
src/lib/conceptCheck.ts
src/lib/fileParse.ts
src/lib/pdf.ts
src/lib/policy.ts
src/lib/prompt.ts
src/lib/sourceLink.ts
src/lib/supabaseClient.ts
src/lib/supabaseServer.ts
src/lib/types.ts
src/middleware.ts
supabase/.temp/cli-latest
supabase/migrations/001_initial_schema.sql
supabase/migrations/002_auth_profiles.sql
supabase/migrations/003_profile_names.sql
supabase/migrations/004_enrollments.sql
supabase/migrations/005_announcements.sql
supabase/migrations/006_course_materials.sql
supabase/migrations/007_assignment_style.sql
supabase/migrations/008_assignment_due_date.sql
supabase/migrations/009_assignment_materials.sql
supabase/migrations/010_concept_checks.sql
supabase/migrations/011_concept_check_saved.sql
supabase/migrations/012_insights_per_assignment.sql
supabase/migrations/016_anchor_and_general_chat.sql
tsconfig.json
```

### Dependencies

- package.json: @supabase/ssr@^0.8.0, @supabase/supabase-js@^2.95.3, @tailwindcss/postcss@^4, @tailwindcss/typography@^0.5.19, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.6, katex@^0.16.28, marked@^17.0.2, next@16.1.6, openai@^6.22.0, pdf-parse@^2.4.5, react@19.2.3, react-dom@19.2.3, react-markdown@^10.1.0, rehype-katex@^7.0.1, remark-gfm@^4.0.1, remark-math@^6.0.0, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- Update README with project description, features, and emojis
- Anchor documents + general chat gating + anti-hallucination prompt fixes
- LaTeX source companion for PDF uploads + fix unicode extraction
- fix concept checks: make mandatory in prompt + render markdown in explanations
- clickable source citations + restrict uploads to PDF/TXT/MD
- concept check
- fix policy-blocked messages going silent and externalize native packages
- debugging final touches
- Fix pdf-parse import to avoid test-file bug
- Switch to pdf-parse v1 for reliable server-side PDF extraction
- Per-assignment insights and general mode restrictions
- Fix PDF extraction by replacing pdf-parse with direct pdfjs-dist
- Add material extraction status and re-extract button
- final touches
- updated functionality
- course materials
- assignment feature
- Changed chatbot UI.
- updated homepage
- updated chatbot page UI, added pigeon conversation feature.

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

### PROJECT_STRUCTURE.md

```markdown
# Course TA Bot — Project Structure Memo

## Overview

This is a Next.js (App Router) + TypeScript project that lets instructors create course-specific, policy-constrained AI teaching assistants. Students chat with the TA for scaffolded learning help. The app uses Supabase for persistence and OpenAI for LLM responses.

---

## Top-Level Files

| File | Purpose |
|------|---------|
| `package.json` | Dependencies and scripts (`dev`, `build`, `start`, `lint`) |
| `tsconfig.json` | TypeScript config with `@/` path alias pointing to `src/` |
| `next.config.ts` | Next.js configuration |
| `postcss.config.mjs` | PostCSS config for Tailwind |
| `eslint.config.mjs` | ESLint rules |
| `.env.local.example` | Template for required env vars (Supabase URL/key, OpenAI key) |

---

## `src/lib/` — Core Logic

| File | What it does |
|------|-------------|
| `types.ts` | All shared TypeScript interfaces: `Course`, `Assignment`, `BotConfig`, `PolicyConfig`, `Session`, `Message`, `StylePreset`, `TopicGate`, `UsageInsight`, `Feedback` |
| `supabaseClient.ts` | Browser-side Supabase client (uses `NEXT_PUBLIC_` env vars) |
| `supabaseServer.ts` | Server-side Supabase client factory for use in API route handlers |
| `policy.ts` | Guardrail logic — checks if a student message requests something disallowed (e.g. full solutions), checks topic gating, and builds a policy instruction block for the LLM system prompt |
| `prompt.ts` | Assembles the full system prompt sent to OpenAI by combining course info, teaching style, policy rules, course context, and assignment details (including staff notes and FAQ) |
| `analytics.ts` | Stub for aggregating usage insights (top topics, session/message counts) — needs real queries wired up |
| `pdf.ts` | Uses `jsPDF` to export an array of messages into a formatted PDF document |

---

## `src/config/` — Constants & Defaults

| File | What it does |
|------|-------------|
| `stylePresets.ts` | Four teaching style presets (Socratic, Direct, Debugging Coach, Exam Review) — each has a label, description, and a system prompt fragment injected into the LLM prompt |
| `defaultPolicy.ts` | Default guardrail settings: no final answers, no full code, require attempt first, 3 hint levels, allowed/disallowed artifact lists, and a default refusal message |

---

## `src/app/api/` — API Route Handlers

All routes are server-side Next.js route handlers.

| Route | Methods | What it does |
|-------|---------|-------------|
| `api/chat/route.ts` | POST | Main chat endpoint. Fetches course + config + assignment from Supabase, runs policy check, builds system prompt, streams response from OpenAI via SSE |
| `api/courses/route.ts` | GET, POST | List all courses / create a new course |
| `api/assignments/route.ts` | GET, POST | List assignments (filterable by `course_id`) / create a new assignment |
| `api/sessions/route.ts` | GET, POST | List sessions (filterable by `course_id`, `student_id`) / create a new session |
| `api/messages/route.ts` | POST | Save
[truncated — 4052 more characters]
```

### package.json

```
{
  "name": "temp-next-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@supabase/ssr": "^0.8.0",
    "@supabase/supabase-js": "^2.95.3",
    "@tailwindcss/typography": "^0.5.19",
    "katex": "^0.16.28",
    "marked": "^17.0.2",
    "next": "16.1.6",
    "openai": "^6.22.0",
    "pdf-parse": "^2.4.5",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "react-markdown": "^10.1.0",
    "rehype-katex": "^7.0.1",
    "remark-gfm": "^4.0.1",
    "remark-math": "^6.0.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter, Instrument_Serif } from "next/font/google";
import "./globals.css";
import AppShell from "@/components/layout/AppShell";

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

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

export const metadata: Metadata = {
  title: "pigeonhole",
  description: "Pascal — AI-powered assistant for course-specific, policy-constrained learning",
};

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

```

### src/app/page.tsx

```typescript
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useUser } from "@/hooks/useUser";
import PigeonHero from "@/components/landing/PigeonHero";

export default function HomePage() {
  const { user, role, loading } = useUser();
  const router = useRouter();

  useEffect(() => {
    if (!loading && user) {
      const dest = role === "instructor" ? "/admin/courses" : "/student/courses";
      router.replace(dest);
    }
  }, [loading, user, role, router]);

  if (loading || user) {
    return (
      <div className="max-w-xl mx-auto text-center mt-24">
        <p className="text-muted text-sm">Loading...</p>
      </div>
    );
  }

  return (
    <div className="overflow-hidden">
      {/* Hero */}
      <section className="relative pt-24 pb-20 overflow-hidden">
        {/* Envelope animations */}
        <style>{`
          @keyframes env1 {
            0%   { transform: translate(0, 0) rotate(-4deg); opacity: 0; }
            5%   { opacity: 0.7; }
            50%  { transform: translate(15vw, -30px) rotate(3deg); opacity: 0.7; }
            95%  { opacity: 0.7; }
            100% { transform: translate(30vw, 10px) rotate(-2deg); opacity: 0; }
          }
          @keyframes env2 {
            0%   { transform: translate(0, 0) rotate(3deg); opacity: 0; }
            5%   { opacity: 0.6; }
            50%  { transform: translate(-12vw, 25px) rotate(-4deg); opacity: 0.6; }
            95%  { opacity: 0.6; }
            100% { transform: translate(-25vw, -15px) rotate(2deg); opacity: 0; }
          }
          @keyframes env3 {
            0%   { transform: translate(0, 0) rotate(-2deg); opacity: 0; }
            5%   { opacity: 0.65; }
            50%  { transform: translate(18vw, 20px) rotate(5deg); opacity: 0.65; }
            95%  { opacity: 0.65; }
            100% { transform: translate(28vw, -20px) rotate(-3deg); opacity: 0; }
          }
          @keyframes env4 {
            0%   { transform: translate(0, 0) rotate(5deg); opacity: 0; }
            5%   { opacity: 0.55; }
            50%  { transform: translate(-14vw, -20px) rotate(-3deg); opacity: 0.55; }
            95%  { opacity: 0.55; }
            100% { transform: translate(-22vw, 15px) rotate(4deg); opacity: 0; }
          }
          .env1 { animation: env1 14s ease-in-out infinite; }
          .env2 { animation: env2 17s ease-in-out infinite; animation-delay: 3s; }
          .env3 { animation: env3 19s ease-in-out infinite; animation-delay: 7s; }
          .env4 { animation: env4 15s ease-in-out infinite; animation-delay: 10s; }
        `}</style>

        <div className="env1 absolute pointer-events-none" style={{ top: '8%', left: '3%' }}>
          <svg width="72" height="54" viewBox="0 0 72 54" fill="none">
            <rect x="4" y="10" width="40" height="28" rx="4" fill="#f5efe6" stroke="#c4b5a0" strokeWidth="1.5" />
            <path d="M4 10 L24 28 L44 10" fill="none" stroke="#c4b5a0" strokeWidth="1.5" strokeLinejoin="round" />
          </svg>
        </div>
        <div className="env2 absolute pointer-events-none" style={{ top: '15%', right: '5%' }}>
          <svg width="60" height="45" viewBox="0 0 60 45" fill="none">
            <rect x="4" y="8" width="34" height="24" rx="3.5" fill="#f5efe6" stroke="#c4b5a0" strokeWidth="1.5" />
            <path d="M4 8 L21 24 L38 8" fill="none" stroke="#c4b5a0" strokeWidth="1.5" strokeLinejoin="round" />
          </svg>
        </div>
        <div className="env3 absolute pointer-events-none" style={{ top: '65%', left: '5%' }}>
          <svg width="55" height="42" viewBox="0 0 55 42" fill="none">
            <rect x="3" y="7" width="30" height="22" rx="3" fill="#f5efe6" stroke="#c4b5a0" strokeWidth="1.5" />
            <path d="M3 7 L18 21 L33 7" fill="none" stroke="#c4b5a0" strokeWidth="1.5" strokeLinejoin="round" />
          </svg>
        </div>
        <div className="env4 absolute pointer-events-none" style={{ top: '72%', right: '4%' }}>
          <svg width="65" height="48" viewBox="0 0 65 48" fill="none">
            <rect x="4" y="9" width="36" height="26" rx="3.5" fill="#f5efe6" stroke="#c4b5a0" strokeWidth="1.5" />
            <path d="M4 9 L22 26 L40 9" fill="none" stroke="#c4b5a0" strokeWidth="1.5" strokeLinejoin="round" />
          </svg>
        </div>

        <div className="text-center relative z-10">
          <div className="flex justify-center mb-8">
            <PigeonHero />
          </div>
          <h1 className="text-8xl font-bold text-foreground mb-6 -mt-6 tracking-tight">
            pigeonhole
          </h1>
          <p className="text-xl text-muted max-w-lg mx-auto leading-relaxed">
            Meet Pascal — an AI assistant that actually helps students learn, without doing the work for them.
          </p>
          <div className="flex gap-4 justify-center mt-10">
            <Link
              href="/signup"
              className="bg-pigeon text-white px-10 py-4 rounded-lg text-lg font-medium hover:bg-pigeon-hover transition-colors"
            >
              Get Started
            </Link>
            <Link
              href="/login"
              className="border border-border text-foreground px-10 py-4 rounded-lg text-lg font-medium hover:bg-accent-light transition-colors"
            >
              Sign In
            </Link>
          </div>
        </div>
      </section>

      {/* How it works */}
      <section className="border-t border-border pt-16 pb-16 max-w-7xl mx-auto px-8">
        <h2 className="text-3xl font-semibold text-foreground text-center mb-10">How it works</h2>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
          <div className="rounded-xl p-7 transition-shadow duration-200 hover:shadow-md cursor-default" style={{ backgroundColor: "#f5efe6", border: "1px solid #c4b5a0" }}>
            <div className="text-base text-pigeon font-medium mb-2">Instructors</div>
            <h3 className="text-lg font-sem
[truncated — 3136 more characters]
```

### src/app/login/page.tsx

```typescript
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { createClient } from "@/lib/supabaseClient";

export default function LoginPage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError("");
    setLoading(true);

    const supabase = createClient();
    const { error } = await supabase.auth.signInWithPassword({ email, password });

    if (error) {
      setError(error.message);
      setLoading(false);
      return;
    }

    router.push("/");
    router.refresh();
  };

  return (
    <div className="max-w-sm mx-auto mt-24">
      <h1 className="text-2xl font-bold text-foreground mb-1">Sign In</h1>
      <p className="text-sm text-muted mb-6">
        Don&apos;t have an account?{" "}
        <Link href="/signup" className="text-accent hover:underline">
          Sign up
        </Link>
      </p>

      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Email</label>
          <input
            type="email"
            className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </div>
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Password</label>
          <input
            type="password"
            className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
        </div>
        {error && <p className="text-red-500 text-sm">{error}</p>}
        <button
          type="submit"
          disabled={loading}
          className="w-full bg-accent text-white px-4 py-2 rounded text-sm font-medium hover:bg-accent/90 disabled:opacity-50 transition-colors"
        >
          {loading ? "Signing in..." : "Sign In"}
        </button>
      </form>
    </div>
  );
}

```

### src/app/profile/page.tsx

```typescript
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

export default function ProfilePage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [role, setRole] = useState("");
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState("");

  useEffect(() => {
    fetch("/api/profile")
      .then((res) => {
        if (!res.ok) {
          router.push("/login");
          return null;
        }
        return res.json();
      })
      .then((data) => {
        if (data) {
          setEmail(data.email);
          setRole(data.role);
          setFirstName(data.firstName);
          setLastName(data.lastName);
        }
        setLoading(false);
      });
  }, [router]);

  const handleSave = async (e: React.FormEvent) => {
    e.preventDefault();
    setSaving(true);
    setMessage("");

    const res = await fetch("/api/profile", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ firstName, lastName }),
    });

    if (res.ok) {
      setMessage("Profile updated.");
      router.refresh();
    } else {
      const data = await res.json();
      setMessage(data.error || "Failed to save.");
    }
    setSaving(false);
  };

  if (loading) {
    return <p className="text-muted text-sm mt-12 text-center">Loading...</p>;
  }

  return (
    <div className="max-w-sm mx-auto mt-12">
      <h1 className="text-2xl font-bold text-foreground mb-6">Profile</h1>

      <form onSubmit={handleSave} className="space-y-4">
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Email</label>
          <input
            type="email"
            className="border border-border rounded px-3 py-2 text-sm w-full bg-gray-50 text-muted"
            value={email}
            disabled
          />
        </div>
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Role</label>
          <input
            type="text"
            className="border border-border rounded px-3 py-2 text-sm w-full bg-gray-50 text-muted capitalize"
            value={role}
            disabled
          />
        </div>
        <div>
          <label className="text-xs font-medium text-muted block mb-1">First Name</label>
          <input
            type="text"
            className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
            value={firstName}
            onChange={(e) => setFirstName(e.target.value)}
          />
        </div>
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Last Name</label>
          <input
            type="text"
            className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
            value={lastName}
            onChange={(e) => setLastName(e.target.value)}
          />
        </div>
        {message && (
          <p className={`text-sm ${message.includes("updated") ? "text-green-600" : "text-red-500"}`}>
            {message}
          </p>
        )}
        <button
          type="submit"
          disabled={saving}
          className="w-full bg-accent text-white px-4 py-2 rounded text-sm font-medium hover:bg-accent/90 disabled:opacity-50 transition-colors"
        >
          {saving ? "Saving..." : "Save"}
        </button>
      </form>
    </div>
  );
}

```

### src/app/signup/page.tsx

```typescript
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { createClient } from "@/lib/supabaseClient";

export default function SignupPage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [role, setRole] = useState<"student" | "instructor">("student");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError("");
    setLoading(true);

    // Create user via server API (bypasses email confirmation)
    const res = await fetch("/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password, firstName, lastName, role }),
    });
    const result = await res.json();

    if (!res.ok) {
      setError(result.error || "Signup failed");
      setLoading(false);
      return;
    }

    // Now sign in client-side
    const supabase = createClient();
    const { error: signInError } = await supabase.auth.signInWithPassword({
      email,
      password,
    });

    if (signInError) {
      setError(signInError.message);
      setLoading(false);
      return;
    }

    router.push("/");
    router.refresh();
  };

  return (
    <div className="max-w-sm mx-auto mt-24">
      <h1 className="text-2xl font-bold text-foreground mb-1">Create Account</h1>
      <p className="text-sm text-muted mb-6">
        Already have an account?{" "}
        <Link href="/login" className="text-accent hover:underline">
          Sign in
        </Link>
      </p>

      <form onSubmit={handleSubmit} className="space-y-4">
        <div className="flex gap-3">
          <div className="flex-1">
            <label className="text-xs font-medium text-muted block mb-1">First Name</label>
            <input
              type="text"
              className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
              value={firstName}
              onChange={(e) => setFirstName(e.target.value)}
              required
            />
          </div>
          <div className="flex-1">
            <label className="text-xs font-medium text-muted block mb-1">Last Name</label>
            <input
              type="text"
              className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
              value={lastName}
              onChange={(e) => setLastName(e.target.value)}
              required
            />
          </div>
        </div>
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Email</label>
          <input
            type="email"
            className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </div>
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Password</label>
          <input
            type="password"
            className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
            minLength={6}
          />
        </div>
        <div>
          <label className="text-xs font-medium text-muted block mb-1">Role</label>
          <select
            className="border border-border rounded px-3 py-2 text-sm w-full focus:outline-none focus:border-accent"
            value={role}
            onChange={(e) => setRole(e.target.value as "student" | "instructor")}
          >
            <option value="student">Student</option>
            <option value="instructor">Instructor</option>
          </select>
        </div>
        {error && <p className="text-red-500 text-sm">{error}</p>}
        <button
          type="submit"
          disabled={loading}
          className="w-full bg-accent text-white px-4 py-2 rounded text-sm font-medium hover:bg-accent/90 disabled:opacity-50 transition-colors"
        >
          {loading ? "Creating account..." : "Sign Up"}
        </button>
      </form>
    </div>
  );
}

```

### src/app/api/signup/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";

export async function POST(req: NextRequest) {
  const { email, password, firstName, lastName, role } = await req.json();

  // Use service role key to bypass email confirmation
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  );

  const { data, error } = await supabase.auth.admin.createUser({
    email,
    password,
    email_confirm: true,
    user_metadata: { role, first_name: firstName || "", last_name: lastName || "" },
  });

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 400 });
  }

  // Create profile row
  const { error: profileError } = await supabase.from("profiles").upsert({
    id: data.user.id,
    role: role || "student",
    display_name: "",
    first_name: firstName || "",
    last_name: lastName || "",
  });

  if (profileError) {
    return NextResponse.json({ error: profileError.message }, { status: 500 });
  }

  return NextResponse.json({ ok: true });
}

```

### src/app/api/roster/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { createServerClient } from "@/lib/supabaseServer";

export async function GET(req: NextRequest) {
  const courseId = req.nextUrl.searchParams.get("course_id");
  if (!courseId) return NextResponse.json({ error: "course_id is required" }, { status: 400 });

  const supabase = await createServerClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  // Verify instructor owns this course
  const { data: course } = await supabase
    .from("courses")
    .select("owner_id")
    .eq("id", courseId)
    .single();

  if (!course || course.owner_id !== user.id) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }

  // Get enrolled students
  const { data: enrollments, error } = await supabase
    .from("enrollments")
    .select("student_id, enrolled_at")
    .eq("course_id", courseId)
    .order("enrolled_at", { ascending: true });

  if (error) return NextResponse.json({ error: error.message }, { status: 500 });
  if (!enrollments || enrollments.length === 0) return NextResponse.json([]);

  // Get profile info for enrolled students
  const studentIds = enrollments.map((e) => e.student_id);
  const { data: profiles } = await supabase
    .from("profiles")
    .select("id, first_name, last_name")
    .in("id", studentIds);

  const profileMap = new Map(
    (profiles ?? []).map((p) => [p.id, p])
  );

  const roster = enrollments.map((e) => {
    const p = profileMap.get(e.student_id);
    return {
      id: e.student_id,
      first_name: p?.first_name ?? "",
      last_name: p?.last_name ?? "",
      enrolled_at: e.enrolled_at,
    };
  });

  return NextResponse.json(roster);
}

```

### src/app/api/debug-materials/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { createServerClient } from "@/lib/supabaseServer";

export async function GET(req: NextRequest) {
  const courseId = req.nextUrl.searchParams.get("course_id");
  const supabase = await createServerClient();

  // If no course_id, show all courses and their material counts
  if (!courseId) {
    const { data: courses } = await supabase.from("courses").select("id, name, code");
    const { data: allMaterials } = await supabase
      .from("course_materials")
      .select("id, course_id, file_name, extracted_text");

    const summary = (courses ?? []).map((c) => {
      const mats = (allMaterials ?? []).filter((m) => m.course_id === c.id);
      return {
        course_id: c.id,
        course_name: c.name,
        course_code: c.code,
        materials: mats.map((m) => ({
          file_name: m.file_name,
          extracted_text_length: m.extracted_text?.length ?? 0,
          has_text: !!(m.extracted_text && m.extracted_text.length > 0),
          preview: m.extracted_text?.slice(0, 150) || "(empty)",
        })),
      };
    });

    return NextResponse.json(summary, { status: 200 });
  }

  // With course_id, show detailed material info
  const { data, error } = await supabase
    .from("course_materials")
    .select("id, file_name, file_type, category, extracted_text")
    .eq("course_id", courseId);

  if (error) return NextResponse.json({ error: error.message }, { status: 500 });

  const summary = (data ?? []).map((m) => ({
    id: m.id,
    file_name: m.file_name,
    file_type: m.file_type,
    category: m.category,
    extracted_text_length: m.extracted_text?.length ?? 0,
    has_text: !!(m.extracted_text && m.extracted_text.length > 0),
    preview: m.extracted_text?.slice(0, 300) ?? "(empty)",
  }));

  return NextResponse.json(summary);
}

```

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