# Project export: Sandbox

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: Sandbox is an AI-powered teach-back workspace that measures understanding, not recall. Instead of giving answers, it evaluates how well you can reconstruct knowledge visually and verbally.
- Devpost: https://devpost.com/software/sandbox-fsvg1l
- GitHub: https://github.com/Gracemxli/treehacks
- Team: 2 GitHub contributor(s) — Grace Li (10 commits), Richa Misra (2 commits)

## Devpost submission (written by the team)

### Overview

Sandbox: An AI-Powered Teach-Back Workspace That Measures Understanding, Not Recall

### Inspiration

As college students in computational biology and pre-med tracks, we constantly work through dense, complex material - metabolic pathways, signaling cascades, regulatory networks. We realized something unsettling: we could memorize definitions and still struggle to explain the system from scratch. Flashcards optimize for recall. Exams demand reconstruction. The moment that inspired Sandbox was this: If you can’t draw it and teach it without notes, you don’t fully understand it. In a world where AI can instantly give answers, the real differentiator is the ability to explain your thinking. We didn’t want another tool that generates solutions - we wanted a tool that evaluates understanding. Sandbox was built to turn students from answer-consumers into idea-builders.

### What it does

Sandbox is an AI-powered teach-back workspace designed to help students learn by explaining ideas visually and verbally. Instead of asking, “What’s the answer?”, Sandbox asks, “Can you reconstruct the idea?” The core belief is that true understanding comes from being able to teach a concept - not just recall it. The user flow begins with signing in through Supabase authentication and organizing content into folders by subject or topic. Students upload dense material such as PDFs, lecture slides, or notes. When they start a learning session, they ask a question based on their uploaded content and then record themselves drawing and explaining their reasoning. Rather than returning a solution, Sandbox evaluates the student’s explanation. It analyzes conceptual coverage, structural clarity, logical flow, missing components, and potential misconceptions. The goal is not to check whether the answer is correct, but to measure how deeply the student understands the system. We model understanding as something more nuanced than binary accuracy. Instead of grading correctness, Sandbox evaluates understanding as a function of coverage, structure, causal reasoning, and conceptual connections: U = f(coverage, structure, causal reasoning, connections) Sandbox gives feedback on how well a student rebuilt the system from memory - not whether they memorized it.

### How we built it

We architected Sandbox as a full-stack, AI-powered system centered around grounded document understanding and multimodal evaluation. The system is designed to tightly integrate structured content ingestion, semantic retrieval, and real-time teach-back analysis into a single cohesive learning workflow. Rather than building a simple Q&A interface, we engineered a pipeline that transforms static documents into structured, retrievable knowledge representations that power evaluation of student reasoning. On the frontend, we built a modular web application using React and Next.js (v16 with Turbopack), written in TypeScript for type safety and maintainability. The application follows a folder-based architectural structure that mirrors the cognitive model of learning: subjects contain folders, folders contain documents, and documents power sessions. The codebase reflects this modular philosophy, with clearly separated concerns across components such as a custom Canvas for drawing explanations, folder upload panels for document ingestion, dashboard sidebars for structured navigation, subject and folder dialog modals, sandbox session cards, and authentication providers. The frontend handles user authentication (via Supabase), session management, subject/folder organization, document uploads, and recording of drawing-based explanations. Each teach-back session is structured as a stateful interaction, ensuring that visual explanations, prompts, and user metadata are stored in a way that downstream AI evaluation can interpret semantically rather than superficially. On the backend and AI infrastructure layer, we implemented a semantic retrieval system using Pinecone as our vector database. Every uploaded document (PDFs, lecture slides, notes) is parsed, chunked, and embedded into high-dimensional vector representations. These embeddings are stored in Pinecone, enabling contextual retrieval grounded strictly in the user’s uploaded material. This allows Sandbox to generate session questions and evaluate explanations based on semantically relevant content rather than generic model knowledge. By combining structured document ingestion, vector-based retrieval, and multimodal explanation capture (drawing + speech/thought process), Sandbox moves beyond correctness-based grading. The system evaluates conceptual coverage, structural coherence, causal reasoning, and knowledge connections. In other words, we model understanding as a reconstruction problem, not an answer-matching problem. Technically, the architecture cleanly separates: 1) UI/interaction layer (Next.js + React + TypeScript), 2) Authentication and session persistence (Supabase), 3) Document ingestion and preprocessing pipeline, 4) Vector storage and semantic retrieval (Pinecone), 5) AI reasoning layer for teach-back evaluation

### Challenges we ran into

One of our biggest challenges was defining and measuring “understanding.” Grading correctness is easy, but evaluating structure, reasoning, and conceptual completeness is not. We had to formalize what conceptual coverage means, design heuristics for structural feedback, and avoid leaking the “right answer.” This required shifting from answer-generation to reasoning-analysis - building a system that critiques reconstruction instead of outputs. Grounding the AI properly was another core challenge. Our retrieval system needed to be fast, strictly scoped to user-uploaded content, and isolated across users and folders. Designing clean namespace logic in Pinecone was critical to preventing cross-contamination and ensuring context-aware, hallucination-resistant feedback. We also faced multimodal complexity. Aligning drawing input, transcribed speech, and document-grounded retrieval is far more complex than a text-only chat interface. The system had to interpret structure and conceptual flow across modalities. Finally, full-stack integration under hackathon time pressure was intense. We were configuring Supabase authentication, Pinecone indexing, embedding pipelines, Next.js (Turbopack + TypeScript), environment variables, and resolving server conflicts. But the system now runs fully end-to-end.

### Accomplishments we're proud of

First, we built a fully functional teach-back evaluation engine - not a prototype, but a working end-to-end system. A student can upload dense material, trigger document ingestion and embedding, store vectors in Pinecone with clean namespace isolation, start a learning session, ask a grounded question, record a visual explanation, and receive structured reasoning feedback. The pipeline runs seamlessly from embedding to similarity retrieval to evaluation - all grounded strictly in the student’s uploaded content. Second, we successfully integrated a complex full-stack architecture under tight constraints. We combined Supabase authentication and database storage, Pinecone vector indexing, a modular ingestion and embedding pipeline, transcription services, and a React + Next.js (TypeScript, Turbopack) frontend with organized subject/folder/session management. Each backend service — embedding, retrieval, ingestion, evaluation, Supabase access - is modularized, allowing the system to scale and evolve cleanly. Third, and most importantly, we shifted the role of AI in learning. We deliberately avoided building “just another AI tutor” that generates answers. Instead, we built a cognitive feedback system. Sandbox evaluates reasoning - conceptual coverage, structure, causal flow, and connections - rather than correctness. That philosophical shift reframes AI from answer-provider to thinking evaluator, and that’s the core innovation we’re proud of.

### What we learned

Through building Sandbox, we learned that AI is exceptionally good at generating answers - but very few systems are designed to measure thinking. If AI is going to meaningfully support learning, it can’t just produce explanations; it has to evaluate how a student reconstructs ideas. Retrieval grounding proved essential for trust - without strict context isolation and vector-based retrieval, feedback becomes generic or unreliable. We also realized that learning requires productive friction. Students don’t need more information; they need structured reflection that forces them to organize, connect, and rebuild concepts themselves. We also grew as engineers. We architected a modular backend, integrated vector search with user-scoped namespaces, and tested the system using both white-box validation of service logic and black-box simulation of full user workflows. Under time pressure, we debugged environment variables, API key issues, and index mismatches while maintaining system integrity. More importantly, we learned how to communicate clearly when systems broke, divide responsibilities based on team strengths, and make architectural decisions collaboratively. Building Sandbox wasn’t just about writing code - it was about thinking systematically, testing rigorously, and executing as a team.

### What's next

Looking ahead, our vision for Sandbox is to transform it from a powerful prototype into the default way students test mastery. We plan to introduce structured evaluation dashboards that score and visualize conceptual coverage over time, allowing students to compare teach-back sessions and track longitudinal gaps in understanding. We also want to add a collaborative “teach each other” mode and expand beyond STEM into domains like law, language learning, and test preparation - making Sandbox a universal cognitive feedback platform. On the AI side, we aim to deepen multimodal alignment between drawing and speech, enabling more precise interpretation of diagram structure and conceptual flow. We plan to implement concept graph reconstruction, automated misconception detection, and adaptive follow-up questioning that dynamically probes weak areas. The goal is to evolve Sandbox from structured feedback into an intelligent reasoning coach. From a systems perspective, we plan to optimize our Pinecone indexing strategy for scale, introduce caching layers to reduce retrieval latency, refine embedding chunking strategies for better semantic coherence, and expand our Supabase schema to support richer session analytics. We also intend to move toward production-grade infrastructure beyond a development server, preparing Sandbox for scalable deployment. Loom Video: https://www.loom.com/share/5001005d45774bc4a88b705fb97ed9fa

## README (from the GitHub repository)

# Interactive Study Tool

A production-ready MVP web application for interactive studying with voice practice, visual notes, and AI-powered feedback.

## Project Structure

```
treehacks/
├── web/          # Next.js 14+ frontend (App Router, TypeScript, Tailwind)
└── api/          # FastAPI backend (Python)
```

## Features

- **Document Upload**: Support for PDF, TXT, and DOCX files
- **Link Processing**: Extract content from URLs using Perplexity API
- **Interactive Study Session**: 
  - Blank note page with typed notes and canvas drawing
  - AI-generated study questions
  - 3-minute timed speaking practice
  - Real-time speech transcription (Web Speech API with audio fallback)
  - Automatic snapshot capture every 5 seconds
- **AI Feedback**: Structured evaluation with scores, strengths, gaps, and suggestions

## Prerequisites

- Node.js 18+ and npm/yarn
- Python 3.9+
- API Keys:
  - Pinecone (for vector storage)
  - Anthropic Claude (for question generation and evaluation)
  - Perplexity (for link content extraction)
  - OpenAI (for embeddings)

## Setup Instructions

### Backend (FastAPI)

1. Navigate to the API directory:
```bash
cd api
```

2. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
```

3. Install dependencies:
```bash
pip install -r requirements.txt
```

4. Create `.env` file (copy from `.env.example`):
```bash
cp .env.example .env
```

5. Edit `.env` and add your API keys:
```
PINECONE_API_KEY=your_key_here
PINECONE_INDEX=your_index_name
PINECONE_ENV=us-east-1-aws
ANTHROPIC_API_KEY=your_key_here
PERPLEXITY_API_KEY=your_key_here
OPENAI_API_KEY=your_key_here
EMBEDDING_MODEL=text-embedding-3-small
```

6. Start the FastAPI server:
```bash
python main.py
# Or: uvicorn main:app --reload --host 0.0.0.0 --port 8000
```

The API will be available at `http://localhost:8000`

### Frontend (Next.js)

1. Navigate to the web directory:
```bash
cd web
```

2. Install dependencies:
```bash
npm install
# Or: yarn install
```

3. Create `.env.local` file:
```bash
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
```

4. Start the development server:
```bash
npm run dev
# Or: yarn dev
```

The app will be available at `http://localhost:3000`

## Usage

1. **Upload Documents**: Go to the home page and upload PDF/TXT/DOCX files or paste URLs
2. **Start Session**: Click "Process & Start Session" to begin
3. **Study**: On the session page, you'll see a generated question
4. **Take Notes**: Type notes and draw on the canvas
5. **Practice**: Click "Start 3-min Response" to begin recording
6. **Review Feedback**: After recording, view AI-generated feedback

## API Endpoints

### POST /ingest
Upload documents and links for processing.

**Request:**
- `files`: Multipart form data (PDF, TXT, DOCX)
- `links`: JSON array of URLs

**Response:**
```json
{
  "session_id": "uuid",
  "chunks_processed": 10
}
```

### GET /question?session_id={session_id}
Get a generated study question.

**Response:**
```json
{
  "question": "Explain the key concepts..."
}
```

### POST /evaluate
Evaluate a response with transcript and snapshots.

**Request:**
```json
{
  "transcript": "User's spoken response...",
  "snapshots": [
    {
      "t": 1234567890,
      "mime": "image/jpeg",
      "dataBase64": "base64_encoded_image"
    }
  ],
  "session_id": "uuid",
  "audio_base64": "optional_base64_audio"
}
```

**Response:**
```json
{
  "score_overall": 8,
  "strengths": ["Clear explanation", "Good structure"],
  "gaps": ["Missing examples", "Could be more detailed"],
  "suggested_better_structure": ["Start with overview", "Use examples"],
  "next_question": "Follow-up question..."
}
```

## Canvas Features

- **Pen Tool**: Draw with adjustable thickness
- **Eraser Tool**: Erase parts of the drawing
- **Undo**: Undo last stroke
- **Clear**: Clear entire canvas

## Speech Transcription

- **Primary**: Web Speech API (browser-native, real-time)
- **Fallback**: Audio recording sent to backend for processing (if Web Speech API unavailable)

## Notes

- Snapshots are captured every 5 seconds during recording
- All API keys are stored server-side for security
- CORS is configured to allow the Next.js frontend origin
- The app uses Pinecone for vector storage and retrieval

## Troubleshooting

1. **CORS Errors**: Ensure the FastAPI server allows your frontend origin
2. **Microphone Access**: Grant browser permissions for microphone
3. **API Errors**: Check that all API keys are correctly set in `.env`
4. **Pinecone**: Ensure your Pinecone index exists and has the correct dimension (1536 for text-embedding-3-small)

## License

MIT



## Detected evidence (automated analysis)

Indexed codebase: 52 recognized source files, 178 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — 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 (58 of 58)

```
API_KEYS_GUIDE.md
api/.gitignore
api/check_env.py
api/main.py
api/requirements.txt
api/routers/__init__.py
api/routers/evaluate.py
api/routers/ingest.py
api/routers/question.py
api/services/__init__.py
api/services/claude_service.py
api/services/document_processor.py
api/services/embedding_service.py
api/services/perplexity_service.py
api/services/pinecone_service.py
api/services/supabase_service.py
api/services/transcription_service.py
IMPLEMENTATION_SUMMARY.md
migration_add_sandbox_fields.sql
migration_folder_suggested_topics.sql
migration_subjects_folders_topics.sql
PROJECT_STRUCTURE.md
README.md
SETUP.md
supabase_schema.sql
SUPABASE_SETUP.md
web/.env.example
web/.gitignore
web/app/auth/page.tsx
web/app/globals.css
web/app/layout.tsx
web/app/page.tsx
web/app/sandbox/[sandboxId]/evaluations/page.tsx
web/app/session/[sessionId]/page.tsx
web/app/upload/page.tsx
web/components/add-folder-dialog.tsx
web/components/auth-form.tsx
web/components/auth-provider.tsx
web/components/Canvas.tsx
web/components/create-subject-dialog.tsx
web/components/dashboard-sidebar.tsx
web/components/folder-upload-panel.tsx
web/components/sandbox-card.tsx
web/components/ui/badge.tsx
web/components/ui/button.tsx
web/components/ui/card.tsx
web/components/ui/dialog.tsx
web/components/ui/input.tsx
web/components/ui/sidebar.tsx
web/lib/supabase/client.ts
web/lib/supabase/server.ts
web/lib/utils.ts
web/next.config.js
web/package.json
web/postcss.config.js
web/SHADCN_SETUP.md
web/tailwind.config.ts
web/tsconfig.json
```

### Dependencies

- api/requirements.txt: anthropic@==0.34.2, fastapi@==0.111.0, httpx@>=0.24,<0.26, numpy@==1.26.4, openai@==1.40.0, pinecone-client@==5.0.1, pydantic@==2.8.2, pypdf@==4.0.1, python-docx@==1.1.2, python-dotenv@==1.0.1, python-multipart@==0.0.9, supabase@==2.3.4, tiktoken@==0.7.0, uvicorn[standard]@==0.30.1
- web/package.json: @supabase/ssr@^0.8.0, @supabase/supabase-js@^2.39.0, @types/node@^20.14.10, @types/react@^18.3.3, @types/react-dom@^18.3.0, autoprefixer@^10.4.19, clsx@^2.1.1, html2canvas@^1.4.1, lucide-react@^0.344.0, next@^16.1.6, postcss@^8.4.38, react@^18.3.1, react-dom@^18.3.1, tailwind-merge@^2.6.1, tailwindcss@^3.4.4, typescript@^5.5.3, zustand@^4.5.2

### Recent commits (newest first)

- Update Version
- Merge pull request #5 from Gracemxli/fix-session-page
- hi
- fix session page
- Merge pull request #3 from Gracemxli/database
- user auth
- Merge pull request #2 from Gracemxli/test-capital
- made the audio work
- Merge pull request #1 from Gracemxli/richa
- fix cclaude
- First Commit
- fix
- init push

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

### SUPABASE_SETUP.md

```markdown
# Supabase Setup Guide

This guide will help you set up Supabase for the Interactive Study Tool.

## Prerequisites

1. A Supabase account (sign up at https://supabase.com)
2. A Supabase project created

## Step 1: Create Database Tables

1. Go to your Supabase project dashboard
2. Navigate to the SQL Editor
3. Copy and paste the contents of `supabase_schema.sql` into the editor
4. Run the SQL script to create all tables, indexes, and RLS policies

## Step 2: Create Storage Bucket

1. Go to Storage in your Supabase dashboard
2. Create a new bucket named `sandbox-screenshots`
3. Set it to **Public** (or configure RLS policies if you prefer private)
4. If public, you can skip RLS policies. If private, add policies to allow authenticated users to upload/read

## Step 3: Get Your Supabase Credentials

1. Go to Project Settings > API
2. Copy the following:
   - **Project URL** (this is your `SUPABASE_URL`)
   - **anon/public key** (this is your `NEXT_PUBLIC_SUPABASE_ANON_KEY`)
   - **service_role key** (this is your `SUPABASE_SERVICE_ROLE_KEY` - keep this secret!)

## Step 4: Configure Environment Variables

### Frontend (web/.env.local)

Create or update `web/.env.local`:

```env
NEXT_PUBLIC_SUPABASE_URL=your_project_url_here
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key_here
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
```

### Backend (api/.env)

Create or update `api/.env`:

```env
SUPABASE_URL=your_project_url_here
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
# ... your other environment variables
```

## Step 5: Install Dependencies

### Frontend
```bash
cd web
npm install
```

### Backend
```bash
cd api
source venv/bin/activate  # or your virtual environment
pip install -r requirements.txt
```

## Step 6: Test the Setup

1. Start your backend server:
   ```bash
   cd api
   uvicorn main:app --reload
   ```

2. Start your frontend server:
   ```bash
   cd web
   npm run dev
   ```

3. Navigate to `http://localhost:3000/auth` and try signing up/signing in
4. Create a subject and sandbox to verify everything works

## Features Implemented

✅ User authentication (sign up/sign in)
✅ Subjects CRUD (create, read, update, delete)
✅ Sandboxes CRUD with last screenshot storage
✅ Evaluations storage with all feedback data
✅ Row Level Security (RLS) policies for data isolation
✅ Automatic screenshot saving to Supabase Storage

## Database Schema

- **subjects**: User's study subjects
- **sandboxes**: Learning sandboxes (study sessions)
- **evaluations**: Complete evaluation data including transcript, audio, snapshots, and feedback

## Security Notes

- RLS policies ensure users can only access their own data
- Service role key should NEVER be exposed to the frontend
- Anon key is safe to use in the frontend (RLS policies protect data)


```

### API_KEYS_GUIDE.md

```markdown
# Supabase API Keys - Where Each Key Goes

Based on your Supabase dashboard, here's exactly where to put each key:

## 🔑 Key Mapping

### From Your Supabase Dashboard:

1. **Publishable Key** (`sb_publishable_...`)
   - This is the **public key** safe for browser use
   - Maps to: `NEXT_PUBLIC_SUPABASE_ANON_KEY` (frontend)

2. **Secret Key** (`sb_secret_...`)
   - This is the **private key** for backend/privileged access
   - Maps to: `SUPABASE_SERVICE_ROLE_KEY` (backend)

3. **Project URL**
   - Found at the top of your dashboard or in Project Settings
   - Maps to: `SUPABASE_URL` (both frontend and backend)

## 📍 Where Each Key Goes

### Frontend (`web/.env.local`)

```env
# Project URL (same for both frontend and backend)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co

# Publishable Key (from "Publishable key" section)
NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_MAWebNquu6PXFmbo4gE6yQ_VFp_J...

# API Base URL
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
```

**Used in:**
- `web/lib/supabase/client.ts` - Browser-side Supabase client
- `web/lib/supabase/server.ts` - Server-side Supabase client (Next.js server components)

### Backend (`api/.env`)

```env
# Project URL (same as frontend)
SUPABASE_URL=https://your-project-id.supabase.co

# Secret Key (from "Secret keys" section - click eye icon to reveal)
SUPABASE_SERVICE_ROLE_KEY=sb_secret_82SoX...

# Your other API keys...
ANTHROPIC_API_KEY=...
OPENAI_API_KEY=...
PINECONE_API_KEY=...
```

**Used in:**
- `api/services/supabase_service.py` - Backend Supabase service

## 🔒 Security Notes

- ✅ **Publishable Key**: Safe to expose in frontend code (starts with `NEXT_PUBLIC_`)
  - Protected by Row Level Security (RLS) policies
  - Can only access data the user is authorized to see

- 🔐 **Secret Key**: **NEVER expose this in frontend code!**
  - Only use in backend/server code
  - Has full access to your database (bypasses RLS)
  - Keep it in `.env` file (which is gitignored)

## 📝 Quick Setup Checklist

1. ✅ Copy **Project URL** → `NEXT_PUBLIC_SUPABASE_URL` (frontend) and `SUPABASE_URL` (backend)
2. ✅ Copy **Publishable Key** → `NEXT_PUBLIC_SUPABASE_ANON_KEY` (frontend)
3. ✅ Copy **Secret Key** (click eye icon to reveal) → `SUPABASE_SERVICE_ROLE_KEY` (backend)
4. ✅ Create `web/.env.local` with frontend keys
5. ✅ Add backend keys to `api/.env`

## 🎯 File Locations Summary

| Key | Environment Variable | File Location | Used By |
|-----|---------------------|---------------|---------|
| Project URL | `NEXT_PUBLIC_SUPABASE_URL` | `web/.env.local` | Frontend Supabase client |
| Project URL | `SUPABASE_URL` | `api/.env` | Backend Supabase service |
| Publishable Key | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | `web/.env.local` | Frontend Supabase client |
| Secret Key | `SUPABASE_SERVICE_ROLE_KEY` | `api/.env` | Backend Supabase service |


```

### api/requirements.txt

```
fastapi==0.111.0
uvicorn[standard]==0.30.1
python-multipart==0.0.9
pydantic==2.8.2
pinecone-client==5.0.1
anthropic==0.34.2
openai==1.40.0
pypdf==4.0.1
python-docx==1.1.2
# httpx 0.25.x required for supabase 2.3.4 (supabase needs <0.26); also satisfies fastapi, anthropic, openai
httpx>=0.24,<0.26
python-dotenv==1.0.1
numpy==1.26.4
tiktoken==0.7.0
supabase==2.3.4


```

### web/package.json

```
{
  "name": "interactive-study-tool",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@supabase/ssr": "^0.8.0",
    "@supabase/supabase-js": "^2.39.0",
    "clsx": "^2.1.1",
    "html2canvas": "^1.4.1",
    "lucide-react": "^0.344.0",
    "next": "^16.1.6",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "tailwind-merge": "^2.6.1",
    "zustand": "^4.5.2"
  },
  "devDependencies": {
    "@types/node": "^20.14.10",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "autoprefixer": "^10.4.19",
    "postcss": "^8.4.38",
    "tailwindcss": "^3.4.4",
    "typescript": "^5.5.3"
  }
}

```

### api/main.py

```python
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from typing import List, Optional
import uvicorn
import os
from dotenv import load_dotenv

from routers import ingest, question, evaluate

load_dotenv()

app = FastAPI(title="Interactive Study Tool API")

# CORS configuration
origins = [
    "http://localhost:3000",
    "http://localhost:3001",
    "http://127.0.0.1:3000",
]

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

# Include routers
app.include_router(ingest.router, prefix="/ingest", tags=["ingest"])
app.include_router(question.router, prefix="/question", tags=["question"])
app.include_router(evaluate.router, prefix="/evaluate", tags=["evaluate"])


@app.get("/")
async def root():
    return {"message": "Interactive Study Tool API"}


@app.get("/health")
async def health():
    return {"status": "healthy"}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)


```

### web/app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";
import { AuthProvider } from "@/components/auth-provider";

export const metadata: Metadata = {
  title: "Interactive Study Tool",
  description: "AI-powered study tool with voice practice and visual notes",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}


```

### web/app/page.tsx

```typescript
"use client";

import { useState, useEffect, useMemo } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { DashboardSidebar } from "@/components/dashboard-sidebar";
import { SandboxCard } from "@/components/sandbox-card";
import { CreateSubjectDialog } from "@/components/create-subject-dialog";
import { AddFolderDialog } from "@/components/add-folder-dialog";
import { FolderUploadPanel } from "@/components/folder-upload-panel";
import { Button } from "@/components/ui/button";
import { Plus, LogOut } from "lucide-react";
import { useAuth } from "@/components/auth-provider";
import { supabase } from "@/lib/supabase/client";

interface Subject {
  id: string;
  name: string;
  color: string;
  suggested_topics?: string[] | null;
}

interface Folder {
  id: string;
  name: string;
  subject_id: string;
  suggested_topics?: string[] | null;
}

interface Sandbox {
  id: string;
  title: string;
  date: string;
  imageUrl?: string;
  subjectId: string | null;
  folderId?: string | null;
}

export default function Home() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const { user, loading: authLoading, signOut } = useAuth();
  const [subjects, setSubjects] = useState<Subject[]>([]);
  const [sandboxes, setSandboxes] = useState<Sandbox[]>([]);
  const [folders, setFolders] = useState<Folder[]>([]);
  const [activeSubjectId, setActiveSubjectId] = useState<string | null>(null);
  const [activeFolderId, setActiveFolderId] = useState<string | null>(null);
  const [selectedTopicForNew, setSelectedTopicForNew] = useState<string>("");
  const [createSubjectOpen, setCreateSubjectOpen] = useState(false);
  const [addFolderOpen, setAddFolderOpen] = useState(false);
  const [loading, setLoading] = useState(true);
  const [folderSuggestedTopicsFromUpload, setFolderSuggestedTopicsFromUpload] = useState<Record<string, string[]>>({});

  useEffect(() => {
    if (!authLoading && !user) {
      router.push("/auth");
    }
  }, [user, authLoading, router]);

  const fetchSubjectsAndSandboxes = async () => {
    if (!user) return;
    try {
      const { data: subjectsData, error: subjectsError } = await supabase
        .from("subjects")
        .select("*")
        .order("created_at", { ascending: true });

      if (subjectsError) throw subjectsError;
      if (subjectsData) {
        setSubjects(subjectsData);
        if (subjectsData.length > 0 && !activeSubjectId) {
          setActiveSubjectId(subjectsData[0].id);
        }
      }

      const { data: sandboxesData, error: sandboxesError } = await supabase
        .from("sandboxes")
        .select("*")
        .order("created_at", { ascending: false });

      if (sandboxesError) throw sandboxesError;
      if (sandboxesData) {
        setSandboxes(
          sandboxesData.map((sb) => ({
            id: sb.id,
            title: sb.title,
            date: new Date(sb.created_at).toLocaleDateString("en-US", {
              month: "long",
              day: "numeric",
              year: "numeric",
            }),
            imageUrl: sb.last_screenshot_url || undefined,
            subjectId: sb.subject_id,
            folderId: sb.folder_id ?? null,
          }))
        );
      }
    } catch (error) {
      console.error("Error fetching data:", error);
    } finally {
      setLoading(false);
    }
  };

  // Refetch when user is set and whenever we're on the home page (e.g. after returning from a session)
  useEffect(() => {
    if (!user) return;
    if (pathname === "/") {
      setLoading(true);
      fetchSubjectsAndSandboxes();
      // If we landed with ?refetch=1 (e.g. from "Back to Home"), clean URL after refetch
      if (searchParams.get("refetch")) {
        router.replace("/", { scroll: false });
      }
    }
  }, [user, pathname]);

  // Refetch when user returns to this tab. Do NOT set loading=true so we don't unmount the
  // folder upload form when the file picker closes (focus event would clear selected files).
  useEffect(() => {
    if (!user || pathname !== "/") return;
    const onFocus = () => fetchSubjectsAndSandboxes();
    window.addEventListener("focus", onFocus);
    return () => window.removeEventListener("focus", onFocus);
  }, [user, pathname]);

  useEffect(() => {
    if (!user) return;
    const fetchFolders = async () => {
      const { data, error } = await supabase
        .from("subject_folders")
        .select("*")
        .order("name");
      if (!error && data) setFolders(data);
      else setFolders([]);
    };
    fetchFolders();
  }, [user]);

  const foldersBySubjectId = useMemo(() => {
    const map: Record<string, Folder[]> = {};
    for (const f of folders) {
      if (!map[f.subject_id]) map[f.subject_id] = [];
      map[f.subject_id].push(f);
    }
    return map;
  }, [folders]);

  const handleNewSandbox = () => {
    const params = new URLSearchParams();
    if (activeSubjectId) params.set("subject_id", activeSubjectId);
    if (activeFolderId) params.set("folder_id", activeFolderId);
    if (selectedTopicForNew) params.set("topic", selectedTopicForNew);
    router.push(`/upload?${params.toString()}`);
  };

  const handleSandboxClick = (sandboxId: string) => {
    router.push(`/sandbox/${sandboxId}/evaluations`);
  };

  const activeSubject = subjects.find((s) => s.id === activeSubjectId);
  const activeFolder = folders.find((f) => f.id === activeFolderId);
  const suggestedTopicsFromSubject = (activeSubject?.suggested_topics as string[] | null) ?? [];
  const suggestedTopicsFromFolder = (activeFolder?.suggested_topics as string[] | null) ?? [];
  const suggestedTopicsFromUpload = activeFolderId ? folderSuggestedTopicsFromUpload[activeFolderId] ?? [] : [];
  const suggestedTopics = activeFolderId && (suggestedTopicsFromUpload.length > 0 || suggestedTopicsFromFolder.length > 0)
    ? (suggestedTopicsFromUpload.length > 0 ? suggestedTopicsFromUpload : suggestedTopicsFromFolder)
    : sugge
[truncated — 6085 more characters]
```

### web/app/auth/page.tsx

```typescript
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/components/auth-provider";
import { AuthForm } from "@/components/auth-form";

export default function AuthPage() {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && user) {
      router.push("/");
    }
  }, [user, loading, router]);

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-xl">Loading...</div>
      </div>
    );
  }

  if (user) {
    return null;
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
      <AuthForm />
    </div>
  );
}


```

### web/lib/supabase/server.ts

```typescript
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}


```

### web/app/upload/page.tsx

```typescript
"use client";

import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import { useAuth } from "@/components/auth-provider";
import { supabase } from "@/lib/supabase/client";

interface LinkInput {
  id: string;
  url: string;
}

interface Subject {
  id: string;
  name: string;
  color: string;
}

export default function UploadPage() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { user, loading: authLoading } = useAuth();
  const [files, setFiles] = useState<File[]>([]);
  const [links, setLinks] = useState<LinkInput[]>([]);
  const [isProcessing, setIsProcessing] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [title, setTitle] = useState("");
  const [topic, setTopic] = useState("");
  const [subjects, setSubjects] = useState<Subject[]>([]);
  const [folders, setFolders] = useState<{ id: string; name: string }[]>([]);
  const [selectedSubjectId, setSelectedSubjectId] = useState<string | null>(null);
  const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);

  useEffect(() => {
    if (!authLoading && !user) {
      router.push("/auth");
    }
  }, [user, authLoading, router]);

  useEffect(() => {
    const subjectIdParam = searchParams.get("subject_id");
    const folderIdParam = searchParams.get("folder_id");
    const topicParam = searchParams.get("topic");
    if (subjectIdParam) setSelectedSubjectId(subjectIdParam);
    if (folderIdParam) setSelectedFolderId(folderIdParam);
    if (topicParam) setTopic(decodeURIComponent(topicParam));
  }, [searchParams]);

  useEffect(() => {
    if (!user) return;

    const fetchSubjects = async () => {
      const { data, error } = await supabase
        .from("subjects")
        .select("*")
        .order("created_at", { ascending: true });

      if (error) {
        console.error("Error fetching subjects:", error);
      } else if (data) {
        setSubjects(data);
        if (data.length > 0 && !selectedSubjectId) {
          setSelectedSubjectId(data[0].id);
        }
      }
    };

    fetchSubjects();
  }, [user, selectedSubjectId]);

  useEffect(() => {
    if (!selectedSubjectId) {
      setFolders([]);
      return;
    }
    const fetchFolders = async () => {
      const { data, error } = await supabase
        .from("subject_folders")
        .select("id, name")
        .eq("subject_id", selectedSubjectId)
        .order("name");
      if (!error && data) setFolders(data);
      else setFolders([]);
    };
    fetchFolders();
  }, [selectedSubjectId]);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      setFiles(Array.from(e.target.files));
    }
  };

  const addLink = () => {
    setLinks([...links, { id: Date.now().toString(), url: "" }]);
  };

  const removeLink = (id: string) => {
    setLinks(links.filter((link) => link.id !== id));
  };

  const updateLink = (id: string, url: string) => {
    setLinks(links.map((link) => (link.id === id ? { ...link, url } : link)));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!user) {
      router.push("/auth");
      return;
    }

    setIsProcessing(true);
    setError(null);

    try {
      const formData = new FormData();
      
      // Add files
      files.forEach((file) => {
        formData.append("files", file);
      });

      // Add links
      const validLinks = links.filter((link) => link.url.trim() !== "");
      formData.append("links", JSON.stringify(validLinks.map((l) => l.url)));

      // Add user_id, subject_id, and title
      formData.append("user_id", user.id);
      if (selectedSubjectId) formData.append("subject_id", selectedSubjectId);
      if (selectedFolderId) formData.append("folder_id", selectedFolderId);
      if (title.trim()) formData.append("title", title.trim());
      if (topic.trim()) formData.append("topic", topic.trim());

      const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000";
      const response = await fetch(`${apiBaseUrl}/ingest`, {
        method: "POST",
        body: formData,
      });

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      const sessionId = data.session_id as string;

      // Ensure sandbox row exists in Supabase so it shows on dashboard and in the correct folder when revisited
      const sandboxTitle = title.trim() || topic.trim() || `Sandbox ${sessionId.slice(0, 8)}`;
      await supabase.from("sandboxes").upsert(
        {
          id: sessionId,
          user_id: user.id,
          subject_id: selectedSubjectId || null,
          title: sandboxTitle,
          ...(selectedFolderId && { folder_id: selectedFolderId }),
        },
        { onConflict: "id" }
      );

      router.push(`/session/${sessionId}`);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to process documents");
      setIsProcessing(false);
    }
  };

  return (
    <div className="min-h-screen bg-background p-8">
      <div className="mx-auto max-w-4xl">
        <Link href="/" className="mb-6 inline-flex items-center text-sm text-muted-foreground hover:text-foreground">
          <ArrowLeft className="mr-2 h-4 w-4" />
          Back to Dashboard
        </Link>

        <Card>
          <CardHeader>
            <CardTitle>Create New Learning Sandbox</CardTitle>
            <CardDescription>
              Upload documents or add links to start your study session
            </CardDescripti
[truncated — 5419 more characters]
```

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